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 secondstartis refused rather than replacing the first, unless--if-open keepsays 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.--workeris 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.--providerand--provider-sessionrecord 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 makesopened_by: hookin the working context a fact rather than a claim.statusprints 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 ignoremakes "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