gates.completion
The active task's own change set — from the commit it started at to the working tree, uncommitted files included — put through the CI model: which gates it selects and why, what each one last reported and over which files, which verdicts have gone stale because the tree moved underneath them, which required gates have never reported at all, and which obligations the change implies whether or not the task declared them. `finishable` is false only when a required gate is known to be failing, stale or blocked by one that is; absence of a verdict is reported as unverified rather than silently accepted.
The active task's own change set — from the commit it started at to the working tree, uncommitted files included — put through the CI model: which gates it selects and why, what each one last reported and over which files, which verdicts have gone stale because the tree moved underneath them, which required gates have never reported at all, and which obligations the change implies whether or not the task declared them. `finishable` is false only when a required gate is known to be failing, stale or blocked by one that is; absence of a verdict is reported as unverified rather than silently accepted.
- query
- behaviorally_verified
- module gates
- #gates
- #completion
- #tasks
- #evidence
Exposure
Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.
| surface | as |
|---|---|
| MCP | tool majordomus_completion resource majordomus://gates/completion |
| HTTP | GET /api/v1/gates/completion operationId gates.completion |
| command line | — |
Schemas
The canonical input and output, from the Rust types; the MCP tool schema and the OpenAPI parameters and responses are derived from these.
input · CompletionInput
use majordomus_cli::capability::builtin::gates::CompletionInput;
// the default asks about the active task's own change set
let mine = CompletionInput::default();
assert!(mine.changed.is_none() && mine.base.is_none());
// and a caller may name the change instead, which is how a test drives it
let named = CompletionInput {
changed: Some(vec!["docs/CLI.md".into()]),
..Default::default()
};
assert_eq!(named.changed.unwrap(), ["docs/CLI.md"]);
Which change to judge completion over.
| property | type | required | description |
|---|---|---|---|
| base | string | null | no | The commit to compare with, when the task's own starting commit is not wanted. |
| changed | array | null | no | The changed paths, repository-relative. Absent means the task's own change set: everything between the commit the task started at and the working tree. |
| on_demand | boolean | null | no | Also plan the gates the model marks on-demand. Off by default, because a plan that selects a gate whose runner is unavailable is a plan whose verdict never arrives. |
JSON Schema
{
"additionalProperties": false,
"description": "```\nuse majordomus_cli::capability::builtin::gates::CompletionInput;\n\n// the default asks about the active task's own change set\nlet mine = CompletionInput::default();\nassert!(mine.changed.is_none() && mine.base.is_none());\n\n// and a caller may name the change instead, which is how a test drives it\nlet named = CompletionInput {\n changed: Some(vec![\"docs/CLI.md\".into()]),\n ..Default::default()\n};\nassert_eq!(named.changed.unwrap(), [\"docs/CLI.md\"]);\n```\nWhich change to judge completion over.",
"properties": {
"base": {
"description": "The commit to compare with, when the task's own starting commit is not wanted.",
"type": [
"string",
"null"
]
},
"changed": {
"description": "The changed paths, repository-relative. Absent means the task's own change set:\neverything between the commit the task started at and the working tree.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"on_demand": {
"description": "Also plan the gates the model marks on-demand. Off by default, because a plan that\nselects a gate whose runner is unavailable is a plan whose verdict never arrives.",
"type": [
"boolean",
"null"
]
}
},
"title": "CompletionInput",
"type": "object"
}output · Completion
use majordomus_cli::gates::Completion;
// the document every surface reads, as `gates.completion` answers with it. The property
// that matters is the one asserted here: a gate that never reported leaves the task
// finishable and *unverified*, which is a different thing from a task that passed.
let c: Completion = serde_json::from_value(serde_json::json!({
"present": true,
"model": ".ai/repo/ci/gates.yaml",
"plan": { "mode": "affected", "reason": "1 path in 1 class", "changed": ["docs/CLI.md"],
"classes": [], "unclassified": [], "selected": {}, "excluded": {} },
"finishable": true,
"unverified": ["reference-check"],
"tallies": { "queued": 1 },
"gates": [],
"obligations": [],
"questions": []
}))
.unwrap();
assert!(c.finishable, "absence of a verdict does not refuse");
assert_eq!(c.unverified, ["reference-check"], "and it is never silently accepted");
assert!(c.blocking.is_empty());
Whether the active task may be called finished, and everything the answer rests on.
finishable is the field a caller acts on and the one thing no surface may recompute:
the shell tool's check and finish read it, the HTTP API serves it, the Cockpit
renders it, and all of them are reading this one execution.
| property | type | required | description |
|---|---|---|---|
| blocking | array | no | The required gates whose status refuses completion. |
| findings | array | no | What could not be established, each as one line. Never empty when something was skipped: a gap is reported rather than left to be inferred. |
| finishable | boolean | yes | False when a required gate is known to be failing, stale, or blocked by one that is.
Absence of a verdict does not make this false — it makes unverified non-empty. |
| gates | array | yes | Every gate of the model, what refuses first. |
| model | string | yes | The CI model this was judged against. |
| obligations | array | yes | Which obligations the change set implies, and whether the task promised them. The
judgement of a declared one is obligations.closure's, not this module's. |
| plan | reference | yes | Affected or full, and why. |
| present | boolean | yes | True when a task record was found in this checkout. |
| questions | array | yes | The done invariant: the questions a task must answer before anybody may call it
finished, each with the source that answered it and the evidence it read. Composed
from the obligation closure, the gates, the change set, the repository's convergence
and the closing readings; a question nothing here reaches is unknown and names the
command that would answer it. |
| tallies | object | yes | How many gates stand in each status, by word. |
| task | one of 2 | no | The task, as its record declares it. |
| unverified | array | no | The required gates that have never reported, or cannot be judged here. A task finished over these is finished unverified, and the record says so. |
JSON Schema
{
"$defs": {
"ActiveTask": {
"description": "The active task of this checkout, when there is one.",
"properties": {
"binding": {
"description": "The standing `intents.binding` answered when the task started — what `start` was\ntold then, not where the work stands now.",
"type": "string"
},
"evidence_standing": {
"description": "The pin of where the served criteria's evidence stood when the task started.",
"type": "string"
},
"exemption": {
"description": "The exemption class the worker gave instead (`start --exempt`).",
"type": "string"
},
"exemption_because": {
"description": "The reason given with the exemption.",
"type": "string"
},
"head": {
"description": "The commit it started at.",
"type": "string"
},
"id": {
"description": "The task id.",
"type": "string"
},
"intent": {
"description": "The intent the worker said this task serves (`start --intent`).",
"type": "string"
},
"issue": {
"description": "The issue the worker said this task executes (`start --issue`). A name, never state:\nwhat it serves is answered by `intents.binding` on every read (ADR 0111).",
"type": "string"
},
"outcome": {
"description": "Its typed outcome so far: `active`, `handed_over`, or a finished one.",
"type": "string"
},
"plan_revision": {
"description": "The pin of what the work was for when the task started.",
"type": "string"
},
"profile": {
"description": "The execution profile it runs under.",
"type": "string"
},
"requires": {
"description": "The obligations it owes before the outcome `completed` is available.\n\nBeside `scope` and not inside it, because the two are different promises: scope is\ncontainment — where a worker may write — and this is delivery. A change can sit\nentirely inside its scope and still be uncommitted on a laptop (ADR 0030).\n`super::obligations` is what judges each of these against its evidence; here it\nis reported as declared.",
"items": {
"type": "string"
},
"type": "array"
},
"scope": {
"description": "The paths it claims.",
"items": {
"type": "string"
},
"type": "array"
},
"started_at": {
"description": "When it started.",
"type": "string"
},
"task": {
"description": "What is being worked on.",
"type": "string"
}
},
"required": [
"id",
"task",
"profile",
"outcome"
],
"type": "object"
},
"DoneQuestion": {
"description": "```\nuse majordomus_cli::gates::{DoneQuestion, GateStatus};\n\nlet q = DoneQuestion {\n id: \"pushed\".into(),\n question: \"Has the commit reached the remote?\".into(),\n status: GateStatus::Queued,\n evidence: \"the obligation is owed\".into(),\n source: \"obligation push (obligations.closure)\".into(),\n remediation: \"git push\".into(),\n};\n// every question names what answered it and what would settle it: a question with\n// neither is a checklist line, which is the artifact this type exists instead of\nassert!(!q.source.is_empty() && !q.remediation.is_empty());\nassert!(q.status.unverified());\n```\nOne question of the done invariant, and what answered it.",
"properties": {
"evidence": {
"description": "What was actually read — never a restatement of the status.",
"type": "string"
},
"id": {
"description": "The question's identity, stable across reports.",
"type": "string"
},
"question": {
"description": "The question, as a person would ask it.",
"type": "string"
},
"remediation": {
"description": "What would settle it, as a command.",
"type": "string"
},
"source": {
"description": "What answered it: an obligation token, a gate id, or the capability that would.",
"type": "string"
},
"status": {
"$ref": "#/$defs/GateStatus",
"description": "The answer, in the same vocabulary every gate uses."
}
},
"required": [
"id",
"question",
"status",
"evidence",
"source",
"remediation"
],
"type": "object"
},
"Gate": {
"description": "```\nuse majordomus_cli::gates::{Gate, GateStatus};\n\nlet gate = Gate {\n id: \"rust-check\".into(),\n title: \"the crate builds, is formatted and passes its tests\".into(),\n scope: \"apps/majordomus-cli\".into(),\n required: true,\n status: GateStatus::Queued,\n reason: \"the plan selects it and no run has ever reported\".into(),\n remediation: \"majordomus evidence --gate rust-check --exit 0\".into(),\n source: \"the plan\".into(),\n evidence: None,\n inputs: vec![\"apps/majordomus-cli/**\".into()],\n inputs_hash: \"3f0a\".into(),\n evaluated_at: \"2026-01-01T00:00:00Z\".into(),\n};\n// silence does not refuse and is not a pass either: it is a debt, reported as one\nassert!(!gate.status.refuses());\nassert!(gate.status.unverified());\nassert!(gate.evidence.is_none());\n```\nOne completion gate: what it is, whether it applies, what it said, and what to do.",
"properties": {
"evaluated_at": {
"description": "When this judgement was made, RFC 3339 UTC. A judgement is about a moment.",
"type": "string"
},
"evidence": {
"anyOf": [
{
"$ref": "#/$defs/GateRun"
},
{
"type": "null"
}
],
"description": "The run that last reported, when one has."
},
"id": {
"description": "The gate's identity in the model.",
"type": "string"
},
"inputs": {
"description": "The pathspecs its evidence is taken over, derived from the model.",
"items": {
"type": "string"
},
"type": "array"
},
"inputs_hash": {
"description": "The hash of those files as they stand now, when it could be taken.",
"type": "string"
},
"reason": {
"description": "Why it is in this status, in words a worker can act on.",
"type": "string"
},
"remediation": {
"description": "What would settle it, as a command.",
"type": "string"
},
"required": {
"description": "True when the plan selects it and its verdict therefore decides completion. False\nfor a gate the plan left out, and for one whose runner is only had on demand.",
"type": "boolean"
},
"scope": {
"description": "What it is a gate over: the CI job that runs it.",
"type": "string"
},
"source": {
"description": "Where the gate is declared: the model, and the command it runs.",
"type": "string"
},
"status": {
"$ref": "#/$defs/GateStatus",
"description": "What is known about it."
},
"title": {
"description": "One line naming it, from the model's own summary.",
"type": "string"
}
},
"required": [
"id",
"title",
"scope",
"required",
"status",
"reason",
"remediation",
"source",
"evaluated_at"
],
"type": "object"
},
"GateClassMatch": {
"description": "```\nuse majordomus_cli::gates::GateClassMatch;\n\nlet hit = GateClassMatch { id: \"docs\".into(), paths: vec![\"docs/CLI.md\".into()] };\n// the class, and the files of the change that put it in the plan — never the class alone,\n// because a plan a reader cannot check is a plan a reader has to believe\nassert_eq!(hit.paths.len(), 1);\n```\nOne class a change fell in, and the paths that put it there.",
"properties": {
"id": {
"description": "The class's identity.",
"type": "string"
},
"paths": {
"description": "The changed paths this class covers.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"id",
"paths"
],
"type": "object"
},
"GatePlan": {
"description": "```\nuse majordomus_cli::gates::{GateClassMatch, GatePlan, GatePlanMode};\nuse std::collections::BTreeMap;\n\nlet plan = GatePlan {\n mode: GatePlanMode::Affected,\n reason: \"3 path(s) fall in 1 class\".into(),\n changed: vec![\"docs/CLI.md\".into()],\n classes: vec![GateClassMatch { id: \"docs\".into(), paths: vec![\"docs/CLI.md\".into()] }],\n unclassified: Vec::new(),\n selected: BTreeMap::from([(\"reference-check\".to_string(), \"class docs\".to_string())]),\n excluded: BTreeMap::new(),\n};\n// the plan says what it selected and why, so a reader can check it rather than believe it\nassert_eq!(plan.selected[\"reference-check\"], \"class docs\");\nassert!(!plan.selected.contains_key(\"rust-check\"), \"a document change cannot break the crate\");\n```\nWhich gates a change must pass, and why each one is in or out.",
"properties": {
"changed": {
"description": "The changed paths the plan was computed over, sorted.",
"items": {
"type": "string"
},
"type": "array"
},
"classes": {
"description": "The classes they fall in.",
"items": {
"$ref": "#/$defs/GateClassMatch"
},
"type": "array"
},
"excluded": {
"additionalProperties": {
"type": "string"
},
"description": "Gate id to the reason it was left out, for the gates that are not.",
"type": "object"
},
"mode": {
"$ref": "#/$defs/GatePlanMode",
"description": "Affected, or full."
},
"reason": {
"description": "Why, in words.",
"type": "string"
},
"selected": {
"additionalProperties": {
"type": "string"
},
"description": "Gate id to the reason it was selected, for the gates in the plan.",
"type": "object"
},
"unclassified": {
"description": "Changed paths no class of the model covers; each one escalates the plan.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"mode",
"reason",
"changed",
"classes",
"unclassified",
"selected",
"excluded"
],
"type": "object"
},
"GatePlanMode": {
"description": "```\nuse majordomus_cli::gates::GatePlanMode;\n\n// a plan is either the gates the change selects, or everything the runners allow\nassert_eq!(GatePlanMode::Affected.as_str(), \"affected\");\nassert_eq!(GatePlanMode::Full.as_str(), \"full\");\nassert_ne!(GatePlanMode::Affected, GatePlanMode::Full);\n```\nWhy the plan selects what it selects.",
"oneOf": [
{
"const": "affected",
"description": "The gates of the classes the changed paths fall in.",
"type": "string"
},
{
"const": "full",
"description": "Every gate but the on-demand ones: asked for, or escalated to.",
"type": "string"
}
]
},
"GateRun": {
"description": "```\nuse majordomus_cli::gates::GateRun;\n\n// what `majordomus evidence --gate` writes: a run, and the tree it was taken over\nlet run = GateRun {\n recorded_at: \"2026-01-01T00:00:00Z\".into(),\n head: \"9b1e2d4\".into(),\n branch: \"master\".into(),\n exit: 0,\n command: \"scripts/rust-check --ci\".into(),\n inputs_hash: \"3f0a\".into(),\n session: \"s-1\".into(),\n};\n// the hash is what makes a run go stale rather than merely old: the same run read\n// against a different tree is evidence about a tree that no longer exists\nassert_eq!(run.exit, 0);\nassert_ne!(run.inputs_hash, \"0000\");\n```\nThe `task.gate` ledger line that last reported a gate, as the ledger holds it.",
"properties": {
"branch": {
"description": "The branch it was recorded on.",
"type": "string"
},
"command": {
"description": "The command that produced it — narrative is not evidence.",
"type": "string"
},
"exit": {
"description": "The exit status the gate reported. `0` is a pass.",
"format": "int64",
"type": "integer"
},
"head": {
"description": "The commit the ledger's envelope stamped on it.",
"type": "string"
},
"inputs_hash": {
"description": "The hash of the files that select the gate, as they stood when it ran.",
"type": "string"
},
"recorded_at": {
"description": "When it was recorded, RFC 3339 UTC.",
"type": "string"
},
"session": {
"description": "The session that recorded it.",
"type": "string"
}
},
"required": [
"recorded_at",
"head",
"branch",
"exit"
],
"type": "object"
},
"GateStatus": {
"description": "```\nuse majordomus_cli::gates::GateStatus;\n\n// the three answers a reader is entitled to, and they are different answers\nassert!(GateStatus::Fail.refuses(), \"a run that failed refuses\");\nassert!(GateStatus::Queued.unverified(), \"asked and unanswered is a debt\");\nassert!(!GateStatus::Exempt.refuses() && !GateStatus::Exempt.unverified());\n\n// and the one that is neither: a run whose tree has moved proves nothing about this one\nassert!(GateStatus::Stale.refuses());\nassert_eq!(GateStatus::Stale.as_str(), \"stale\");\n```\nWhat is known about one gate. Every word is one this repository's design tokens already\ncarry (`share/design/tokens.yaml`, `status.states`), so a surface renders it without\nbeing taught a sixth vocabulary and the design gate does not refuse it.",
"oneOf": [
{
"const": "pass",
"description": "It ran over these inputs and succeeded. The only word that discharges anything.",
"type": "string"
},
{
"const": "fail",
"description": "It ran over these inputs and failed. Completion is refused.",
"type": "string"
},
{
"const": "stale",
"description": "It ran, and the files that select it have changed since; what it proved is about a\ntree that no longer exists. Completion is refused, because the alternative is\ntreating a claim about the past as a claim about the present.",
"type": "string"
},
{
"const": "blocked",
"description": "Something it cannot run without has not passed, so its own silence means nothing.",
"type": "string"
},
{
"const": "queued",
"description": "The plan selects it and no run has ever reported. Not `unknown`: the question was\nasked and no answer came, which is a debt somebody owes, and not `pass`, which is\nthe mistake this word exists to make impossible.",
"type": "string"
},
{
"const": "exempt",
"description": "The plan does not select it: nothing the task changed can make it true or false.",
"type": "string"
},
{
"const": "unknown",
"description": "It cannot be judged here at all — no model, no git, a ledger that would not read.\nAn honest gap, and never a pass.",
"type": "string"
}
]
},
"ImpliedObligation": {
"description": "```\nuse majordomus_cli::gates::ImpliedObligation;\n\nlet owed = ImpliedObligation {\n id: \"tests\".into(),\n title: \"The cases the change obliges were run\".into(),\n applicable: true,\n declared: false,\n reason: \"lib/check.sh is an input of this obligation\".into(),\n because: vec![\"lib/check.sh\".into()],\n remediation: \"majordomus start --requires tests\".into(),\n};\n// applicable and undeclared is the gap nothing else reports: the change owes it and\n// the task promised nothing, so no validator holds anybody to it\nassert!(owed.applicable && !owed.declared);\n```\nOne obligation the change set implies, and whether the task promised it.\n\nApplicability is derived and nothing else: a token's own `inputs` in\n`share/obligations.yaml` name the files it is about, so a change to one of them is what\nmakes the token owed. A token with no `inputs` names a fact outside the tree — a push, a\npublication, a deployment — which no change to a tree can imply, so those are owed when\nthe task declares them and reported as not applicable when it does not.",
"properties": {
"applicable": {
"description": "True when the change set, or the task's own declaration, makes it owed.",
"type": "boolean"
},
"because": {
"description": "The changed paths that implied it, when paths did.",
"items": {
"type": "string"
},
"type": "array"
},
"declared": {
"description": "True when the task's `requires` names it, so `finish` already holds the task to it.",
"type": "boolean"
},
"id": {
"description": "The token.",
"type": "string"
},
"reason": {
"description": "Why it applies, or why it does not.",
"type": "string"
},
"remediation": {
"description": "What would discharge it.",
"type": "string"
},
"title": {
"description": "The obligation as a heading, from the vocabulary.",
"type": "string"
}
},
"required": [
"id",
"title",
"applicable",
"declared",
"reason",
"remediation"
],
"type": "object"
}
},
"description": "```\nuse majordomus_cli::gates::Completion;\n\n// the document every surface reads, as `gates.completion` answers with it. The property\n// that matters is the one asserted here: a gate that never reported leaves the task\n// finishable and *unverified*, which is a different thing from a task that passed.\nlet c: Completion = serde_json::from_value(serde_json::json!({\n \"present\": true,\n \"model\": \".ai/repo/ci/gates.yaml\",\n \"plan\": { \"mode\": \"affected\", \"reason\": \"1 path in 1 class\", \"changed\": [\"docs/CLI.md\"],\n \"classes\": [], \"unclassified\": [], \"selected\": {}, \"excluded\": {} },\n \"finishable\": true,\n \"unverified\": [\"reference-check\"],\n \"tallies\": { \"queued\": 1 },\n \"gates\": [],\n \"obligations\": [],\n \"questions\": []\n}))\n.unwrap();\nassert!(c.finishable, \"absence of a verdict does not refuse\");\nassert_eq!(c.unverified, [\"reference-check\"], \"and it is never silently accepted\");\nassert!(c.blocking.is_empty());\n```\nWhether the active task may be called finished, and everything the answer rests on.\n\n`finishable` is the field a caller acts on and the one thing no surface may recompute:\nthe shell tool's `check` and `finish` read it, the HTTP API serves it, the Cockpit\nrenders it, and all of them are reading this one execution.",
"properties": {
"blocking": {
"description": "The required gates whose status refuses completion.",
"items": {
"type": "string"
},
"type": "array"
},
"findings": {
"description": "What could not be established, each as one line. Never empty when something was\nskipped: a gap is reported rather than left to be inferred.",
"items": {
"type": "string"
},
"type": "array"
},
"finishable": {
"description": "False when a required gate is known to be failing, stale, or blocked by one that is.\nAbsence of a verdict does not make this false — it makes `unverified` non-empty.",
"type": "boolean"
},
"gates": {
"description": "Every gate of the model, what refuses first.",
"items": {
"$ref": "#/$defs/Gate"
},
"type": "array"
},
"model": {
"description": "The CI model this was judged against.",
"type": "string"
},
"obligations": {
"description": "Which obligations the change set implies, and whether the task promised them. The\njudgement of a declared one is `obligations.closure`'s, not this module's.",
"items": {
"$ref": "#/$defs/ImpliedObligation"
},
"type": "array"
},
"plan": {
"$ref": "#/$defs/GatePlan",
"description": "Affected or full, and why."
},
"present": {
"description": "True when a task record was found in this checkout.",
"type": "boolean"
},
"questions": {
"description": "The done invariant: the questions a task must answer before anybody may call it\nfinished, each with the source that answered it and the evidence it read. Composed\nfrom the obligation closure, the gates, the change set, the repository's convergence\nand the closing readings; a question nothing here reaches is `unknown` and names the\ncommand that would answer it.",
"items": {
"$ref": "#/$defs/DoneQuestion"
},
"type": "array"
},
"tallies": {
"additionalProperties": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"description": "How many gates stand in each status, by word.",
"type": "object"
},
"task": {
"anyOf": [
{
"$ref": "#/$defs/ActiveTask"
},
{
"type": "null"
}
],
"description": "The task, as its record declares it."
},
"unverified": {
"description": "The required gates that have never reported, or cannot be judged here. A task\nfinished over these is finished unverified, and the record says so.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"present",
"model",
"plan",
"finishable",
"tallies",
"gates",
"obligations",
"questions"
],
"title": "Completion",
"type": "object"
}Policies
- benchmark
- required — a target on every transport the exposure declares; the cases are the input type's
- cache
- disabled — every call runs the handler
Benchmark targets
Derived from the registry for this repository: one requirement per transport, and the cases the input type provides. The whole matrix is on the benchmarks page.
| transport | state | cases | targets |
|---|---|---|---|
| direct | covered | 4 | this-task one-source-file a-document everything-on-demand |
| mcp | covered | 4 | this-task one-source-file a-document everything-on-demand |
| http | covered | 4 | this-task one-source-file a-document everything-on-demand |