Skip to content

majordomus session

Open, inspect and close one execution episode.

Open, inspect and close one execution episode.

  • state-mutating
  • context
  • --json

One open session per worktree; the open record is never tracked. A closed record is an envelope of references selected by the session stamp on each ledger line, never by a time range. start freezes the resolved context into the local working-context store and close appends the outcome to it; --if-open and --if-none are what let a provider hook run both on every event.

Syntax

majordomus session <start|status|close|list|show|latest|context> [options]

Lifecycle

  1. Set up
  2. Begin
  3. Work
  4. Verify
  5. Conclude
  6. Inspect

Other commands at this stage:start, plan.

Reads

  • .ai/local/state/session-current.yaml
  • .ai/repo/sessions/
  • .ai/local/state/ledger.jsonl

Writes

  • .ai/local/state/session-current.yaml
  • .ai/repo/sessions/
  • .ai/local/session-contexts/
  • .ai/local/state/ledger.jsonl

Exit codes

Only the codes this command can produce. The full contract is on the commands index.

  • 0 ok
  • 2 usage
  • 10 contract unmet
  • 12 missing artifact
  • 13 internal error
  • 15 refused

One worker's sitting, as a durable envelope

A session is one execution episode in one worktree. It claims no paths and gates nothing; it exists so that the records an episode wrote can be read back together, selected by the session stamp each ledger line carries.

Opening an episode

session open
no

Given this repository

# A repository with Majordomus installed and projections generated, and one commit of work.
"$MJ" init >/dev/null
"$MJ" update >/dev/null
mkdir -p lib docs
echo a > lib/a
echo d > docs/d
git add . && git commit -qm base

Run

$ majordomus session start --worker some-provider/some-model

Output contains

  • ^session s-[0-9]+-[0-9a-f]+ opened at

exit 0

The identity fields are computed from git. The worker is recorded only because it was supplied; an unrecorded worker stays unrecorded rather than guessed.

Opening a second one here

session open
yes

Given this repository

# Installed, with one execution episode open in this worktree.
. "$FIXTURE_SETUP/installed.sh"
"$MJ" session start --worker some-provider/some-model >/dev/null

Run

$ majordomus session start

Output contains

  • is open here

exit 15

One open session per worktree. Replacing the first would orphan every record it stamped, so the second is refused and told what to do first.

Closing into an envelope

session open
yes

Given this repository

# Installed, with one execution episode open in this worktree.
. "$FIXTURE_SETUP/installed.sh"
"$MJ" session start --worker some-provider/some-model >/dev/null

Run

$ majordomus session close

Output contains

  • ^\.ai/repo/sessions/

exit 0

The closed record names the tasks, checkpoints, handovers, decisions and questions of the episode and copies none of them. The path printed is the record, under the layer's tracked sessions section: a closed episode is a shared object (ADR 0014).

Closing when nothing is open

session open
no

Given this repository

# A repository with Majordomus installed and projections generated, and one commit of work.
"$MJ" init >/dev/null
"$MJ" update >/dev/null
mkdir -p lib docs
echo a > lib/a
echo d > docs/d
git add . && git commit -qm base

Run

$ majordomus session close

Output contains

  • no open session

exit 12

Absence is reported as a missing artifact, exit 12, and never as a silent success.

Where the episode's working context is

session open
yes

Given this repository

# Installed, with one execution episode open in this worktree.
. "$FIXTURE_SETUP/installed.sh"
"$MJ" session start --worker some-provider/some-model >/dev/null

Run

$ majordomus session context

Output contains

  • ^\.ai/local/session-contexts/

exit 0

`start` freezes what the context builder resolved into the local working-context store, and this prints the path rather than the document: local evidence is never poured into a terminal where a context can pick it up.

Reference

Open, inspect and close one execution episode. A session is the seventh durable record and the only one that is not task-shaped.

A task is a unit of work: it is scoped, it has a profile, and it can outlive the worker doing it. A session is one worker's sitting: it claims no paths, gates no acceptance, and may cross several tasks — while one task may be crossed by several sessions. Neither contains the other, which is why they are two records rather than one field.

Sessions are optional. A worker that never opens one loses the episode boundary and nothing else; every other record is written exactly as before.

  • start [--owner <who>] [--worker <id>] [--if-open refuse|keep] opens the episode. One open session per worktree: a second start is refused rather than replacing the first, unless --if-open keep says to keep what is open — which is what a provider hook passes, because its start event fires again on a resume and on a compaction. --worker is a free-form identity string, recorded only when supplied — an unrecorded worker stays unrecorded, because a guessed one is indistinguishable from a recorded one the moment it is written down. --provider and --provider-session record which provider's event opened the episode and that provider's own session identity; only something running inside that provider's hook can supply them, which is what makes opened_by: hook in the working context a fact rather than a claim.
  • status prints the open session with the divergence label of the commit it opened at, or reports that there is none. Read-only. Absence is an answer, not a failure.
  • close [--outcome closed|interrupted] [--if-none refuse|ignore] closes the episode into an immutable record under the layer's sessions section and removes the open one. An authored summary may arrive on stdin and is optional; identity fields in it are refused, as they are in a checkpoint. --if-none ignore makes "nothing was open" the normal case rather than a failure, which is what a provider's end event passes.
  • context [<session-id>] prints the path of an episode's working context. Read-only, and it prints the path rather than the document: the document is local evidence, and a command that pours it into a terminal invites it into somebody's context.

