lifecycle.closed
The tracked, durable projection of closed episodes: how many there are, how many closed on this branch, and the newest twenty. This is the only half of the subsystem that survives a clone; everything under .ai/local/ names this machine and travels nowhere.
The tracked, durable projection of closed episodes: how many there are, how many closed on this branch, and the newest twenty. This is the only half of the subsystem that survives a clone; everything under .ai/local/ names this machine and travels nowhere.
- query
- behaviorally_verified
- module lifecycle
- #continuity
- #session
- #record
Exposure
Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.
| surface | as |
|---|---|
| MCP | tool majordomus_lifecycle_closed |
| HTTP | GET /api/v1/lifecycle/closed operationId lifecycle.closed |
| 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 · Empty
No properties: the capability takes no input.
JSON Schema
{
"additionalProperties": false,
"description": "No input.",
"title": "Empty",
"type": "object"
}output · ClosedSessions
total is exact and newest is a window, and keeping them as two numbers is the whole
design of this type. The tracked directory grows without bound and a Cockpit card is not
a listing page, so the rows are capped — but a capped list that did not carry the true
total would quietly understate the repository, which is the shape of defect this
repository has been bitten by often enough to name. window says how many rows came
back, so a reader can see the cap rather than infer it.
use majordomus_cli::capability::builtin::lifecycle::ClosedSessions;
use serde_json::json;
let row = |id: &str| json!({
"session_id": id,
"uri": format!("majordomus://session/{id}"),
"path": format!(".ai/repo/project/sessions/{id}.md"),
"branch": "master",
});
let c: ClosedSessions = serde_json::from_value(json!({
"total": 137,
"on_this_branch": 41,
"newest": [row("s-0137"), row("s-0136")],
"window": 2,
})).unwrap();
// the window is what came back; the total is what exists
assert_eq!(c.window, c.newest.len());
assert!(c.total > c.window, "the rows are capped and the total is not");
assert!(c.on_this_branch <= c.total);| property | type | required | description |
|---|---|---|---|
| newest | array | no | The newest CLOSED_WINDOW, newest first. |
| on_this_branch | integer | yes | How many of them closed on the branch this checkout is on. |
| total | integer | yes | How many closed records the index holds. Exact. |
| window | integer | yes | How many are carried in newest, against the total. |
JSON Schema
{
"$defs": {
"ClosedSession": {
"description": "One closed episode of the tracked, durable projection — the half of this subsystem that\nsurvives a clone.\n\nIt carries both a `path` and a `uri` because they answer different questions: the path\nis where the record is in this checkout, and the `majordomus://session/<identity>`\nidentifier is what every other surface addresses the same record by, so a reader can\nfollow it to the object page without constructing one.\n\nEverything but the identity, the identifier and the path is optional, because a record\nwritten by an older close carries fewer fields and is still a record.\n\n```\nuse majordomus_cli::capability::builtin::lifecycle::ClosedSession;\nuse serde_json::json;\n\nlet s: ClosedSession = serde_json::from_value(json!({\n \"session_id\": \"s-0042\",\n \"uri\": \"majordomus://session/2026-09-12-s-0042\",\n \"path\": \".ai/repo/project/sessions/2026-09-12-s-0042.md\",\n \"created_at\": \"2026-09-12T15:44:00Z\",\n \"branch\": \"master\",\n \"head\": \"cd04012f1\",\n \"outcome\": \"landed\",\n})).unwrap();\n\nassert!(s.uri.starts_with(\"majordomus://session/\"), \"addressable as an object\");\nassert_eq!(s.outcome, \"landed\");\n\n// an older record with less in it is still one, and says so by absence\nlet sparse: ClosedSession = serde_json::from_value(json!({\n \"session_id\": \"s-0001\",\n \"uri\": \"majordomus://session/2026-08-01-s-0001\",\n \"path\": \".ai/repo/project/sessions/2026-08-01-s-0001.md\",\n})).unwrap();\nassert!(sparse.outcome.is_empty() && sparse.title.is_empty());\n```",
"properties": {
"branch": {
"description": "The branch it closed on.",
"type": "string"
},
"created_at": {
"description": "When it closed, as recorded.",
"type": "string"
},
"head": {
"description": "The commit it closed at.",
"type": "string"
},
"outcome": {
"description": "The typed outcome the close recorded.",
"type": "string"
},
"path": {
"description": "Repository-relative path.",
"type": "string"
},
"session_id": {
"description": "The episode id.",
"type": "string"
},
"title": {
"description": "Its title, when the record carries one.",
"type": "string"
},
"uri": {
"description": "`majordomus://session/<identity>`, for the object page.",
"type": "string"
}
},
"required": [
"session_id",
"uri",
"path"
],
"type": "object"
}
},
"description": "The tracked half of the subsystem: the records a clone receives.\n\n`total` is exact and `newest` is a window, and keeping them as two numbers is the whole\ndesign of this type. The tracked directory grows without bound and a Cockpit card is not\na listing page, so the rows are capped — but a capped list that did not carry the true\ntotal would quietly understate the repository, which is the shape of defect this\nrepository has been bitten by often enough to name. `window` says how many rows came\nback, so a reader can see the cap rather than infer it.\n\n```\nuse majordomus_cli::capability::builtin::lifecycle::ClosedSessions;\nuse serde_json::json;\n\nlet row = |id: &str| json!({\n \"session_id\": id,\n \"uri\": format!(\"majordomus://session/{id}\"),\n \"path\": format!(\".ai/repo/project/sessions/{id}.md\"),\n \"branch\": \"master\",\n});\nlet c: ClosedSessions = serde_json::from_value(json!({\n \"total\": 137,\n \"on_this_branch\": 41,\n \"newest\": [row(\"s-0137\"), row(\"s-0136\")],\n \"window\": 2,\n})).unwrap();\n\n// the window is what came back; the total is what exists\nassert_eq!(c.window, c.newest.len());\nassert!(c.total > c.window, \"the rows are capped and the total is not\");\nassert!(c.on_this_branch <= c.total);\n```",
"properties": {
"newest": {
"description": "The newest `CLOSED_WINDOW`, newest first.",
"items": {
"$ref": "#/$defs/ClosedSession"
},
"type": "array"
},
"on_this_branch": {
"description": "How many of them closed on the branch this checkout is on.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"total": {
"description": "How many closed records the index holds. Exact.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"window": {
"description": "How many are carried in `newest`, against the total.",
"format": "uint",
"minimum": 0,
"type": "integer"
}
},
"required": [
"total",
"on_this_branch",
"window"
],
"title": "ClosedSessions",
"type": "object"
}Policies
- benchmark
- required — a target on every transport the exposure declares; the cases are the input type's
- cache
- process — up to 2 entries in the process, 30s each, scoped by the registry fingerprint
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 | 1 | default cold+warm |
| mcp | covered | 1 | default cold+warm |
| http | covered | 1 | default cold+warm |
Claims implemented beside it
Attached by the file that implements each claim; every one links to its page with the implementation and the test that proves it.