The session record is only as good as the discipline that opens it. A worker that is asked to run session start runs it when it remembers, which is not the sitting that ended in a crash, a compaction, or somebody closing the window — and those are the ones somebody later needs to read about. Meanwhile the layer names a store for the working context of an episode and nothing has ever written a file into it, so a reader cannot tell an empty store from an unimplemented one.
setup: provider-reachable
given:
- 'Majordomus installed, no session open, no provider hook wired'
- 'work about to begin, which is what the compaction and end events have to describe'
steps:
- id: unwired
run: ['capture', 'status']
note: 'a repository that wires nothing says so; it is never presented as an episode boundary that happens to be quiet'
expect:
exit: 0
stdout_contains: ['claude-code:session +unconfigured']
- id: install
run: ['capture', 'install']
note: 'one shim per event, and the entries that make the provider run them'
expect:
exit: 0
stdout_contains: ['majordomus-session-start', 'majordomus-session-end', 'majordomus-session-compact']
files_exist: ['.claude/hooks/majordomus-session-start', '.claude/hooks/majordomus-session-end', '.claude/hooks/majordomus-session-compact']
- id: proven-by-running-it
run: ['capture', 'status']
note: 'verified means a synthetic payload went through the end shim and reached the command, with the mutation left out'
expect:
exit: 0
stdout_contains: ['claude-code:session +verified']
- id: open
run: ['session', 'start']
note: 'the open freezes the context the builder resolved into the store the layer names — as a snapshot of what this worker was told, not as its working context'
expect:
exit: 0
stdout_contains: ['working context: \.ai/local/session-contexts/']
- id: what-is-true-now
run: ['session', 'context']
note: 'the document, composed on this read: the episode, git now against git at the open, and what the episode has recorded since. It used to answer with the snapshot path, which is a file written before the worker had done anything'
expect:
exit: 0
stdout_contains: ['^# Working context of session s-', '## Repository now', '## Recorded in this episode']
- id: where-the-snapshot-is
run: ['session', 'context', '--path']
note: 'the frozen opening snapshot is still addressable by name, for a caller that wants the file rather than the answer'
expect:
exit: 0
stdout_contains: ['^\.ai/local/session-contexts/2[0-9]+T[0-9]+Z--s-']
- id: work-to-describe
run: ['start', 'Prove the lifecycle runs itself', '--scope', 'lib']
note: 'a checkpoint is a progress note inside a task, so the two events below have something to be about'
expect:
exit: 0
- id: what-a-compaction-would-record
run: ['checkpoint', '--derive']
note: 'what the compaction event runs: a progress note composed from git and the ledger, because at that moment nobody is being asked anything'
expect:
exit: 0
stdout_contains: ['^\.ai/local/state/checkpoints/']
- id: what-an-end-would-write
run: ['handover', '--derive']
note: 'and what the end event writes when the task is still active: the sections the policy requires, derived from records that already exist, with no model called'
expect:
exit: 0
stdout_contains: ['^\.ai/local/state/handovers/']
- id: what-the-next-worker-is-handed
run: ['handover', '--resolve']
note: 'the record the start event quotes into the next episode, with the label that says how far to trust it'
expect:
exit: 0
stdout_contains: ['Match: same_worktree_same_branch', 'Git state: exact']
- id: close
run: ['session', 'close']
note: 'the shared record; the working context learns the outcome in the same document it was opened with'
expect:
exit: 0
stdout_contains: ['^\.ai/repo/sessions/']
- id: an-end-with-nothing-open
run: ['session', 'close', '--if-none', 'ignore']
note: 'what the provider''s end event passes: the episode may have been closed by hand, or never opened, and neither is a failure'
expect:
exit: 0
then:
- 'the episode boundary exists without a worker choosing to draw it'
- 'what the worker was told at the open is evidence rather than recollection'
- 'the continuation record exists whether or not anybody was willing to type one'
- 'the next episode is handed that record, with the label that says how far to trust it'
- 'the working context stays under the ignored half of the layer and never becomes a transcript'
The provider's own SessionStart, PreCompact and SessionEnd events run the lifecycle. Each is idempotent, because the events are: a resume keeps the open episode, an end with nothing open writes nothing, and a compaction with no active task records nothing.
The open freezes what the context builder resolved, next to a section for the worker's own notes, and writes a briefing to standard output — which the provider adds to the context it is about to build. That is the step that makes the record readable as well as written: a continuation package nothing loads is one nobody reads. A compaction records a derived checkpoint, because the conversation is about to stop holding what it knows. An end with the task still active writes a derived handover before the envelope closes, so the next worker inherits both an index of the episode and something to act on.
doctor holds the repository to the wiring by driving a payload through the shim, and refuses a working context that carries a conversation. majordomus_continuity reads the same state back over MCP, over HTTP and in the Cockpit, with the same two tiers and the same four divergence labels — and never writes, because the lifecycle has one writer.
-
a repository that wires nothing says so; it is never presented as an episode boundary that happens to be quiet
agents unsupported none as above: a convention hands nothing to anything.
agents:session unsupported none not a product but a class of tools defined by the file they read (agents.md). A convention has no process and no events; the concrete tool's own entry in this file is what says what it can do.
bb unsupported none same search, same date, same caveat: nothing published hands a prompt to an external command before the model runs. Because bb runs Claude Code and Codex as child processes, each child's own prompt hook fires underneath it — the coverage is the agent's, never bb's.
bb:session unsupported none no conversation-lifecycle hook is published. Searched <date>: getbb.app; github.com/get-bb/bb docs/ (25 files), plugins/ (31 plugins), README.md; docs/configuration.md ('Repository worktree hooks') and docs/cli-guide-and-skill.md document only .bb-env-setup.sh / .bb-env-teardown.sh, which fire on environment provisioning and removal. docs/provider-plugin-api.md names an internal system/interaction/lifecycle event stream with no external-command contract. The @get-bb/plugin-sdk surface is rendered in-app and not published, so this is an absence of documentation, not a documented absence. ADR 0024 reached the same conclusion: the agent bb runs keeps the hook.
claude-code unconfigured hook .claude/settings.json does not exist (run: majordomus capture install)
claude-code:session unconfigured hooks .claude/settings.json does not exist (run: majordomus capture install)
codex unadapted hook the provider documents a prompt event and this distribution ships no adapter for it — https://learn.chatgpt.com/docs/hooks — UserPromptSubmit, 'Runs when a user submits a prompt, before model invocation, and can block the prompt'; the raw text arrives as `prompt` (not `prompt_text`, as Claude Code sends it). Verified <date>.
codex:session unadapted hooks the provider documents session events and this distribution ships no adapter for them — https://learn.chatgpt.com/docs/hooks (canonical; developers.openai.com/codex/hooks 308-redirects to it) — SessionStart (source: startup, resume, clear, compact), SessionEnd, PreCompact and PostCompact; PreCompact can block with continue:false. Configured in ~/.codex/hooks.json or an inline [hooks] table in .codex/config.toml. Verified <date>. The `notify` key is NOT this: https://learn.chatgpt.com/docs/config-file/config-advanced says it fires 'whenever Codex emits supported events (currently only agent-turn-complete)' — after a turn, never at a session boundary.
gemini unadapted hook the provider documents a prompt event and this distribution ships no adapter for it — https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md — BeforeAgent, 'Fires after a user submits a prompt, but before the agent begins planning'; the raw text arrives as `prompt`. BeforeModel fires later, on the assembled request. Verified <date>.
gemini:session unadapted hooks the provider documents session events and this distribution ships no adapter for them — https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md and https://geminicli.com/docs/hooks/ — SessionStart (source: startup, resume, clear), SessionEnd (reason: exit, clear, logout, prompt_input_exit, other), PreCompress (trigger: auto, manual). Configured as a hooks block in settings.json. Tracker github.com/google-gemini/gemini-cli/issues/9070 'Comprehensive Hooking System' is closed, 34 of 34. Verified <date>. Two caveats an adapter must carry: the CLI does not wait for SessionEnd, and PreCompress is advisory and cannot block.
generic unsupported none no vendor documentation is involved: a worker with no convention of its own exposes no event, and the MCP connection cannot stand in. An MCP server receives initialize, tool calls and notifications; the person's prompt is never among them, in any version of the protocol (modelcontextprotocol.io/specification). Declared none rather than left blank so that no surface can imply otherwise.
generic:session unconfigured connection bin/majordomus-mcp does not exist, so no client can start the shared server whose connection is the boundary
(no adapter) n/a this tool ships no capture adapter for: agents bb codex gemini generic; they read the model and lose the automation
- exit 0
- stdout_contains /claude-code:session +unconfigured/
-
one shim per event, and the entries that make the provider run them
INFO capture .claude/hooks/majordomus-capture — written and made executable (inline)
INFO capture .claude/hooks/majordomus-session-start — written and made executable (inline)
INFO capture .claude/hooks/majordomus-session-end — written and made executable (inline)
INFO capture .claude/hooks/majordomus-session-compact — written and made executable (inline)
INFO capture .claude/skills/majordomus-handover — written; the provider loads it at its next start
INFO capture .claude/skills/majordomus-brief — written; the provider loads it at its next start
INFO capture .claude/settings.json — written with the UserPromptSubmit, SessionStart, SessionEnd, PreCompact hook(s)
- exit 0
- stdout_contains /majordomus-session-start/
- stdout_contains /majordomus-session-end/
- stdout_contains /majordomus-session-compact/
- file_exists .claude/hooks/majordomus-session-start
- file_exists .claude/hooks/majordomus-session-end
- file_exists .claude/hooks/majordomus-session-compact
-
verified means a synthetic payload went through the end shim and reached the command, with the mutation left out
agents unsupported none as above: a convention hands nothing to anything.
agents:session unsupported none not a product but a class of tools defined by the file they read (agents.md). A convention has no process and no events; the concrete tool's own entry in this file is what says what it can do.
bb unsupported none same search, same date, same caveat: nothing published hands a prompt to an external command before the model runs. Because bb runs Claude Code and Codex as child processes, each child's own prompt hook fires underneath it — the coverage is the agent's, never bb's.
bb:session unsupported none no conversation-lifecycle hook is published. Searched <date>: getbb.app; github.com/get-bb/bb docs/ (25 files), plugins/ (31 plugins), README.md; docs/configuration.md ('Repository worktree hooks') and docs/cli-guide-and-skill.md document only .bb-env-setup.sh / .bb-env-teardown.sh, which fire on environment provisioning and removal. docs/provider-plugin-api.md names an internal system/interaction/lifecycle event stream with no external-command contract. The @get-bb/plugin-sdk surface is rendered in-app and not published, so this is an absence of documentation, not a documented absence. ADR 0024 reached the same conclusion: the agent bb runs keeps the hook.
claude-code verified hook .claude/hooks/majordomus-capture is wired, and a synthetic payload through it produced one record and its rendering
claude-code:session verified hooks .claude/hooks/majordomus-session-start, .claude/hooks/majordomus-session-end, .claude/hooks/majordomus-session-compact are wired, and a synthetic payload through the end shim reached the command
codex unadapted hook the provider documents a prompt event and this distribution ships no adapter for it — https://learn.chatgpt.com/docs/hooks — UserPromptSubmit, 'Runs when a user submits a prompt, before model invocation, and can block the prompt'; the raw text arrives as `prompt` (not `prompt_text`, as Claude Code sends it). Verified <date>.
codex:session unadapted hooks the provider documents session events and this distribution ships no adapter for them — https://learn.chatgpt.com/docs/hooks (canonical; developers.openai.com/codex/hooks 308-redirects to it) — SessionStart (source: startup, resume, clear, compact), SessionEnd, PreCompact and PostCompact; PreCompact can block with continue:false. Configured in ~/.codex/hooks.json or an inline [hooks] table in .codex/config.toml. Verified <date>. The `notify` key is NOT this: https://learn.chatgpt.com/docs/config-file/config-advanced says it fires 'whenever Codex emits supported events (currently only agent-turn-complete)' — after a turn, never at a session boundary.
gemini unadapted hook the provider documents a prompt event and this distribution ships no adapter for it — https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md — BeforeAgent, 'Fires after a user submits a prompt, but before the agent begins planning'; the raw text arrives as `prompt`. BeforeModel fires later, on the assembled request. Verified <date>.
gemini:session unadapted hooks the provider documents session events and this distribution ships no adapter for them — https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md and https://geminicli.com/docs/hooks/ — SessionStart (source: startup, resume, clear), SessionEnd (reason: exit, clear, logout, prompt_input_exit, other), PreCompress (trigger: auto, manual). Configured as a hooks block in settings.json. Tracker github.com/google-gemini/gemini-cli/issues/9070 'Comprehensive Hooking System' is closed, 34 of 34. Verified <date>. Two caveats an adapter must carry: the CLI does not wait for SessionEnd, and PreCompress is advisory and cannot block.
generic unsupported none no vendor documentation is involved: a worker with no convention of its own exposes no event, and the MCP connection cannot stand in. An MCP server receives initialize, tool calls and notifications; the person's prompt is never among them, in any version of the protocol (modelcontextprotocol.io/specification). Declared none rather than left blank so that no surface can imply otherwise.
generic:session unconfigured connection bin/majordomus-mcp does not exist, so no client can start the shared server whose connection is the boundary
(no adapter) n/a this tool ships no capture adapter for: agents bb codex gemini generic; they read the model and lose the automation
- exit 0
- stdout_contains /claude-code:session +verified/
-
the open freezes the context the builder resolved into the store the layer names — as a snapshot of what this worker was told, not as its working context
session s-<id> opened at <time> (head <head>)
working context: .ai/local/session-contexts/<time>--s-<id>.md
next: majordomus plan next; majordomus context; majordomus session close when the episode ends
- exit 0
- stdout_contains /working context: \.ai/local/session-contexts//
-
the document, composed on this read: the episode, git now against git at the open, and what the episode has recorded since. It used to answer with the snapshot path, which is a file written before the worker had done anything
# Working context of session s-<id>
Composed now, from this episode's record, from git, and from the ledger lines this
episode wrote. It is not stored and is not a snapshot: every read recomposes it.
## Episode
- session: s-<id>
- opened: <time> by hand
- task: none (profile none)
- opening snapshot: .ai/local/session-contexts/<time>--s-<id>.md
- briefing: exact at <head>
## Repository now
- branch: main
- head: <head>
- working tree: dirty
- this episode: 0 commit(s), 0 file(s) changed
## Recorded in this episode
What the worker itself put on the record. These are the three commands that write
here; nothing infers an entry from a diff, and a section with nothing in it means
nothing was recorded, not that nothing happened.
### Progress notes
None. `majordomus checkpoint` records what this episode is doing and why, and is
what puts a note of the worker's own into this context and into the closed record.
### Decisions
None. `majordomus decision add "<what>" --why "<why>"` records one; nothing infers a decision from a diff.
### Open questions
None. `majordomus question add "<question>"` opens one, and `question resolve` closes it.
### Continuation records
None. `majordomus handover` writes the record the next worker starts from.
## The repository's own context
Not repeated here: `majordomus context` resolves it, now, against the current tree.
What this episode was told at its open is under `## Context at open` in .ai/local/session-contexts/<time>--s-<id>.md.
- exit 0
- stdout_contains /^# Working context of session s-/
- stdout_contains /## Repository now/
- stdout_contains /## Recorded in this episode/
-
the frozen opening snapshot is still addressable by name, for a caller that wants the file rather than the answer
.ai/local/session-contexts/<time>--s-<id>.md
- exit 0
- stdout_contains /^\.ai/local/session-contexts/2[0-9]+T[0-9]+Z--s-/
-
a checkpoint is a progress note inside a task, so the two events below have something to be about
started t-<id> profile=implementation scope=lib
next: majordomus context; checkpoint every 15m; majordomus check before claiming anything
-
what the compaction event runs: a progress note composed from git and the ledger, because at that moment nobody is being asked anything
.ai/local/state/checkpoints/<time>--main--<head>--<hash>.md
- exit 0
- stdout_contains /^\.ai/local/state/checkpoints//
-
and what the end event writes when the task is still active: the sections the policy requires, derived from records that already exist, with no model called
.ai/local/state/handovers/<time>--main--<head>--<hash>.md
- exit 0
- stdout_contains /^\.ai/local/state/handovers//
-
the record the start event quotes into the next episode, with the label that says how far to trust it
Handover: .ai/local/state/handovers/<time>--main--<head>--<hash>.md
Match: same_worktree_same_branch
Git state: exact
Created: <time> (<age> ago)
Task: t-<id>
Freshness: fresh — <age> ago
---
# Objective
Prove the lifecycle runs itself
Profile implementation. Declared scope: lib.
# Current State
On main at <head>, working tree dirty.
2 file(s) changed since the task started:
- .claude/
- .majordomus/
Recorded in this episode: 1 checkpoint(s), 0 decision(s), 0 question(s) opened, 0 handover(s).
The newest checkpoint (<age> ago), quoted whole:
> At <head> on main, working tree dirty; 2 file(s) changed since the task started.
> Derived, not authored: `majordomus checkpoint --derive`.
# Next Action
Run `majordomus context` for the briefing this record is a projection of, then `majordomus check` to see whether the task is consistent with its scope and state.
# Decisions
None recorded in this checkout.
# Reasoning
No reasoning was recorded for this task.
# Open Questions
None. Nothing on this branch is blocking acceptance.
# Verification
Not run by this record. Nothing here asserts a test result; `majordomus finish --verify-command` is what records one.
---
Derived by `majordomus handover --derive` from the task record, the ledger, git and the open questions. It asserts nothing that is not in one of those, and no model wrote it.
- exit 0
- stdout_contains /Match: same_worktree_same_branch/
- stdout_contains /Git state: exact/
-
the shared record; the working context learns the outcome in the same document it was opened with
majordomus: session close: knowledge derived: knowledge derive: 0 written, 0 unchanged, 0 skipped for episode s-<id>
.ai/repo/sessions/<time>--s-<id>--main--<head>--<hash>.md
- exit 0
- stdout_contains /^\.ai/repo/sessions//
-
what the provider's end event passes: the episode may have been closed by hand, or never opened, and neither is a failure
The chain from this page to the code: the use case names commands, rules and claims; each rule is dispatched from the registry and each claim names the test that proves it; the scenario above executed the commands. Nothing in this chain is prose about intent.
Computed from what they share: claims, rules, commands, category, applications. Nobody maintains this list.