Skip to content

Keep the prompts that started the work, below the model rather than around it

Wire the provider hook, prove it by running a payload through it, and see that a prompt the tool could not parse is reported rather than silently dropped.

guaranteed Sessions and handoverbasicmaintaineragentwrites state

Situation

The prompt that started the work is the one thing nobody keeps. It lives in a provider transcript that the next session cannot read, so six weeks later the repository holds the change and no record of what was asked for.

Scenario

setup: capture-installed
given:
  - 'Majordomus installed, with the provider hook written and the executable on PATH'
steps:
  - id: is-it-real
    run: ['capture', 'status']
    note: 'the state is read from the provider configuration and a payload driven through the shim, not from a flag somebody set'
    expect:
      exit: 0
      stdout_contains: ['claude-code', 'majordomus-capture']
  - id: one-prompt-one-record
    run: ['capture', 'prompt', '--provider', 'claude-code']
    stdin: claude-code-prompt.json
    note: 'what the provider sends on UserPromptSubmit; the record is the person half of the exchange'
    expect:
      exit: 0
      files_exist: ['.ai/local/prompts']
  - id: both-formats-or-neither
    run: ['capture', 'render']
    note: 'a prompt is kept twice under one stem: the record for a machine, the rendering for a person. render rebuilds a missing rendering from its record and never the other way round, because only that direction is possible'
    expect:
      exit: 0
      stdout_contains: ['rendering']
  - id: never-rejects-a-prompt
    run: ['capture', 'prompt', '--provider', 'claude-code']
    stdin: not-a-payload.txt
    note: 'in a hook, exit 2 rejects the person prompt, so a payload the tool cannot read is written to the failure log and the turn goes on'
    expect:
      exit: 0
      files_exist: ['.ai/local/prompts/.capture.log']
  - id: a-subcommand-that-is-not-one
    run: ['capture', 'nonsense']
    note: 'the command line still refuses what a person typed wrong: only the hook path is forbidden to exit 2'
    expect:
      exit: 2
      stdout_contains: ['unknown subcommand']
then:
  - 'the archive is written under the ignored half of the layer and never committed'
  - 'every prompt is present as both a JSON record and its Markdown rendering'
  - 'a prompt the tool could not read is a named failure, not a silence'

Outcome

The prompts are captured below the model, by the provider hook, into files the repository ignores and nothing loads into a context. Whether the wiring is real is answered by running a payload through it rather than by a claim, and a payload that cannot be read is reported instead of dropped.

Each prompt is kept twice under one stem, because the two readers are different. The .json record is what the provider sent — its own spans, still escaped, pretty printed one member per line — and it is what everything else is derived from. The .md beside it is that record rendered for a person: the fields as front matter and again as a table, then the prompt itself under ## PROMPT, decoded and fenced. An archive nobody opens is evidence of nothing, and a rendering nothing can be rebuilt from is not evidence at all, so the pair is enforced rather than left to habit. The asymmetry is what makes that safe: capture render rebuilds any rendering from its record, and no command can go the other way.

The scenario

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

given

  • Majordomus installed, with the provider hook written and the executable on PATH

when

  1. 1majordomus capture status
  2. 2majordomus capture prompt --provider claude-code
  3. 3majordomus capture render
  4. 4majordomus capture prompt --provider claude-code
  5. 5majordomus capture nonsense

then

  • the archive is written under the ignored half of the layer and never committed
  • every prompt is present as both a JSON record and its Markdown rendering
  • a prompt the tool could not read is a named failure, not a silence

What the tool did

Evidence of the run this page was generated from: pass, 5 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. the state is read from the provider configuration and a payload driven through the shim, not from a flag somebody set

    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/
    • stdout_contains /majordomus-capture/
  2. 2$ majordomus capture prompt --provider claude-code exit 0< claude-code-prompt.json

    what the provider sends on UserPromptSubmit; the record is the person half of the exchange

    • exit 0
    • file_exists .ai/local/prompts
  3. a prompt is kept twice under one stem: the record for a machine, the rendering for a person. render rebuilds a missing rendering from its record and never the other way round, because only that direction is possible

    0 rendering(s) written into .ai/local/prompts
    • exit 0
    • stdout_contains /rendering/
  4. in a hook, exit 2 rejects the person prompt, so a payload the tool cannot read is written to the failure log and the turn goes on

    • exit 0
    • file_exists .ai/local/prompts/.capture.log
  5. the command line still refuses what a person typed wrong: only the hook path is forbidden to exit 2

    majordomus: capture: unknown subcommand 'nonsense' (prompt|render|reconcile|prune|session|install|status)
    • exit 2
    • stdout_contains /unknown subcommand/

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.

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.