Skip to content

Automated session retrospectives

/rose-session-learn turns what happened in a coding session into durable, team-shared improvements — a code fix, a skill amendment, a line in the nearest AGENTS.md. It only runs when someone remembers to run it, so sessions closed without it lose their learnings.

Autorun does it unattended. It is opt-in per operator and off by default; with it disabled nothing below happens and the Stop hook only nudges, as before.

How it runs

Stop hook  (scripts/stop-nudge.sh, once per session_id)
  └─ nohup worker.sh                                        detached
       ├─ digest.sh          transcript -> signal, or exit below the gate
       ├─ claude -p          read-only reflect -> candidate JSONL
       ├─ ledger.sh add      bank candidates locally
       └─ ledger.sh ripe     key recurred in >=N distinct sessions?
            ├─ Linear MCP    file the issue that hosts the work
            └─ scratch worktree -> agent edits -> commit -> push -> PR

There is no cron and no LaunchAgent. Claude transcripts (~/.claude/projects/*.jsonl) never leave the machine, which rules out cloud routines and GitHub Actions; and a scheduler would have to rediscover which sessions ended, which the Stop hook already knows — it is handed the exact transcript path.

Why a ledger sits in the middle

A single session cannot tell a real pattern from a one-off. A job that opened a PR per session would bury the team in noise, so nothing reaches the repo from one run. Candidates are banked locally and only promoted once the same learning has recurred across N independent sessions. Recurrence is the filter; the PR is the approval gate that replaces the interactive apply.

Measured over 222 transcripts across 7 days: 115 sessions pass the gate, and a 15-session sample banked 28 candidates under 22 keys with one key recurring in six workspaces — roughly one promotion per fifteen gated sessions.

The gate

digest.sh keeps a session only if it has ≥3 operator turns AND ≥1 failure (an is_error tool result or a hook_blocking_error), then reduces it to the learnable part: operator turns, assistant text and thinking, failed tool results, blocked edits. That is 2.6% of the bytes — 253 MB of transcript becomes 6.7 MB. The discarded remainder is file contents, diffs, search results and tool parameters, none of which a retrospective needs.

The gate selects on friction, not effort: a long clean session taught nobody anything; a short one that failed twice did.

The ledger

~/.claude/rose-session-learn/ledger.jsonl — local and uncommitted. The transcripts behind it exist only on one machine, so recurrence can only be counted per-operator, and several detached workers append concurrently, which in a committed file means a merge conflict per session. The ledger is scratch state on the way to a PR; the PR is the shared artifact.

Deduplication is by key, a kebab-case slug naming the trigger, emitted by the reflecting model. Existing keys are fed back into the next reflect prompt with an instruction to reuse an exact match, so the model does the fuzzy matching and the ledger only counts. A key's weight is its number of distinct session ids.

ledger.sh pending          # key -> distinct-session count
ledger.sh ripe [N]         # keys at or above the threshold
ledger.sh show <key>       # every record behind a key
worker.sh --preview-issue  # what the ledger would file right now

Read by the worker at the end of every run — which is why no second scheduler exists — and by you.

The Linear issue

Filed before the branch, so the branch can be Linear's own branchName; that is what attaches the PR automatically. Title and description come entirely from the ledger, so no model names the work. It carries the session-learning label, starts in Triage, and moves to In Review once the PR exists — never before, since a promotion can still fail at commit or push.

Configure the automation exclusion

Naming the branch after the issue is also what makes Linear's GitHub automation fire on push: an issue filed as Triage comes back In Progress, assigned, and pulled into the live cycle. Nothing on the API side can prevent that. Exclude the session-learning label from the auto-assign and auto-add-to-cycle rules in Linear, or every unattended proposal inflates the active sprint.

If Linear is unreachable the worker logs it, falls back to a learnings/<stamp> branch and opens the PR anyway. Losing the issue must never cost the learning.

What the promoting agent may and may not do

It works in a throwaway worktree branched from origin/develop, never a live workspace — an agent with write access firing while nobody is watching must not land in a branch someone is mid-work on.

It keeps a shell, and is told to run the tests it touches. Code it cannot test is code the pre-commit hook rejects after the run has ended, discarding the work; the prompt tells it to drop any learning it cannot make pass, because a dropped learning costs nothing and a red branch costs a human. git commit, git push and gh are denied — the script owns those, and two writers on one branch is a real bug.

Candidate text is published verbatim into a Linear issue and a GitHub PR, so secret-scan.sh drops any record matching a credential shape (key prefixes, JWTs, private-key headers, connection strings with passwords, KEY=<blob>). It rejects rather than redacts: partial redaction is a bypass surface, and dropping costs nothing given recurrence.

Enabling it

Prefix the hook command in your personal, gitignored .claude/settings.local.json:

"command": "ROSE_SESSION_LEARN_AUTORUN=1 \"$CLAUDE_PROJECT_DIR\"/.agents/skills/rose-session-learn/scripts/stop-nudge.sh"

Autorun is silent by design — the session ends normally and the retrospective runs behind it. Watch ~/.claude/rose-session-learn/worker.log.

Start by watching ledger.sh pending for a few days before letting it promote: a wrong gate or a drifting key scheme shows up there first.

Tunables

Variable Default
ROSE_SESSION_LEARN_AUTORUN unset Enables autorun; unset means nudge only.
ROSE_LEARN_THRESHOLD 3 Distinct sessions before a key is promoted.
ROSE_LEARN_REFLECT_MODEL sonnet Runs on every gated session — keep it cheap.
ROSE_LEARN_PROMOTE_MODEL opus Runs rarely and writes code.
ROSE_LEARN_LINEAR_TEAM Rose
ROSE_LEARN_LINEAR_LABELS session-learning
ROSE_LEARN_LINEAR_REVIEW_STATUS In Review Set once the PR exists.
ROSE_LEARN_STATE ~/.claude/rose-session-learn Logs, lock, ledger.

Logs

File
worker.log one line per session, plus the reflect stage's stderr
promote-agent.log the promoting agent's stderr
commit.log the pre-commit hook output for the last commit attempt
unparsed.log the model spoke but nothing parsed

They are separate on purpose. Sharing one stream once made a hook failure indistinguishable from an agent failure and produced two wrong diagnoses.

Checks

cd .agents/skills/rose-session-learn/scripts
./test-ledger.sh        # counting, promotion, thresholds
./test-parse.sh         # model-output parsing and the schema gate
./test-issue.sh         # Linear issue fields (renders only, never calls Linear)
./test-secret-scan.sh   # credential shapes drop, ordinary errors survive

Known limits

A transcript shows what was corrected, not why — "the operator redirected me" and "the operator changed their mind" are indistinguishable on disk. Recurrence filters most of that out, not all. This is a safety net, not a replacement for running /rose-session-learn on a session you just lived.