The open freezes the context it was given. start writes .ai/local/session-contexts/<stamp>--<session-id>.md — the front matter of the episode, the context builder's output verbatim, and a ## Notes section for the worker — and close appends a ## Close section naming the outcome and the record. The document is appended to and never rewritten, so what a worker typed into it survives. The store is local: it names this machine and it is a snapshot of a projection, so it is never published and never loaded into a context on its own, and doctor refuses a document that carries a conversation. See capture session for the hooks that make the boundary independent of anybody remembering to draw it.

The closed record is an envelope of references. It names the tasks, issues, milestones, checkpoints, handovers, decisions, questions and evidence of the episode, and copies the body of none of them. It also carries the commits between the opening and closing commit — or the single entry diverged when the opening commit is no longer an ancestor, because a list computed across a history that no longer connects is a fiction.

The lists are derived at close, not accumulated while the session is open. No other command knows sessions exist: checkpoint, decision, question and plan are unchanged. The references are read out of the ledger, which is already append-only, already written only by Majordomus, and already validated.

Selection is by the session stamp on each ledger line, not by a time range. Every line carries the session that wrote it, next to the commit and branch it already carried. A time range was implemented first and was wrong the first time it ran: the ledger is one file per repository, two workers were writing to it, and no timestamp separates them, so one episode's envelope claimed the other's tasks, checkpoints and handovers. A line with no session belongs to no episode — sessions are optional, and work done outside one is attributed to nobody rather than to whoever had a session open nearby.

--outcome takes closed or interrupted. Both are self-reported and neither is verified; interrupted exists because "this episode was cut short and its records may be incomplete" is the one thing about an ended session that changes what somebody does next.

Exit 12 with no open session, 10 when the summary carries identity fields, 15 when the open record belongs to another checkout.

  • list [--all] prints closed episodes, newest first, with each one's divergence label.
  • show <session-id> prints one record whole.
  • latest [--path] prints the newest record that resolves for this worktree and branch.

All three are read-only, and all three print the record's divergence label — exact, advanced, diverged, different_context. No second vocabulary for staleness is invented, because a session written before a branch was rewritten, handed to the next worker as though it still described this history, is exactly what those four words exist to prevent.

Resolution is the rule every other record follows: same repository, same worktree and branch, then same branch, then nothing. A record from an unrelated worktree is never offered — borrowed context cannot be recognised as wrong until it has been acted on. --all lifts the rule explicitly and shows each record's branch, because a record from elsewhere is worth seeing when you asked for everything and is never worth being handed silently.

Ordering is by the recorded timestamp, with ledger position breaking a tie inside one second. Filesystem modification time is never read: it does not survive a clone and it is not the time the record asserts, so touching an old record does not make it the newest. Filename order normally agrees with ledger order, which makes an implementation that fell through to the filename look correct; test/cases/62_session_divergence.sh makes the two disagree on purpose and fails when the ledger is not what decides.

A malformed record is skipped with a warning on stderr and never silently, and never fatally: one unreadable file must not cost the whole listing.

Writes: state/session-current.yaml, mode 0600, written atomically, and one session.started line in the ledger. session_id, repository_id, worktree, branch, start_head and start_working_tree are computed from git and are never authored.

Nothing under .ai/local/state/ is tracked, the open session record included: an open session carries nothing anyone else needs, and a record that arrived from another checkout would make this one inherit an episode it did not open. A record that names another worktree is reported and never obeyed, which keeps the defence in place for every way one can still arrive — a copied working directory, a synced folder.

Exit 15 when a session is already open here, 10 when the record does not parse — a corrupt record fails loudly rather than being read as "no session", because reading it as absent is exactly what would let a second start overwrite it.

$ majordomus session status
No open session in this worktree.
next: majordomus session start

$ majordomus session start --worker some-provider/some-model
session s-20260904153733-fc51 opened at 2026-09-04T15:37:33Z (head 9c13909)
next: majordomus plan next; majordomus context; majordomus session close when the episode ends

$ majordomus session start
majordomus: session s-20260904153733-fc51 is open here since 2026-09-04T15:37:33Z; run majordomus session close first

$ majordomus session close <<'EOF'
The extraction boundary and the session schema landed; the compiler's discovery stage is next.
EOF
.ai/repo/sessions/20260904T171402Z--s-20260904153733-fc51--master--3c9ba2f--c0ffee1234567890.md

Evidence

Every case below runs in CI on Linux and macOS. The site refuses to build if a public command has no behavioural case, no negative case, or no demonstration.

Guarantees this command carries