diff --git a/.claude/skills/handover/SKILL.md b/.claude/skills/handover/SKILL.md index 83001e7..7d19077 100644 --- a/.claude/skills/handover/SKILL.md +++ b/.claude/skills/handover/SKILL.md @@ -8,13 +8,16 @@ description: Procedure for an outgoing lead to write the handover file that a fr A lead session fills up its context and has to be replaced by a fresh one. The outgoing lead writes a handover file, and the new session reads that file and carries on. -**Today this is a manual handoff.** You write the file, then tell the operator where it is. The -operator starts the new session and points it at the file. +**There are two ways to hand off, and the file is the same either way.** -**fleetd #480 will automate the same cycle** — the lead asks, fleetd checks the file, clears the -pane, and tells the fresh session to read it. That work is not shipped yet, so **do not call a -`fleet_handover` tool: it does not exist.** Check the live tool list before assuming otherwise. -Nothing else in this procedure changes when it does ship; only who performs the swap changes. +- **By hand.** You write the file, then tell the operator where it is. The operator starts the new + session and points it at the file. This always works. +- **With `fleet_handover`** (fleetd #480, merged 2026-09-11). You ask fleetd to do the swap: it + checks the file, clears your pane, and tells the fresh session to read it. This needs + `leadRollover:` in `fleetd.yaml`; without it every action answers a clean refusal naming + `NOT_CONFIGURED`, and you fall back to the manual path. Section 11 below is the procedure. + +Nothing else in this skill changes between the two. Only who performs the swap changes. **The new lead's only inheritance is that file.** It does not see your conversation, your plan, or your screen. If the file is thin or wrong, the new lead re-derives what you already knew, and @@ -125,6 +128,41 @@ Do not include: A handover file is a record of state and decisions. It is not a diary. +## 11. Using `fleet_handover` (only if `leadRollover:` is configured) + +**Run the three steps in this order. The order is not a style choice — the wrong order is +refused.** + +1. **`fleet_handover{action: "open", reason: ""}`.** It returns a `token` and the + `handoverPath` you must write to. Nothing has happened to your pane yet. +2. **Write the handover file at that path**, following sections 1–10 above. +3. **Ask the operator, then `fleet_handover{action: "confirm", token, operatorConfirmed: true}`.** + +Why that order: `confirm` refuses with `HANDOVER_STALE` unless the file was modified **after** the +`open` request. That check stops a leftover file from an earlier session being accepted as this +one's handover. So writing the file first and then calling `open` — the obvious order — always +fails. + +`{action: "cancel", token}` drops a pending request without rolling. + +**Things that will surprise you:** + +- **`accepted` does not mean your pane has been cleared.** It means every gate passed and the roll + is scheduled to run once your current turn ends. Say your goodbye in the same turn — you will not + get another one. +- **There is no terminal or session parameter, on purpose.** The pane is always your own, resolved + from your connection, so you can only ever roll yourself. +- **`operatorConfirmed` is your report of what a human told you.** Do not pass `true` because you + are confident. Ask, wait for the answer, then pass what they said. `requireOperatorConfirm` + defaults to `true` and this is the only thing standing between a judgement call and a wiped + session. +- **The roll can still refuse after `confirm` returns**, and by then there is no caller to tell. + Those outcomes are logged only, as `lead-rollover:` lines in the daemon log. +- **If the bootstrap prompt never lands, your context is gone and no fresh session starts.** This + has not yet been proven end-to-end (see fleetd #480). The recovery is the manual path: the file + is already written, so the operator starts a session and points it at the file. That is why you + write the file before you confirm, and never the other way round. + ## Writing style Write in plain English. Use everyday words, one idea per sentence, and active voice. Keep every diff --git a/CLAUDE.md b/CLAUDE.md index 4a2f508..0216a99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -142,6 +142,7 @@ the merge — and merging on a reviewer's word is delegating it by proxy. | Read your own held lead-to-lead mail (no ack) | `fleet_poll{coordId: }` — primary-only; never acks, so `fleet_list`'s `held[]` still shows it after. `fleet_list`'s `held[]` gives only a truncated preview — this is the only way to read the full body | | Collect a held reply | `fleet_poll{target}` · then `fleet_ack{target, msgId}` | | Tear down a member | `fleet_stop{paneId}` | +| Replace your OWN lead session when its context is full | `fleet_handover{action:"open", reason?}` → write the handover file it names → `fleet_handover{action:"confirm", token, operatorConfirmed}`. Primary-only. **In that order**: the file must be modified *after* `open`, or `confirm` refuses it as stale. There is no terminal parameter — the pane is always your own, so you can never roll another lead. `{action:"cancel", token}` drops a pending request | ### Lead ↔ lead — coordinate, never delegate