# SESSION_SETUP — the session ledger (opt-in, cross-platform)

The session ledger closes the kit's multi-session blind spot: every Claude Code session gets a
**RUN_STATE shard**, every "ask" is captured, every close leaves a **mechanical envelope** in the
journal, sessions **see each other's claims** before they collide on a board (Law 3), and
`docs/STATUS.md` is a **derived, always-regenerable** high-level report stamped with the commit it
was derived at. One zero-dependency Node implementation (`session-ledger.js`) runs identically on
Windows, macOS, and Linux — no twin scripts, nothing to drift.

It is **opt-in and feature-detected**: no hooks wired → nothing happens; hooks wired but `node`
absent → the guard launchers exit 0 silently and the capability stays off. It never breaks a session.

## What each hook event does

| Event | Subcommand | Behavior |
|---|---|---|
| `SessionStart` | `start` | Create/reopen this session's `RUN_STATE` shard; inject a bounded orientation: STATUS top block, other ACTIVE/stale sessions + their claims, open approval-inbox decisions, the last session's handoff |
| `UserPromptSubmit` | `heartbeat` | Silent liveness + capture the ask (first line). Prints ONLY when a new foreign write-claim appeared |
| `PostToolUse` (write tools) | `heartbeat --debounce` | Keeps the session visibly alive during long autonomous turns; exits instantly if the shard is < 60 s fresh |
| `Stop` (every turn end) | `turn` | Update the turn ledger (diff since session start, boards touched), regenerate `STATUS.md`, and — **at most once per session** (Law 11), never when `stop_hook_active` is set — block with an instruction to file the session story if real work exists with no journal narrative |
| `PreCompact` | `precompact` | Snapshot the session's asks into the shard before compaction compresses the conversation |
| `SessionEnd` | `close` | Close the shard and append the **mechanical envelope** (asks, turns, diff, boards touched, HEAD movement) to `docs/journal/<date>-session-<id>.md` — the story's skeleton survives even when no narrative was written |

The **narrative** ("purpose / plan / done / pending / decisions to give") is filed by the model via
the `progress-reporting` skill with kind `session`, into the same journal file the envelope lands in.
The Stop gate points at the exact path.

## Wiring

Copy the pieces (bootstrap already installs them):

- `.claude/hooks/session-ledger.js` — the implementation
- `.claude/hooks/session-hook.ps1` + `.claude/hooks/session-hook.sh` — guard launchers
- merge `.claude/settings.session.example.json` into your `.claude/settings.json`

**Windows** — the example file is ready as-is (uses `session-hook.ps1`).

**macOS / Linux / WSL** — swap each command for the POSIX launcher, e.g.:

```json
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-hook.sh\" start" }
```

and `chmod +x .claude/hooks/session-hook.sh` (bootstrap.sh does this for you). If `node` is on
PATH everywhere you run Claude Code, you may skip the launchers entirely and call
`node "$CLAUDE_PROJECT_DIR/.claude/hooks/session-ledger.js" start` directly — same one line on
every OS.

## Bindings (per `CLAUDE.md` / config)

Defaults live in the script; override any of them in `.claude/session-ledger.config.json`
(materialize the values from your `CLAUDE.md` once — the script cannot parse prose):

```json
{
  "runsDir": "docs/runs",
  "journalDir": "docs/journal",
  "statusFile": "docs/STATUS.md",
  "inboxDir": "docs/APPROVAL_INBOX",
  "reportsDir": "docs/reports",
  "metricsDir": "docs/metrics",
  "staleMinutes": 30,
  "debounceSeconds": 60,
  "boards": ["docs/SCREEN_REGISTRY.md", "docs/WORKFLOW_REGISTRY.md", "docs/INTERACTION_REGISTRY.md", "docs/DESIGN_BACKLOG.md", "docs/PROGRESS.md"]
}
```

Git hygiene is automatic: `docs/runs/` self-gitignores (machine-local operational state);
`docs/journal/` and `docs/STATUS.md` are meant to be **committed by the orchestrator at
integration points** — hooks write files, they never run `git commit`.

## Claims — making Law 3 visible

An orchestrator session that intends to write shared boards declares it once:

```
node .claude/hooks/session-ledger.js claim WORKFLOW_REGISTRY SCREEN_REGISTRY
```

Other sessions then see the claim at `SessionStart` orientation and get a one-time injected warning
the moment the claim appears mid-session. Claims of a session whose heartbeat is older than
`staleMinutes` are treated as **expired** — a crashed session never blocks the checkout forever.
Closed shards older than `retentionDays` (default 14) are pruned automatically at the next
`SessionStart`; the journal, not the shard, is the durable record.

## Stage timing → METRICS

Bracket sweep stages so the METRICS `durMin` derives from real timestamps
(see `docs/METRICS_README.md`):

```
node .claude/hooks/session-ledger.js stage build start
node .claude/hooks/session-ledger.js stage build end
```

## Stated limits

- **Worktrees:** `/build-wave` worktrees each carry their own `docs/runs/` — wave sessions are not
  visible to the main checkout's ledger. Acceptable by design: a wave is driven by ONE orchestrator
  session and integrates serially; the ledger coordinates sessions on a single checkout.
- **Narrative quality is the model's duty.** The gate forces the *moment* (once), the envelope
  guarantees the *skeleton*; the prose itself comes from following `progress-reporting --kind session`.
- **Same machine only.** Sessions on different machines against the same remote are coordinated by
  git, not by the ledger.

## Self-test

```
node .claude/hooks/session-ledger.js selftest        # SELFTEST: PASS
pwsh .claude/tools/SessionLedger.Tests.ps1           # kit-gate wrapper (skips cleanly if node absent)
```
