plan.drift
Every contradiction between an issue's declared status and its delivery (plan.delivery), each with the objects that prove it: `merged_not_recorded` — READY, BLOCKED or ACTIVE, with a merge on the trunk naming it (pull request, merge commit, release) and no open pull request naming it; `active_without_work` — ACTIVE since before the window, with no branch, pull request, merge or live mesh claim naming it; `done_without_trace` — DONE with no merge naming it, advisory because record-only issues exist. Each finding carries its age, whether it is past the window `.ai/repo/ci/plan.yaml` declares (`drift.window`, seconds), the sources that were not read and could have dissolved it, and the command that settles it (`majordomus plan verify|done <id>`). With `gate`, judged as a gate: on a `head` every merged_not_recorded is pending; on the `trunk` one merged longer than the window ago is refused and a younger one pending; the other classes never refuse; a missing or unreadable window refuses (exit 12); refusals fail the gate (exit 10) only under `drift.enforcement: blocking`, and are reported with exit 0 under `advisory`. Observed live, or `at` a commit as plan.delivery is. Inferred on every call; it never writes a stamp.
Every contradiction between an issue's declared status and its delivery (plan.delivery), each with the objects that prove it: `merged_not_recorded` — READY, BLOCKED or ACTIVE, with a merge on the trunk naming it (pull request, merge commit, release) and no open pull request naming it; `active_without_work` — ACTIVE since before the window, with no branch, pull request, merge or live mesh claim naming it; `done_without_trace` — DONE with no merge naming it, advisory because record-only issues exist. Each finding carries its age, whether it is past the window `.ai/repo/ci/plan.yaml` declares (`drift.window`, seconds), the sources that were not read and could have dissolved it, and the command that settles it (`majordomus plan verify|done <id>`). With `gate`, judged as a gate: on a `head` every merged_not_recorded is pending; on the `trunk` one merged longer than the window ago is refused and a younger one pending; the other classes never refuse; a missing or unreadable window refuses (exit 12); refusals fail the gate (exit 10) only under `drift.enforcement: blocking`, and are reported with exit 0 under `advisory`. Observed live, or `at` a commit as plan.delivery is. Inferred on every call; it never writes a stamp.
- query
- experimental
- module plan
- #plan
- #project
- #delivery
- #drift
- #gate
Exposure
Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.
| surface | as |
|---|---|
| MCP | tool majordomus_plan_drift |
| HTTP | GET /api/v1/plan/drift operationId plan.drift |
| command line | majordomus plan drift |
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 · PlanDriftInput
plan.drift.
use majordomus_cli::capability::builtin::plan::{DriftGate, PlanDriftInput};
let every: PlanDriftInput = serde_json::from_str("{}").unwrap();
assert!(every.issue.is_none() && every.at.is_none() && every.gate.is_none());
let gate: PlanDriftInput = serde_json::from_value(serde_json::json!({ "gate": "trunk" })).unwrap();
assert_eq!(gate.gate, Some(DriftGate::Trunk));
assert!(serde_json::from_str::<PlanDriftInput>(r#"{"gate":"main"}"#).is_err());
assert!(serde_json::from_str::<PlanDriftInput>(r#"{"window":60}"#).is_err());| property | type | required | description |
|---|---|---|---|
| at | string | null | no | Observe as of this commit, as plan.delivery does. Default: live. |
| gate | one of 2 | no | Judge as a gate on a head or on the trunk. Default: report only, exit 0. |
| issue | string | null | no | One issue id. Default: every issue of the plan. |
JSON Schema
{
"$defs": {
"DriftGate": {
"description": "Which tree a `plan.drift` gate judges.\n\n```\nuse majordomus_cli::capability::builtin::plan::DriftGate;\nassert_eq!(serde_json::to_value(DriftGate::Trunk).unwrap(), \"trunk\");\nassert_eq!(serde_json::from_str::<DriftGate>(r#\"\"head\"\"#).unwrap(), DriftGate::Head);\n```",
"oneOf": [
{
"const": "head",
"description": "A head: not the shared plan, so every merged_not_recorded is pending.",
"type": "string"
},
{
"const": "trunk",
"description": "The trunk: merged work unrecorded past the window is refused.",
"type": "string"
}
]
}
},
"additionalProperties": false,
"description": "Which issues, as of when, and judged as which gate: the input of `plan.drift`.\n\n```\nuse majordomus_cli::capability::builtin::plan::{DriftGate, PlanDriftInput};\n\nlet every: PlanDriftInput = serde_json::from_str(\"{}\").unwrap();\nassert!(every.issue.is_none() && every.at.is_none() && every.gate.is_none());\nlet gate: PlanDriftInput = serde_json::from_value(serde_json::json!({ \"gate\": \"trunk\" })).unwrap();\nassert_eq!(gate.gate, Some(DriftGate::Trunk));\nassert!(serde_json::from_str::<PlanDriftInput>(r#\"{\"gate\":\"main\"}\"#).is_err());\nassert!(serde_json::from_str::<PlanDriftInput>(r#\"{\"window\":60}\"#).is_err());\n```",
"properties": {
"at": {
"description": "Observe as of this commit, as `plan.delivery` does. Default: live.",
"type": [
"string",
"null"
]
},
"gate": {
"anyOf": [
{
"$ref": "#/$defs/DriftGate"
},
{
"type": "null"
}
],
"description": "Judge as a gate on a `head` or on the `trunk`. Default: report only, exit 0."
},
"issue": {
"description": "One issue id. Default: every issue of the plan.",
"type": [
"string",
"null"
]
}
},
"title": "PlanDriftInput",
"type": "object"
}output · DriftReport
| property | type | required | description |
|---|---|---|---|
| classes | array | yes | The class vocabulary, in order. |
| enforced | boolean | yes | Whether a refusal fails the gate (drift.enforcement: blocking). |
| exit_code | integer | yes | The exit code the gate ends with: 0, EXIT_REFUSED or EXIT_UNJUDGED. |
| findings | array | yes | The findings, in issue order, then class order. |
| gate | one of 2 | no | The tree a gate judged, when one was asked. |
| observed | reference | yes | Where and when the delivery was observed. |
| pending | integer | yes | How many it reported as pending. |
| refused | integer | yes | How many findings the gate refused. |
| schema | integer | yes | The version of this shape. |
| sources | array | yes | Every source, read or not: the delivery's, and the mesh claims. |
| tallies | object | yes | How many findings of each class, keyed by every class. |
| verdict | string | yes | report (no gate), clean, pending, advisory (refusals not enforced),
refused, or unjudged (no window). |
| window | reference | yes | The declared window. |
JSON Schema
{
"$defs": {
"DeliveryObservation": {
"description": "Where and when the answer was observed.",
"properties": {
"commit": {
"description": "The commit that history ends at.",
"type": "string"
},
"mode": {
"description": "`live`, or `at` a named commit.",
"type": "string"
},
"observed_at": {
"description": "When: the clock in live mode, the commit's committer time at a commit.",
"type": "string"
},
"trunk": {
"description": "The history read: the trunk ref in live mode, the commit asked for otherwise.",
"type": "string"
}
},
"required": [
"mode",
"trunk",
"commit",
"observed_at"
],
"type": "object"
},
"DeliveryReading": {
"description": "Whether one source of the answer was read.",
"oneOf": [
{
"const": "read",
"description": "It was read; what it holds is in the answer.",
"type": "string"
},
{
"const": "unread",
"description": "It was not read, and `reason` says why. Nothing it could have proved is claimed or\ndenied.",
"type": "string"
}
]
},
"DeliverySource": {
"description": "One source the answer is built from, and whether it was read.",
"properties": {
"detail": {
"description": "What was read, or why nothing was: the ref, the file, the command that would record\nit.",
"type": "string"
},
"name": {
"description": "`branches`, `forge`, `history`, `releases` or `publication`.",
"type": "string"
},
"observed_at": {
"description": "When what was read was observed, RFC 3339, when it was read.",
"type": [
"string",
"null"
]
},
"state": {
"$ref": "#/$defs/DeliveryReading",
"description": "Read or unread."
}
},
"required": [
"name",
"state",
"detail"
],
"type": "object"
},
"DeliveryStage": {
"description": "How far one issue's work has travelled, in order: each stage implies every one before it\nhas been passed through, so the furthest one proved is the issue's stage.",
"oneOf": [
{
"const": "none",
"description": "Nothing that was read names the issue.",
"type": "string"
},
{
"const": "branch",
"description": "A branch names it.",
"type": "string"
},
{
"const": "pr_open",
"description": "An open pull request names it.",
"type": "string"
},
{
"const": "merged",
"description": "A merge in the trunk's history names it.",
"type": "string"
},
{
"const": "released",
"description": "A `v*` release contains its newest merge.",
"type": "string"
},
{
"const": "deployed",
"description": "The published `latest.json` names that release or a later one.",
"type": "string"
}
]
},
"DriftClass": {
"description": "One kind of contradiction between the stamps and the work.",
"oneOf": [
{
"const": "merged_not_recorded",
"description": "READY, BLOCKED or ACTIVE, with merged work and no open pull request naming it.",
"type": "string"
},
{
"const": "active_without_work",
"description": "ACTIVE since before the window, with no branch, pull request, merge or mesh claim.",
"type": "string"
},
{
"const": "done_without_trace",
"description": "DONE with no merge naming it. Advisory: record-only issues exist.",
"type": "string"
}
]
},
"DriftFinding": {
"description": "One contradiction between an issue's stamps and its delivery.",
"properties": {
"advisory": {
"description": "True for a class that never refuses.",
"type": "boolean"
},
"age_seconds": {
"description": "Seconds from `since` to the observation.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"class": {
"$ref": "#/$defs/DriftClass",
"description": "The contradiction."
},
"issue": {
"description": "The issue id.",
"type": "string"
},
"milestone": {
"description": "Its milestone.",
"type": "string"
},
"past_window": {
"description": "Whether that is longer than the window; absent when either is unknown.",
"type": [
"boolean",
"null"
]
},
"proof": {
"$ref": "#/$defs/DriftProof",
"description": "What proves it."
},
"remedy": {
"description": "The command that settles it.",
"type": "string"
},
"settled": {
"description": "True when no unread source could have dissolved it.",
"type": "boolean"
},
"since": {
"description": "Since when the contradiction holds: the merge for `merged_not_recorded`, the start\nstamp for `active_without_work`.",
"type": [
"string",
"null"
]
},
"stage": {
"$ref": "#/$defs/DeliveryStage",
"description": "Its inferred delivery stage."
},
"status": {
"description": "Its declared status.",
"type": "string"
},
"title": {
"description": "Its title.",
"type": "string"
},
"unread": {
"description": "The sources that were not read and could have dissolved it.",
"items": {
"type": "string"
},
"type": "array"
},
"verdict": {
"anyOf": [
{
"$ref": "#/$defs/DriftVerdict"
},
{
"type": "null"
}
],
"description": "What the gate made of it, when a gate was asked."
}
},
"required": [
"issue",
"title",
"milestone",
"status",
"stage",
"class",
"advisory",
"proof",
"settled",
"unread",
"remedy"
],
"type": "object"
},
"DriftGate": {
"description": "Which tree a `plan.drift` gate judges.\n\n```\nuse majordomus_cli::capability::builtin::plan::DriftGate;\nassert_eq!(serde_json::to_value(DriftGate::Trunk).unwrap(), \"trunk\");\nassert_eq!(serde_json::from_str::<DriftGate>(r#\"\"head\"\"#).unwrap(), DriftGate::Head);\n```",
"oneOf": [
{
"const": "head",
"description": "A head: not the shared plan, so every merged_not_recorded is pending.",
"type": "string"
},
{
"const": "trunk",
"description": "The trunk: merged work unrecorded past the window is refused.",
"type": "string"
}
]
},
"DriftProof": {
"description": "The objects that prove a finding.",
"properties": {
"merge_commit": {
"description": "The newest commit in the trunk's history that names the issue.",
"type": [
"string",
"null"
]
},
"merged_at": {
"description": "When that commit was made.",
"type": [
"string",
"null"
]
},
"pull_request": {
"description": "The pull request the newest merge naming the issue merged.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"release": {
"description": "The first release that contains it.",
"type": [
"string",
"null"
]
},
"started_at": {
"description": "When execution was recorded to have begun, for `active_without_work`.",
"type": [
"string",
"null"
]
},
"via": {
"description": "How it names the issue (`title_or_branch`, `slug`, `subject`, `forge_title`).",
"type": [
"string",
"null"
]
}
},
"type": "object"
},
"DriftVerdict": {
"description": "What a gate made of one finding.",
"oneOf": [
{
"const": "refused",
"description": "Refused: merged work the trunk has carried unrecorded past the window.",
"type": "string"
},
{
"const": "pending",
"description": "Reported and not refused: on a head, or inside the window.",
"type": "string"
},
{
"const": "reported",
"description": "Reported; this class never refuses.",
"type": "string"
},
{
"const": "advisory",
"description": "Advisory; this class never refuses (`done_without_trace`).",
"type": "string"
}
]
},
"DriftWindow": {
"description": "The declared window, or why there is none.",
"properties": {
"detail": {
"description": "What was read, or why nothing was.",
"type": "string"
},
"enforcement": {
"description": "`advisory` (refusals are reported, the gate passes) or `blocking`.",
"type": [
"string",
"null"
]
},
"model": {
"description": "The file it is declared in.",
"type": "string"
},
"seconds": {
"description": "The seconds the trunk may carry merged work the plan has not recorded.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"state": {
"$ref": "#/$defs/DeliveryReading",
"description": "Read, or unread with `detail` saying why."
}
},
"required": [
"model",
"state",
"detail"
],
"type": "object"
}
},
"description": "Every contradiction between the plan's stamps and its delivery, and, when a gate was\nasked, its verdict.",
"properties": {
"classes": {
"description": "The class vocabulary, in order.",
"items": {
"$ref": "#/$defs/DriftClass"
},
"type": "array"
},
"enforced": {
"description": "Whether a refusal fails the gate (`drift.enforcement: blocking`).",
"type": "boolean"
},
"exit_code": {
"description": "The exit code the gate ends with: 0, `EXIT_REFUSED` or `EXIT_UNJUDGED`.",
"format": "uint8",
"maximum": 255,
"minimum": 0,
"type": "integer"
},
"findings": {
"description": "The findings, in issue order, then class order.",
"items": {
"$ref": "#/$defs/DriftFinding"
},
"type": "array"
},
"gate": {
"anyOf": [
{
"$ref": "#/$defs/DriftGate"
},
{
"type": "null"
}
],
"description": "The tree a gate judged, when one was asked."
},
"observed": {
"$ref": "#/$defs/DeliveryObservation",
"description": "Where and when the delivery was observed."
},
"pending": {
"description": "How many it reported as pending.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"refused": {
"description": "How many findings the gate refused.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"schema": {
"description": "The version of this shape.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"sources": {
"description": "Every source, read or not: the delivery's, and the mesh claims.",
"items": {
"$ref": "#/$defs/DeliverySource"
},
"type": "array"
},
"tallies": {
"additionalProperties": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"description": "How many findings of each class, keyed by every class.",
"type": "object"
},
"verdict": {
"description": "`report` (no gate), `clean`, `pending`, `advisory` (refusals not enforced),\n`refused`, or `unjudged` (no window).",
"type": "string"
},
"window": {
"$ref": "#/$defs/DriftWindow",
"description": "The declared window."
}
},
"required": [
"schema",
"observed",
"window",
"sources",
"classes",
"tallies",
"findings",
"refused",
"pending",
"enforced",
"verdict",
"exit_code"
],
"title": "DriftReport",
"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 | 2 | every-issue one-issue-at-head-as-trunk-gate |
| mcp | covered | 2 | every-issue one-issue-at-head-as-trunk-gate |
| http | covered | 2 | every-issue one-issue-at-head-as-trunk-gate |