-
Snapshot live state (don't trust memory — observe):
-
git -C <repo> status -sb and git -C <repo> log --oneline -8
-
Uncommitted diff summary (git diff --stat), current branch, any open PR (gh pr view if relevant)
-
Any in-flight deploy/build/job state relevant to this work
-
Resolve which clawgate task this session belongs to — one read-only command, no network reasoning of your own:
bash ~/workspace/devrc/scripts/lib/clawgate_handoff.sh resolve
It reads GET /api/sessions/{id}/tasks for the session named by CLAUDE_CODE_SESSION_ID and prints one verdict. 🔴 That is the variable's exact name, and there is no CLAUDE_SESSION_ID. Reading a name that does not exist ships an INERT feature that cannot be told from a working one, because an unset variable and a session that touched nothing produce the same empty result. The tool refuses rather than guessing: NO SESSION ID (exit 3) is its own outcome and is never folded into "no task".
Each linked row carries a role, and the verdict RANKS by it instead of counting links: worked (this session commented on the task or flipped its status), created (it FILED the task), read (it only fetched it). worked is the signal because claude/skills/clawgate/flows/task-pickup.md mandates the comment/status write-back on every pickup and a Stop hook blocks the turn without it.
🔴 CAPTURE the status — a PIPE EATS it: … | tail; echo "rc=$?" prints 0 for a real 5. Use out=$(… resolve 2>&1); rc=$?. 📖 ~/.claude/skills/handoff/reference/exit-code.md.
Act on the exit code — and on nothing else:
- 0, one WORKED task → record it in step 2's front matter. Still 0 however many
created/read rows sit beside it. With no role on ANY row (an older server) it falls back to "exactly one task" and prints ROLES UNAVAILABLE — read the rows yourself before recording.
- 6 → ASK the user which one. Do not guess, and do not record more than one. The output says which of four this is: several WORKED tasks; no worked task at all beside one or more
created/read links; a role the tool does not recognise; or several tasks with roles unavailable.
- 🔴 The no-worked case is not "pick one" — filing or reading a task is not doing its work, so the doc most likely belongs to none of them. Record nothing unless you recognise one. A lone
created row is this case, not a resolution.
- 5, nothing resolved → 🔴 write no field, and say so plainly in your report. An unknown session id answers
200 with an EMPTY ARRAY rather than a 404, so an empty result cannot distinguish "this session touched no task" from "the id is wrong". It is not a clean bill of health.
- 3 or 4, the board did not answer → same: no field, and say the board was not reached. Never treat silence as "no task".
🔴 NEVER create a task here. /handoff records what already exists; a task minted to fill a blank field is a fact nobody asserted, and it will be reconciled against for the life of the doc. Authoring a task is its own interviewed flow (claude/skills/clawgate/flows/task-authoring.md, enforced by a PreToolUse hook), not a side effect of writing a handoff.
⚠ Even a worked row is a CANDIDATE, not proof this session did the work this doc describes — it records that the board accepted a write, nothing more. Read the title before recording it, and prefer asking over recording a task you do not recognise. One known blind spot: created is TERMINAL upstream and outranks worked, so a session that FILED a task and then worked it stays created and lands in the no-worked case.
-
For every UNRESOLVED bug/investigation, capture the live diagnosis state (the next section). This is the single highest-value part of the handoff: without it, the next session re-runs every probe you already ran. Record observed values and eliminations, not narrative — paste the actual error string, the actual header/response, the exact failing request, the command whose output you read. "We looked into the CSP issue" is worthless; "frame-ancestors on app.example.test = https://example.test https://*.example.test — does NOT include gen-matrix.embed.example.test, confirmed via response header on GET /apps/run/dogfood-manual" is the whole point.
-
Draft the handoff doc into a SCRATCH FILE. 🔴 claudedocs/handoff-<topic>.md is written by step 5 and by nothing else — whether or not it already exists. Draft ## headings into a scratch file under your scratchpad directory, never inside the repo — an in-repo scratch file lands in step 4's session AND git windows, and --exclude names the handoff doc, not it — then land it in step 5, which owns the merge, the gate and the commit. When the doc EXISTS your scratch file is a delta — omit a section and it is left alone. When it does NOT, the delta simply becomes the doc verbatim, so write the whole structure below into it. Use this structure — be concrete, link exact file paths and commands, no vague prose:
🔴 Never Write the doc yourself, and the NEW-doc case is the one this is about. MEASURED: step 5 is the only step that commits, and run against a doc you already wrote in full it returns status=no-change (exit 5) — whose instruction is report the line and stop. The doc then ends the session untracked, which claude/RULES.md names as unsaved work one routine checkout away from silent deletion. handoff_doc.py handles the no-base case itself and gives it the same diff, the same warnings and the same commit+push; writing the file first is what takes them away.
🔴 The clawgate-task: field from step 1 goes in YAML front matter, at the VERY TOP of the file — before the # Handoff: line, nothing above it. On a NEW doc that means the top of your scratch file, which becomes the doc verbatim. That position is load-bearing: /resume only parses a block whose --- is line 1, because a --- further down a markdown doc is a horizontal rule and letting one open a front-matter block would let body prose mint a task id. Omit the whole block when step 1 resolved nothing.
🔴 On an UPDATE, check before you add: bash ~/workspace/devrc/scripts/lib/clawgate_handoff.sh field <doc> exits 0 and prints the id when a readable field is already there (leave it alone), 1 when there is none (add it), 2 when the field is there and unreadable — either a value that is not a task id or a front-matter block that is never closed; the stderr line says which, and the repair is to that block, never a second field. A doc with two fields reconciles against whichever the parser reaches first, which is not a choice anybody made.
-
Output a kickoff block (fenced, ready to copy-paste into the next session) of the form:
/resume — continue the <topic> work. Canonical handoff (read first): <repo>/claudedocs/handoff-<topic>.md
<one-line of the single most important next action>
🔴 Keep the literal /resume prefix, and do NOT rely on it. Measured twice: both a prose kickoff and the /resume-prefixed replacement got the doc read and the index skipped entirely — a subagent gets the kickoff as prompt TEXT, with no CLI slash-command parsing, so the prefix reads as a topic label. The deterministic hook is the DOC, not this block: both sessions read it first, immediately, which is why the index command lives at the TOP of it. The prefix costs nothing and does work in an interactive session; it is not a mechanism. 📖 Both measurements: ~/.claude/skills/handoff/reference/kickoff-prefix.md.
🔴 Emit this BEFORE steps 4 and 5, unconditionally. The kickoff block is the deliverable, and everything after it can refuse: step 4 can dead-end and step 5 can exit without writing, so a run that never reaches the end must still have handed it over. ⚠ It therefore names a path step 5 may not land: if step 5 is declined or refuses, say in the same breath that the doc does not carry this session's findings — an unqualified kickoff pointing at a stale or absent doc is worse than none.
-
Record what this session touched in the subsystem index — follow the subsystem-index skill, whole, and come back here — ~/.claude/skills/subsystem-index/SKILL.md if the skill does not fire, because a pointer that only names a skill stops resolving the moment that skill's listing entry is evicted, and this one is a prime candidate (its own description says rarely run directly). It owns the protocol: which window to read (--session / --pr / --commit / git branch), how to escalate when the first one comes back thin, what to append, and where the entry is allowed to be written.
🔴 Do not improvise a short version of it. Every rule in there is load-bearing and most were measured after a session got it wrong — the windows are blind in opposite directions, their path sets must never be merged, and the store is client-confidential while devrc is PUBLIC.
⚠ Its outcome is a REPORT, not a gate on this one. Declining to write is a normal, frequent result; so is a dead end that routes the lesson to a skill instead. Say which happened, then carry on to step 5 either way — nothing about the index decides whether the handoff doc lands.
🔴 --exclude claudedocs/handoff-<topic>.md — pass it on every run. This step runs BEFORE step 5 lands the doc, so on a first run the doc is usually not there yet, but a --pr/--commit window over work that already carried one will list it, and a repeat run finds the copy the earlier run committed. Without it claudedocs is a nomination on every single run.
-
Land the handoff doc — the write+push gate. MEASURED: a session re-entered from a handoff, did ten minutes of real analysis, then wrote and pushed an updated handoff to a shared branch that nobody approved. /resume is read-only and followed its contract; nothing gated the doc's own write+push. (This sentence used to add "step 4's index write is gated" as the contrast — that gate was retired 2026-08-15 and step 4 is now a pointer to subsystem-index, so the contrast had become false in two ways.)
🔴 Do NOT forbid updating the handoff — that one was correct and valuable (it answered the doc's open question and corrected a prior misreading), and suppressing it costs the next session the same ten minutes. Make the update safe, not rare.
🔴 This step CREATES the doc as well as updating it. With no base the merge has nothing to classify, so your scratch file becomes the doc verbatim and the run prints it as one added-lines diff — same status=proposed, same warnings, same --confirm/--push. There is no second, ungated path for a first write, and step 2 is where the temptation to invent one lives.
Answer first, in one line: what changed since the doc was written? If the honest answer is nothing, say so and write nothing — a handoff that still describes reality is not stale. On a NEW doc the question has no "since": answer with what this session produced, because a doc that does not exist cannot still be describing reality. Otherwise merge it; this writes nothing and prints the diff you are about to ask about:
python3 /home/zach/workspace/devrc/scripts/lib/handoff_doc.py --repo <repo> --topic <topic> --update <scratch-file> --advanced '<what changed since the doc was written>'
The doc's YAML front matter survives this merge — split_front_matter carries the base's block through, so a delta that starts with prose rather than a ## heading can no longer silently drop the clawgate-task: field. Put a front-matter block in your delta ONLY when you mean to change the recorded task; an explicit one wins. 🔴 The NEW-doc case inverts that: there is no base block to carry, so the delta's own front matter is the doc's only chance at one — if step 1 resolved a task, it must be at line 1 of the scratch file.
🔴 Status header REPLACED, findings APPENDED — which is why the tool merges rather than you rewriting the file. State now/Next steps/ are current state and are overwritten; // append and the earlier text survives , even when your block supersedes an old one — the value is seeing a prior reading was , not finding it gone. A section your delta omits is left untouched. The append allowlist is and everything else replaces, so the run prints a line naming which of the two each section you touched landed in — read it (a NEW doc replaces nothing, so it gets no such line, and its absence there is not a fault), because it is the fact the next paragraph is a consequence of.
Keep the doc tight and high-signal — it is read first thing next session, so every line must earn its place. The "Open investigations" blocks are the exception to brevity: a mid-diagnosis bug is worth verbatim evidence, because re-deriving it next session costs far more than the lines do. Pair: /resume.