Skip to content

session

rigger session start [<project>] [--json]
rigger session draft [<project>] [--heading <TEXT>] [--json]
rigger session end [<project>] [--heading <TEXT>] [--diary <FILE>] [--entry <FILE>] [--remind] [--json]

A session is one sitting, and everything recorded while it is open belongs to it.

Before this, an event knew its project and its day and nothing else, so “what did we do last time” could only be answered with a window of days — which is wrong in both directions. Two sittings in an evening became one blur; one spanning midnight became two. A session records the boundary instead of guessing at it.

Terminal window
$ rigger session start sample
Session open on sample. Everything recorded now belongs to it.

Nothing else has to change. note, wish, and the MCP recording tools work exactly as before; the record applies the boundary itself.

The name may be left out, in which case it is the project the working directory sits in - or the nearest one above it.

Starting twice joins the sitting rather than splitting it:

Terminal window
$ rigger session start sample
A session on sample is already open, since 2026-09-05T09:12:44Z.

An assistant that lost its place, or a hook that fired again, would otherwise orphan half its events under a session nobody ever ends.

Under --json it prints the session and whether it was already open:

Field Type Meaning
session object The open sitting.
session.id integer The session’s id.
session.project_id integer The id of the project it belongs to.
session.started_at string When it opened, an RFC 3339 UTC moment.
session.ended_at null Always null: the sitting is open.
already_open boolean true when a sitting was already open and this call joined it.
kasl object What kasl made of being told about the sitting.
kasl.state string taken (kasl took it), absent (no kasl, or one that does not know focus) or refused (kasl said no; its first line follows in kasl.said).
kasl.said string Only when refused: the first line kasl said.

kasl measures the working day; rigger knows what the day was spent on. When a sitting opens, rigger tells kasl what it is about, and when it closes, that the interval is over - so the time and the work are one truth, and kasl’s report names the stage:

kasl focus start "sample v0.3.0 · Search" --from rigger
kasl focus stop --from rigger

--from rigger lets kasl close only what rigger opened, so a focus started by hand is not ended by an assistant’s session. Joining a sitting already open tells kasl nothing - it is not a second interval.

Without kasl, or with a kasl that does not know focus yet, this is silence: the session opens and closes exactly as it did, and nothing is printed. --json says what came of it under kasl - taken, absent, or refused with the first line kasl said. kasl is given five seconds to answer; a time tracker checking for its own update does not hold a session up. RIGGER_KASL names another kasl to run, and is the only way a record kept under RIGGER_DATA_DIR reaches one at all.

Terminal window
$ rigger session end sample
Session on sample closed, open since 2026-09-05T09:12:44Z.
recorded 1 decision, 1 change
next: ship the retro next
brought the hub up to date: План.md, Дневник.md

The chronicle sync reads out of commit messages is not counted: it is what git already says, and counting it would make any session that happened to run sync look productive.

Under --json it prints the sitting’s summary, then what came of closing it:

Field Type Meaning
session object What the sitting turned out to hold.
session.project string The project’s name.
session.session integer The session’s id.
session.started_at string When it opened, an RFC 3339 UTC moment.
session.ended_at string When it closed (or, for a draft, the moment the draft was made), an RFC 3339 UTC moment.
session.decisions array Decisions recorded during the sitting.
session.decisions[] string One decision.
session.findings array Findings recorded during the sitting.
session.findings[] string One finding.
session.pitfalls array Pitfalls recorded during the sitting.
session.pitfalls[] string One pitfall.
session.changes array Changes recorded during the sitting.
session.changes[] string One change.
session.questions array Questions raised for the owner during the sitting.
session.questions[] string One question.
session.next_step string or null The line the next session starts from; null when none was set.
session.shipped array Versions whose tags landed inside the sitting.
session.shipped[] string One version.
session.tasks_closed array Tasks closed while the sitting was open.
session.tasks_closed[] string One task title.
session.commits array Commits made while the sitting was open.
session.commits[] string One commit’s subject line.
Field Type Meaning
missing array What the ritual would have asked for and did not get, as sentences; empty when nothing is missing.
missing[] string One reminder.
diary string or null The diary file the entry was written to with --diary; null without it.
hub object What bringing the project’s hub up to date did; both lists are empty for a project with no hub.
hub.written array Files of the hub written again because the record had moved on since it last wrote them.
hub.written[] string One file, relative to the hub.
hub.edited array Files left as they were because somebody has written in them since the record did.
hub.edited[] string One file, relative to the hub.
backup string or null The backup file taken at close; null when today’s or a recent copy already exists, or the copy failed.
on_session_end object or null What the project’s closing command came to; null when the project has none.
on_session_end.command string The command that ran.
on_session_end.passed boolean Whether it came back green.
on_session_end.verdict string How it ended, in a line.
kasl object What kasl made of being told about the sitting.
kasl.state string taken (kasl took it), absent (no kasl, or one that does not know focus) or refused (kasl said no; its first line follows in kasl.said).
kasl.said string Only when refused: the first line kasl said.

