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.
session start
Section titled “session start”$ rigger session start sampleSession 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:
$ rigger session start sampleA 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. |
Told to kasl
Section titled “Told to kasl”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 riggerkasl 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.
session end
Section titled “session end”$ rigger session end sampleSession on sample closed, open since 2026-09-05T09:12:44Z.
recorded 1 decision, 1 changenext: ship the retro nextbrought the hub up to date: План.md, Дневник.mdThe 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. |
What the ritual asks for
Section titled “What the ritual asks for”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:
$ rigger session end sampleSession 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 whyWork 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.
The entry it leaves
Section titled “The entry it leaves”Сделано. 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.
session draft
Section titled “session draft”The entry is composed, and the assistant is the one who knows what the day meant. So before closing, it reads the draft:
$ 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 languageedits it - a list of what happened, rather than a blank page - and hands the edited text back:
$ 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. |
In the record
Section titled “In the record”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.
The hub, kept up
Section titled “The hub, kept up”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:
$ 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 themA 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.
--diary
Section titled “--diary”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.
--remind
Section titled “--remind”For a hook. Says nothing unless something is missing, and treats “no session was open” as ordinary rather than as a failure:
$ rigger session end sample --remindSession on sample closed - 1 event: changes but no decision, finding or pitfall - nothing says whyA reminder that fires on every stop is a reminder nobody reads.
As a hook
Section titled “As a hook”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.
What the packet gains
Section titled “What the packet gains”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 committedThe 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.
It insures the record
Section titled “It insures the record”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:
$ rigger session end riggerSession on rigger closed, open since 2026-09-15T08:02:11Z....copied the database to ...\rigger.v18-20260915-120253.bakA 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.
What a project does when a sitting ends
Section titled “What a project does when a sitting ends”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:
$ rigger session end rhapsodSession on rhapsod closed, open since 2026-09-19T08:12:04Z....pnpm publish:novellas: green in 6 secondsA 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.