Expand description
The lifecycle module: the episode lifecycle as something a person can watch.
The module is named for the lifecycle rather than for the session because session is
already a kind of the layer — the closed records under the sessions section — and the
registry composes one module per declarative kind. A capability module that took the
same namespace would be composed twice and the registry refuses to build, which is the
right answer: two different things called session in one namespace is exactly the
ambiguity that refusal exists to catch.
super::continuity answers one question well — what would a worker resuming in this
checkout be handed? — and it answers it about one episode, the one
state/session-current.yaml points at. That is the right answer for a briefing and the
wrong answer for an operator, because the pointer is a symlink that the most recent
session start re-aims: on 2026-09-11 this repository held five open episodes in one
checkout and continuity.state could name exactly one of them. Four workers were
invisible to every surface the tool has, and the episode that never closes is the one
nobody can see.
So this module reads the store rather than the pointer, and it reports the things a subsystem needs somebody to look at:
lifecycle.episodes— every open episode in the local store, which of them this checkout points at, which belong to another worktree, and what the ledger last saw each do.lifecycle.recovery— the episodes that can no longer close themselves, the temporary files a killed close left in the tracked directory, whether the pointer has been migrated to the per-provider layout, and the arithmetic ADR 0052 asks for: how manysession.startedevents the ledger holds against how manysession.closed.lifecycle.runtime— the commit this process is serving answers about, against the commit the repository is on right now. A long-lived server that built its index once and held it served a six-hour-old HEAD from the API, from MCP and from this Cockpit with nothing saying the picture was old (fix/server-sees-the-current-tree). Whether that is still possible is a fact this page should state rather than a fact a reader should assume.lifecycle.providers— which lifecycle events each provider’s adapter declares, whether it can capture prompts, and which of this repository’s enforcement entries are actually wired to its hook. Declared, never inferred from the presence of a file.lifecycle.closed— the tracked durable projection under the layer’s sessions section: the only half of this subsystem that survives a clone.
Read, never written. Like continuity, this module is one of the lifecycle’s
readers; the writer is the shell tool. A second writer would be a second account of
events the ledger already holds, which is what that design refuses.
Served, never published. Everything under .ai/local/ names this machine, so none of
it is projected into docs/generated/ or site/ — the claim local-state-ignored owns
that, over the whole local half (ADR 0014). The half that belongs to these five is that
none of them declares a command line, because a command line is how a value reaches a
script, a log and eventually a commit; test/cases/171 asserts exactly that.
No thresholds and no clock. Nothing here decides that a record is old. Age is
session.freshness in the policy and it is super::continuity’s to judge; a second
engine for it in this file would be the second source of truth ADR 0052 names. What this
module reports is what it can observe without an opinion: a timestamp as recorded, a
count, a path that exists or does not.
§The shape of the module, and the constraint it is under
Two of the paragraphs above are properties of the declaration rather than intentions,
so they can be read off it. Every one of the five capabilities is served — over HTTP and
over MCP — and none of them declares a command line, which is what “served, never
published” amounts to mechanically: a command line is how a value reaches a script, a
log and eventually a commit, and nothing under .ai/local/ may take that path.
use majordomus_cli::capability::builtin::lifecycle;
let m = lifecycle::module();
assert_eq!(m.id.as_str(), "lifecycle");
// declaration order, which is the order the five are read in
let ids: Vec<&str> = m.capabilities.iter().map(|e| e.capability.id.as_str()).collect();
assert_eq!(ids, [
"lifecycle.episodes",
"lifecycle.recovery",
"lifecycle.runtime",
"lifecycle.providers",
"lifecycle.closed",
]);
// served everywhere a worker in front of this checkout can reach, and nowhere a
// script can: the local half of the layer never becomes a committed value
for e in &m.capabilities {
let id = e.capability.id.as_str();
assert!(e.capability.exposure.http.is_some(), "{id} is not served over HTTP");
assert!(e.capability.exposure.mcp.is_some(), "{id} is not served over MCP");
assert!(e.capability.exposure.cli.is_none(), "{id} declares a command line");
}Structs§
- Balance
- What the ledger says about episode lifecycle against what the store holds.
- Closed
Session - One closed episode of the tracked, durable projection — the half of this subsystem that survives a clone.
- Closed
Sessions - The tracked half of the subsystem: the records a clone receives.
- Episode
- One open episode, as its file records it and as the ledger has seen it.
- Episodes
- Every open episode of this checkout’s store — the answer
continuity.statecannot give, because it reads the pointer and this reads the store. - Orphan
- A temporary file left in a record directory by a close that did not finish.
- Pointer
- What the pointer is and whether it leads anywhere.
- Provider
Lifecycle - One provider’s lifecycle surface: what its adapter declares it can do, and what this repository has wired to it.
- Provider
Lifecycles - Every provider the distribution ships a bootstrap for, with the lifecycle half of each.
- Recovery
- What this checkout’s session store needs somebody to do.
- Runtime
View - The commit this process answers about, against the commit the repository is on.
- Stranded
- An open episode that cannot close itself, and what to do about it.
Enums§
- Episode
Standing - Where an open episode stands, as far as the repository can observe it.
- Pointer
Layout - The layout of the pointer, which is how far the per-provider migration has got here.
Constants§
- EPISODES_
URI - The URI under which
lifecycle.episodesis read as an MCP resource. - RECOVERY_
URI - The URI under which
lifecycle.recoveryis read as an MCP resource.
Functions§
- module
- The module’s one declaration: the five capabilities, their schemas and every projection of them.