The end-of-session ritual has always been a list in a skill file that the assistant had to remember at exactly the moment it was running out of context — which is when it is least likely to remember anything. end checks the two things that list names, and says so when they are missing:

Terminal window
$ rigger session end sample
Session on sample closed, open since 2026-09-05T11:40:02Z.
recorded 1 change
The ritual asks for:
changes but no decision, finding or pitfall - nothing says why

Work with nothing written down about why is work the record cannot explain later. A sitting that only reasoned is not asked to justify itself — the check looks for changes without reasoning, not for reasoning without changes.

This is not a gate: the session closes either way. rigger reports; what to do about it is yours.

Сделано. is what the sitting did: the tasks it closed, the changes it recorded, and the commits it made while it was open, each said once - a change recorded by hand usually repeats the commit it describes. The project’s repository is read first, so a commit made in the sitting is in the entry even if nobody ran sync.

The entry is composed, and the assistant is the one who knows what the day meant. So before closing, it reads the draft:

Terminal window
$ rigger session draft sample --heading "v0.3.0 «Search»"
## 2026-09-22 · v0.3.0 «Search»
**Сделано.**
- full-text index
- feat(search): rank by relevance
**Решения.**
- the index is external-content, so text is stored once
**Следующий шаг.** the query language

edits it - a list of what happened, rather than a blank page - and hands the edited text back:

Terminal window
$ rigger session end sample --heading "v0.3.0 «Search»" --entry entry.md

--entry takes a file, or - for standard input. The edited entry replaces the composed one in the record and in the file alike. Its own ## heading is the heading - in the record and in the hub’s diary, whatever --heading said; one without a heading line gets the heading a composed entry would have had, the day and --heading. It is read before the sitting closes, so an entry that cannot be read leaves the session open to try again rather than closed without its diary.

Until v0.31.0 the heading came from two places: the file got the one the entry’s text carried, the record one made from --heading. After any sitting that gave one and not the other, the hub’s diary read differently from the record it was generated from, and doctor --hubs called the file edited.

Under --json it prints the same session summary as end, then the entry:

Field Type Meaning
done array What the sitting did, once each: closed tasks, changes and commits.
done[] string One line of it.
entry string The diary entry the sitting would leave, in Markdown, starting with a ## heading.

The sitting’s entry goes into the record whatever else happens to it, dated the day the session ended. A hub written from the record reads its diary from there - in the rule and spacing the diary already uses between its entries. Before this, end could only append to a file, and once that file was generated the entry went into a file the next export rewrote without it.

Since v0.31.0 end then brings the project’s hub up to date itself, wherever that loses nothing. The record notes what it last wrote into each file of the hub; a file still exactly that is written again from the record, and the entry is at the top of Дневник.md without anybody running export. A file somebody has written in since is left as it is, and named:

Terminal window
$ rigger session end sample
...
brought the hub up to date: Дневник.md
The ritual asks for:
the hub holds edits the record has not seen (Изменения.md): `rigger import sample --hub <dir>` takes them in, `rigger export` discards them

A file the hub does not have is not made - a hub that keeps no rituals file has not asked for one - and a file the record has no note of yet is left for doctor --hubs to raise. A hub that cannot be written does not fail the close: the session is already ended, and the line says why the hub was left.

