Resuming
Something interrupted the loop. Your first job is to find out what actually happened, not to get going again — because the cheapest-looking recovery action, re-dispatching a task whose agent is gone, is also the one that can destroy work nobody knew was there.
/devteam:run already reconciles and recovers on startup. This exists because
it does so immediately, and after an interruption the record and the tree can
disagree in ways only a human can settle.
It also covers the planned handoff, which is not an interruption at all: a session handing to a fresh one at a good stopping point, so the memory files reload near the top of the new context and the finished task's debris does not follow it. That case has a resource a crash does not — the predecessor is still alive — and §0 exists to use it before anything else.
0. If the previous session is still alive, ask it
Reconstruction is what you do when nobody is left to ask. A handoff at a planned stopping point is a strictly better position than a crash, and this skill was written as though only the crash existed.
So before reconstructing anything: is the outgoing session still open?
ListAgents will say. If it is, message it — and keep it open until you are
done, because once it closes you are back to reading files.
You drive, not it. A written handoff can only contain what its author thought to include, and what an outgoing session thinks is important is a poor predictor of what an incoming one cannot work out. So do not ask for a summary. Read the record first, then ask about the specific things you could not determine from it — why a task was declined rather than closed, what a half-finished branch was for, which of two plausible readings of a note is right, what the client actually said as opposed to what was minuted.
Every question you have to ask is a defect in the record
This is the part worth more than the handoff. Treat the questions as a measurement: anything you had to ask about is something the written state failed to carry, and it will fail to carry it again for the next reader, who may not have anybody to ask.
So log them. A short list under the day's RECORD.md entry — "the incoming
session had to ask X, Y and Z" — is a free audit of the record's completeness,
taken at the one moment both the record and somebody who knows better are
available at once. It is also one of the very few points where this pipeline
looks backwards at what it wrote rather than forwards at what it will write
next.
A planned rotation says so in a file, and it is a pointer
devteam/.run/session/handoff-ready exists only when a manager rotated itself
out at a checkpoint (run §7b). Two lines: session <id> and
checkpoint C-n. That is deliberately all of it — it tells you a rotation
is in progress, who started it and at which point, and nothing that could be
stale by the time you read it. Claims come from BOARD.md, open sandboxes
from python3 "${CLAUDE_PLUGIN_ROOT}/scripts/sandbox.py" status, the tree
from the tree. cat is the whole parser.
If the file names your own session you wrote it and your successor has not
arrived yet: keep working (run §7b step 4). If it names another, you are the
successor, and the order below matters:
ListAgents, and expect the identity not to join. Listed → your predecessor is alive and you are in the normal case. Not listed → it has exited; reconstruct from §1, and say in one line inRECORD.mdthat the handoff was announced but the predecessor was gone before you arrived. A stale socket fails loudly, not silently — a send to an exited session returnsENOENT ... the peer process may have restarted, so you will not mistake a dead peer for a quiet one.ListAgentsprints a name and a short ref; the board andhandoff-readycarry a session id; nothing carries both. So you cannot prove the busy peer named<something>is the session that wrote the pointer — you can only ask it. Ask in the same message as your first questions rather than discovering it as a defect afterwards: "are you session<id>?" is one line and it is the join. This is the same missing join the board'sAgent idcolumn exists to fix one layer down, and it is unfixed at the session layer because a session cannot read its own name.Read the record before you ask anything, and do not take the lock yet. §1 and §2 still govern: a rotation is a better position than a crash, not a licence to write early. Your predecessor is still working — that is by design — so what you read will move; the lock is the point at which it stops moving.
Ask. You drive. Only what the files could not tell you. Log every one as
record defect: the incoming session had to ask <what>under the day'sRECORD.mdentry, one line each. That list is the measurement, and it is taken at the only moment when both the record and somebody who knows better are available at once.Take the lock. Write
${CLAUDE_CODE_SESSION_ID}todevteam/.run/session/manager, put the same id onBOARD.md's**Writer.**line, and commit:board: writer <new id> (rotation from <old id>, C-n). Thenwriter handoff: <old> → <new>inRECORD.md. The id must be a real one — a writer line holding an empty pair of backticks reads as neither yours nor anyone's and inverts the guard in both directions.Tell your predecessor, and ask it for one thing: "I hold the lock as of
<commit>. Finish nothing further; end your turn — and reply with the sha of your last intended write." Itsrun§2 says what it does next. From that commit the guard refuses itsdevteam/writes, which is the control for this whole mechanism and is the same one that has always prevented two writers.The sha is asked for because the record cannot answer it. A predecessor writes nothing after being replaced (that is the design), so nothing in
devteam/says where its work stopped — and its last commit can land after the pointer was written, so "the commit before the rotation" is not the answer either. Record it:writer handoff: <old> → <new>, predecessor's last write <sha>. Measured — the first live rotation's successor had to ask this, and logged it as a record defect.Rewrite
handoff-ready; do not remove it. Append one line:completed <iso> by ${CLAUDE_CODE_SESSION_ID}An absent file and a file nobody wrote read the same — the reason P-14b never deletes a heartbeat, and it applies here for a sharper reason. Your predecessor is still a live session that believes it was the manager. If it tries to write, the guard's refusal has to be able to tell "you were replaced, do not take the lock back" from "a stranger holds the lock, take it if they are gone" — and the only thing that distinguishes them is this file naming that session. Deleting it turns the correct refusal into the dangerous one, measured: the first live rotation removed the file at this step, and the predecessor's next write got the generic takeover recipe.
The
completedline is what stops the next reader treating a finished rotation as one in progress. It stays until the next rotation overwrites it.
If the outgoing session is gone, continue from §1 and reconstruct. That is the degraded case, not the normal one.
1. Do not take the lock yet
Read first, write nothing. Do not set the board's writer line, re-pin the environment, or touch a title. If another session is genuinely still alive you have not yet trampled it, and if it is not, nothing was lost by looking.
2. Establish what the record claims
devteam/BOARD.md— what is claimed, by which agent label, since when, and which session holds the writer lockdevteam/RECORD.md, last entries — the last thing that was known to happen- every task title under
devteam/tasks/— the states as the supervisors left them devteam/QUESTIONS.md— anythingopenwas waiting on the client, and may have been waiting for days
3. Establish what the tree actually holds
The record is what somebody meant to be true. These are facts:
git -C "$REPO" status --porcelain
git -C "$REPO" log --oneline -15
ls -la devteam/.run/locks/ # heartbeats: which step since when,
# or `closed <date>, verified PASS`
cat devteam/.run/locks/*.sandbox # under `structural`: which worker ran,
# its pid, and WHERE its overlay is
ls -la devteam/.run/session/ # which session believed it held the lock
The .sandbox files are part of what the tree holds, and they are the only
record of a worker that a restarted session can see (P-14b). ListAgents
never saw a headless worker even while it lived. Each line names the task, the
step, the sandbox id, the pid, and the sandbox's root as an absolute path —
so a session that never spoke to the one that dispatched it can still find the
overlay. Then:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/sandbox.py" status # what is still open
A kept sandbox with a non-empty upper/ is work a predecessor did not
promote. It is the one thing in this whole procedure that a restart does not
recover on its own: the commits are real, they are on disk, and nothing on the
host knows about them. List every one of them in your report, by id and by
what it holds, before anything is re-dispatched — a re-dispatch opens a new
sandbox from the current base and the old work stops being reachable from the
board.
Then run all four checks. They compare the two directly and are the fastest route to the disagreement:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_trace.py" .
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_refs.py" .
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_scope.py" .
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/check_report.py" . <each claimed task>
4. Reconcile, and look for the cases that mislead
For every CLAIMED row, apply the recovery table in run §3 — liveness over
the whole agent subtree, then the heartbeat, then the tree's mtimes (P-14).
Then check specifically for these, because each one makes a lost task look like
a finished one or the reverse:
| Looks like | Might actually be | How to tell |
|---|---|---|
the work was lost — RUNNING, clean tree |
committed under somebody else's message (F-17) | check_scope reporting misattributed-write; git log for the scope's paths |
a finished task — title DONE |
never verified; the verifier ran and FAILed, or never ran | RECORD.md has no verify … PASS line for it |
| nothing in flight — no heartbeat | a supervisor that died before its first dispatch | the task file is dirty but no commit exists |
a task still working — heartbeat says waiting on S-n |
a task that closed, whose heartbeat was never retired | the board and the task title say DONE; a heartbeat is retired to closed <date> at close, never deleted, so a live-looking one after a close is a lie told to this procedure |
| a live claim | a task stopped for a question, whose title was never updated | QUESTIONS.md has an open item naming it (P-27b) |
| a stale claim | a live worker under a completed supervisor | a live child in ListAgents (P-14) |
| a stale claim, nothing live anywhere | a headless worker still writing its overlay | ListAgents cannot see one at all; the .sandbox file's pid can, and a live pid there means the claim is working (P-14b) |
the work was lost — RUNNING, clean tree, dead worker |
done but never promoted | the .sandbox line names a root whose upper/ is not empty; the commits are in the overlay, not the host |
An uncommitted change under a claimed task's scope is work, not debris. Read it before deciding anything. A predecessor's uncommitted diff is frequently the most valuable thing in the tree — it is what the worker had learned and had not yet said.
5. Report before you act
Tell the client, on the charter's Client channel, in under twenty lines:
- what was in flight and how long ago it last moved
- what is uncommitted, path by path, and whether it looks like work or debris
- what the checks say, especially where the record and the tree disagree
- what has been waiting for them — open questions, and for how long
- what you propose to do, task by task: continue, re-dispatch, or ask
- what re-dispatching would destroy, if anything
Then stop and wait. This is a gate, not a courtesy. The client may know something the record cannot: that a task was finished and the commit did not land, that the interruption was a machine reboot rather than a failure, that the whole thing should be abandoned. Re-dispatching a task whose worker had uncommitted findings costs all of them, silently.
Where the channel is none, proceed on the safest reading — continue what
is clearly live, leave anything ambiguous stopped, and record every judgement
as an assumption for the next checkpoint. Do not re-dispatch anything whose
loss you cannot rule out.
6. Only then, hand back to the loop
On the client's word: take the writer lock, record a resumed line and a
stale claim line per recovery in RECORD.md, and run /devteam:run. The
loop's own startup will reconcile again, which is harmless and is one more
chance to notice something.
What resuming must never do
- Never re-dispatch before reporting. The whole point.
- Never stash or discard an uncommitted change to get to a clean tree. A clean tree is not the goal; an accurate one is.
- Never take the lock from a session you have not established is gone. Two writers is the failure the design exists to prevent, and an interruption is exactly when it is most tempting.
- Never quietly repair the record. If the board and a task file disagree, say so, fix it deliberately, and record that you did — a resume that tidies is a resume that erases the evidence of what went wrong.