Skip to content
Cockpit0.29.0
A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live

gates.completion

Whether this task may be called finished

What it is

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.

Descriptor

Canonical id
gates.completion
Module
gates
Kind
query
Stability
behaviorallyverified
Provenance
declared
Source
apps/majordomus-cli/src/capability/builtin/gates.rs
Cache
disabled
Benchmark
required
Tags
gatescompletiontasksevidence

Run

query
A query. It reads and changes nothing.

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.

The commit to compare with, when the task's own starting commit is not wanted.

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.

GET /api/v1/gates/completion

Projections

InterfaceAsWhere
MCP toolmajordomus_completion-
MCP resourcemajordomus://gates/completion-
HTTPGET /api/v1/gates/completionSwagger
Command linenot exposed-

Schemas

Input · CompletionInput
{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "changed": {
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": "string"
      },
      "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."
    },
    "base": {
      "type": [
        "string",
        "null"
      ],
      "description": "The commit to compare with, when the task's own starting commit is not wanted."
    },
    "on_demand": {
      "type": [
        "boolean",
        "null"
      ],
      "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."
    }
  },
  "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.",
  "title": "CompletionInput"
}
Output · Completion
{
  "type": "object",
  "properties": {
    "present": {
      "type": "boolean",
      "description": "True when a task record was found in this checkout."
    },
    "task": {
      "anyOf": [
        {
          "$ref": "#/$defs/ActiveTask"
        },
        {
          "type": "null"
        }
      ],
      "description": "The task, as its record declares it."
    },
    "model": {
      "type": "string",
      "description": "The CI model this was judged against."
    },
    "plan": {
      "$ref": "#/$defs/GatePlan",
      "description": "Affected or full, and why."
    },
    "finishable": {
      "type": "boolean",
      "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."
    },
    "blocking": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "The required gates whose status refuses completion."
    },
    "unverified": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "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."
    },
    "tallies": {
      "type": "object",
      "additionalProperties": {
        "type": "integer",
        "format": "uint",
        "minimum": 0
      },
      "description": "How many gates stand in each status, by word."
    },
    "gates": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Gate"
      },
      "description": "Every gate of the model, what refuses first."
    },
    "obligations": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/ImpliedObligation"
      },
      "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."
    },
    "questions": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/DoneQuestion"
      },
      "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."
    },
    "findings": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "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."
    }
  },
  "required": [
    "present",
    "model",
    "plan",
    "finishable",
    "tallies",
    "gates",
    "obligations",
    "questions"
  ],
  "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.",
  "title": "Completion",
  "$defs": {
    "ActiveTask": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The task id."
        },
        "task": {
          "type": "string",
          "description": "What is being worked on."
        },
        "profile": {
          "type": "string",
          "description": "The execution profile it runs under."
        },
        "outcome": {
          "type": "string",
          "description": "Its typed outcome so far: `active`, `handed_over`, or a finished one."
        },
        "scope": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The paths it claims."
        },
        "requires": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "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."
        },
        "started_at": {
          "type": "string",
          "description": "When it started."
        },
        "head": {
          "type": "string",
          "description": "The commit it started at."
        },
        "issue": {
          "type": "string",
          "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)."
        },
        "intent": {
          "type": "string",
          "description": "The intent the worker said this task serves (`start --intent`)."
        },
        "exemption": {
          "type": "string",
          "description": "The exemption class the worker gave instead (`start --exempt`)."
        },
        "exemption_because": {
          "type": "string",
          "description": "The reason given with the exemption."
        },
        "binding": {
          "type": "string",
          "description": "The standing `intents.binding` answered when the task started — what `start` was\ntold then, not where the work stands now."
        },
        "plan_revision": {
          "type": "string",
          "description": "The pin of what the work was for when the task started."
        },
        "evidence_standing": {
          "type": "string",
          "description": "The pin of where the served criteria's evidence stood when the task started."
        }
      },
      "required": [
        "id",
        "task",
        "profile",
        "outcome"
      ],
      "description": "The active task of this checkout, when there is one."
    },
    "GatePlan": {
      "type": "object",
      "properties": {
        "mode": {
          "$ref": "#/$defs/GatePlanMode",
          "description": "Affected, or full."
        },
        "reason": {
          "type": "string",
          "description": "Why, in words."
        },
        "changed": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The changed paths the plan was computed over, sorted."
        },
        "classes": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/GateClassMatch"
          },
          "description": "The classes they fall in."
        },
        "unclassified": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Changed paths no class of the model covers; each one escalates the plan."
        },
        "selected": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "description": "Gate id to the reason it was selected, for the gates in the plan."
        },
        "excluded": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "description": "Gate id to the reason it was left out, for the gates that are not."
        }
      },
      "required": [
        "mode",
        "reason",
        "changed",
        "classes",
        "unclassified",
        "selected",
        "excluded"
      ],
      "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."
    },
    "GatePlanMode": {
      "oneOf": [
        {
          "type": "string",
          "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."
        }
      ],
      "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."
    },
    "GateClassMatch": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The class's identity."
        },
        "paths": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The changed paths this class covers."
        }
      },
      "required": [
        "id",
        "paths"
      ],
      "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."
    },
    "Gate": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The gate's identity in the model."
        },
        "title": {
          "type": "string",
          "description": "One line naming it, from the model's own summary."
        },
        "scope": {
          "type": "string",
          "description": "What it is a gate over: the CI job that runs it."
        },
        "required": {
          "type": "boolean",
          "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."
        },
        "status": {
          "$ref": "#/$defs/GateStatus",
          "description": "What is known about it."
        },
        "reason": {
          "type": "string",
          "description": "Why it is in this status, in words a worker can act on."
        },
        "remediation": {
          "type": "string",
          "description": "What would settle it, as a command."
        },
        "source": {
          "type": "string",
          "description": "Where the gate is declared: the model, and the command it runs."
        },
        "evidence": {
          "anyOf": [
            {
              "$ref": "#/$defs/GateRun"
            },
            {
              "type": "null"
            }
          ],
          "description": "The run that last reported, when one has."
        },
        "inputs": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The pathspecs its evidence is taken over, derived from the model."
        },
        "inputs_hash": {
          "type": "string",
          "description": "The hash of those files as they stand now, when it could be taken."
        },
        "evaluated_at": {
          "type": "string",
          "description": "When this judgement was made, RFC 3339 UTC. A judgement is about a moment."
        }
      },
      "required": [
        "id",
        "title",
        "scope",
        "required",
        "status",
        "reason",
        "remediation",
        "source",
        "evaluated_at"
      ],
      "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."
    },
    "GateStatus": {
      "oneOf": [
        {
          "type": "string",
          "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."
        }
      ],
      "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."
    },
    "GateRun": {
      "type": "object",
      "properties": {
        "recorded_at": {
          "type": "string",
          "description": "When it was recorded, RFC 3339 UTC."
        },
        "head": {
          "type": "string",
          "description": "The commit the ledger's envelope stamped on it."
        },
        "branch": {
          "type": "string",
          "description": "The branch it was recorded on."
        },
        "exit": {
          "type": "integer",
          "format": "int64",
          "description": "The exit status the gate reported. `0` is a pass."
        },
        "command": {
          "type": "string",
          "description": "The command that produced it — narrative is not evidence."
        },
        "inputs_hash": {
          "type": "string",
          "description": "The hash of the files that select the gate, as they stood when it ran."
        },
        "session": {
          "type": "string",
          "description": "The session that recorded it."
        }
      },
      "required": [
        "recorded_at",
        "head",
        "branch",
        "exit"
      ],
      "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."
    },
    "ImpliedObligation": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The token."
        },
        "title": {
          "type": "string",
          "description": "The obligation as a heading, from the vocabulary."
        },
        "applicable": {
          "type": "boolean",
          "description": "True when the change set, or the task's own declaration, makes it owed."
        },
        "declared": {
          "type": "boolean",
          "description": "True when the task's `requires` names it, so `finish` already holds the task to it."
        },
        "reason": {
          "type": "string",
          "description": "Why it applies, or why it does not."
        },
        "because": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The changed paths that implied it, when paths did."
        },
        "remediation": {
          "type": "string",
          "description": "What would discharge it."
        }
      },
      "required": [
        "id",
        "title",
        "applicable",
        "declared",
        "reason",
        "remediation"
      ],
      "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."
    },
    "DoneQuestion": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The question's identity, stable across reports."
        },
        "question": {
          "type": "string",
          "description": "The question, as a person would ask it."
        },
        "status": {
          "$ref": "#/$defs/GateStatus",
          "description": "The answer, in the same vocabulary every gate uses."
        },
        "evidence": {
          "type": "string",
          "description": "What was actually read — never a restatement of the status."
        },
        "source": {
          "type": "string",
          "description": "What answered it: an obligation token, a gate id, or the capability that would."
        },
        "remediation": {
          "type": "string",
          "description": "What would settle it, as a command."
        }
      },
      "required": [
        "id",
        "question",
        "status",
        "evidence",
        "source",
        "remediation"
      ],
      "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."
    }
  }
}

Examples

These are the capability's own benchmark cases: the same inputs the benchmark runs and the OpenAPI document shows.

this-task
{}
one-source-file
{
  "changed": [
    "apps/majordomus-cli/src/lib.rs"
  ]
}
a-document
{
  "changed": [
    "docs/CLI.md"
  ]
}
everything-on-demand
{
  "changed": [
    ".ai/repo/ci/gates.yaml"
  ],
  "on_demand": true
}