Skip to content

Have the session opened and closed without anybody remembering to

Wire the provider's own session events, and the episode opens when the sitting begins, hands the worker what the last one left, records what a compaction is about to discard, and closes with a continuation record beside its envelope.

guaranteed Sessions and handoverbasicagentoperatorwrites state

Situation

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.

Scenario

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'

Outcome

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.

The scenario

Executed against the real tool, in a repository prepared by provider-reachable, every time this site is generated. What follows is not a description of what should happen; it is what happened.

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

when

  1. 1majordomus capture status
  2. 2majordomus capture install
  3. 3majordomus capture status
  4. 4majordomus session start
  5. 5majordomus session context
  6. 6majordomus session context --path
  7. 7majordomus start Prove the lifecycle runs itself --scope lib
  8. 8majordomus checkpoint --derive
  9. 9majordomus handover --derive
  10. 10majordomus handover --resolve
  11. 11majordomus session close
  12. 12majordomus session close --if-none ignore

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

What the tool did

Evidence of the run this page was generated from: pass, 12 step(s). Output is normalised: the repository's path, timestamps, ids and durations are placeholders; nothing else is edited.

Tested by test/cases/94_use_cases.sh, which runs every scenario and refuses a step whose recorded output is not what it produced.

  1. 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/
  2. 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
  3. 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/
  4. 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//
  5. 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/
  6. 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-/
  7. 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
    • exit 0
  8. 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//
  9. 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//
  10. 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/
  11. 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//
  12. what the provider's end event passes: the episode may have been closed by hand, or never opened, and neither is a failure

    • exit 0

Why the tool knows this

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.

Over MCP the same layer is reached through majordomus_continuity (the MCP surface).

The rules that make it hold

Guarantees exercised

Related use cases

Computed from what they share: claims, rules, commands, category, applications. Nobody maintains this list.

Searches the titles and descriptions of every page this site publishes.