For a diary the record does not write. A diary kept by hand gets the entry at the top, newest first, under whatever preamble the file already has, with the heading the record holds:

# Дневник работы
Одна запись на сессию, новые сверху.
---
## 2026-09-05 · v0.13.0 «Сессии»
**Сделано.**
- разрез витрины по дням
**Грабли.**
- мутация поймала слабый тест
**Следующий шаг.** ship the retro next

--heading names the entry; without one it is titled by its day alone. A file that does not exist yet is created rather than being a reason to fail at the end of a session.

A generated diary is not appended to: an entry added by hand there is an edit, and before v0.31.0 every sitting that did it left the hub’s diary differing from the record. The hub’s own diary is brought up to date with the rest of the hub, as above; any other generated diary says at its top that edits there are overwritten, and is written whole from the record.

The entry is the session’s own sentences, arranged — rigger has no opinion about the day, and inventing one would put words in your diary that nobody said. Empty sections are left out rather than printed as bare headings.

For a hook. Says nothing unless something is missing, and treats “no session was open” as ordinary rather than as a failure:

Terminal window
$ rigger session end sample --remind
Session on sample closed - 1 event:
changes but no decision, finding or pitfall - nothing says why

A reminder that fires on every stop is a reminder nobody reads.

Claude Code fires a Stop hook when a session ends. Pointing it at session end --remind is what makes the ritual stop depending on the assistant remembering it:

{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "rigger session end --remind" }] }
]
}
}

in ~/.claude/settings.json. Stop takes no matcher - it fires every time.

The installer writes this entry itself, along with registering the MCP server, so a fresh install has a record that closes its own sittings. It is skipped when the assistant is not installed, never fails the install, and adds nothing when an entry naming rigger session end is already there. RIGGER_NO_REGISTER=1 opts out.

No project is named, because the hook has none to give: it is handed a working directory and nothing else. It runs in the project, and the record already knows every project by its path, so the directory is the name. Directories above are searched too, since a session ends wherever the last command left the shell rather than at the checkout’s root.

The hook is a binary, not a script. Corporate Windows machines forbid PowerShell scripts through the execution policy, and a hook calling a .ps1 stops working without a clear error.

The context packet opens with a line saying where the last sitting stopped and what has happened since:

Last session ended earlier today: 2 events recorded, 1 change committed

The question an assistant returning to a project actually has is not “what has been going on” but “what changed while I was away”, and without this line every session re-reads the same history unable to tell which part of it is new. When nothing has happened since, it says so — which tells the reader that the events below are all from before.

Commits are counted apart from recorded events, because most of what happens to a project is commits, and the two are missed in different ways.

The record is one file, and a sitting that has just been written down is the work most worth insuring. So session end takes a backup when the newest copy predates today:

Terminal window
$ rigger session end rigger
Session on rigger closed, open since 2026-09-15T08:02:11Z.
...
copied the database to ...\rigger.v18-20260915-120253.bak

A second sitting on the same day does not take another: today’s copy is insurance enough. The copy is taken on every path a session can close by, including --json and the Stop hook - a step that only runs when someone remembers it is the step that stops happening.

A copy that cannot be written is reported on stderr but does not fail the close: the session has already ended, and an error there would invite a second end on a record that has none open.

Some projects end a session by doing something: publishing what was written, running a linter over it, pushing a stand. project set --on-session-end states it once, and session end runs it after the sitting is written down, records the outcome as a change, and says how it went:

Terminal window
$ rigger session end rhapsod
Session on rhapsod closed, open since 2026-09-19T08:12:04Z.
...
pnpm publish:novellas: green in 6 seconds

A red result does not fail the close - the session is already over and already recorded - but it is listed among what the ritual still asks for, so it is not something the next sitting discovers for itself.

  • context - the packet a session starts from.
  • project - --on-session-end sets what a closing sitting runs.
  • note - what a session fills with.
  • digest - the same question over days rather than sittings.
  • open - starting an assistant with the packet in hand.
  • backup - the copy its end takes.