Repository · workstreams
Workstream: Handoff
Status: Delivered (2026-08-01) · Component: Enable agentic swarms
The Handoff service itself (overview) is live at
url://handoff/with its WUI at handoff.wasmserver.com. This workstream covered the next capability: at-a-glance status colors — every handoff card answers "what does this need right now?" without being opened. It shipped on 2026-08-01 and is live; the design text below is retained as the record of what was built.Follow-on workstream: HandoffConversationIdentity ties each claim and status report to the precise conversation (harness-native session id / supervisor thread) doing the work, merging the claim and conversation-attachment mechanisms this document introduces.
Outcome (delivered 2026-08-01, re-verified 2026-09-15)
Every element of the design below is implemented, merged, and serving production traffic. Verified against the live system rather than from memory: handoff.wasmserver.com renders status-color / status-detail classes and per-card report-freshness enrichment on its cards, detail pages carry claims and status-report panels, and 49 canonical files in this repository's handoffs/ mirror carry a blocked-reason: frontmatter key.
- Service and contract: HandoffApi PR 6 (
handoff:api:0.0.8), HandoffEmbedded PR 6 (handoff:embedded:0.0.8), HandoffServiceServer PR 15 (handoff:serviceserver(-client):0.0.12).HandoffRecordgained the durableblockedReasonplus the computedstatusColor/statusDetail;blocked-reasonjoined the canonical file's allowed frontmatter keys and round-trips through the git mirror. - CLI: HandoffCli PR 3 (
handoff:cli:0.0.2) —claim,heartbeat,release,report,claims,reports,update --blocked-reason/--clear-blocked-reason, and the four-colorSTATUScolumn onlist. - WUI: HandoffWui PR 10 (color every handoff by its status) and HandoffWui PR 11 (latest status report on the detail page, report freshness on the card), with HandoffWui PR 14 rendering a report as the Markdown it is written in.
- Skills: claude-code-skills PR 114 — the
status-reportskill uploads every report in full and treats a RUNNING report as the heartbeat; thecreate-handoffskill claims on pickup, sets a blocked reason when filing a blocked effort, and releases rather than walking away holding a claim.
The proposed defaults were all adopted as written: the claim liveness window is 60 minutes (CLAIM_LIVENESS_WINDOW_MS), report retention is the most recent 50 per handoff (STATUS_REPORT_HISTORY_LIMIT), and reopenHandoff clears both the blocked reason and any claims so a reopened handoff starts orange.
The follow-on HandoffConversationIdentity workstream then delivered (2026-08-19) what this document left as a free-form agent name: every claim, heartbeat, release, and report now carries the identity of the conversation that made it.
Remaining work
One loose end, and it is an efficiency cleanup rather than a gap in the capability:
- The WUI still pays N+1 round trips for card enrichment. The home page fetches the latest report (and claims, and blocker colors) one handoff at a time on a bounded pool — see the
LATEST_REPORT_FETCH_PARALLELISMcomment inPages.ktand theHomePageDataSourceboundary inHomePage.kt, which was deliberately shaped so a second implementation could drop in. - The aggregated RPC that removes those calls already exists and is unused.
getHomePageProjectionJson— carryingHomePageLatestReportRecord(status, agent, reported-at, excerpt) and live claims per card — merged on 2026-08-26 in HandoffApi PR 12, HandoffEmbedded PR 12, and HandoffServiceServer PR 21. SwitchingServiceHomePageDataSourceonto it is the work that remains. - Two earlier PRs took the other route and are now stale: HandoffApi PR 7 and HandoffEmbedded PR 7 add the latest-report fields to
HandoffRecorditself. Both have sat open and un-rebased since 2026-08-01 (mergeStateStatus: DIRTY), and the home-page projection has largely overtaken them. Pick one route — adopt the projection in the WUI and close these two, or rebase and land them — rather than leaving both open.
Goal
The main list at handoff.wasmserver.com is the swarm's shared work queue, but today a card cannot distinguish the three situations that matter most to an operator scanning it: this needs me, an agent is on it, and this is waiting for someone to pick it up. Every handoff should render as exactly one of four colors:
| Color | Meaning | Maps to |
|---|---|---|
| 🔴 Red | Blocked on an unresolved user decision or action (user intervention is required), or forward progress is otherwise not being made (e.g. quota exhaustion). | The BLOCKED status in the status-report skill. |
| 🟡 Yellow | The handoff — or one of its dependencies — is actively being worked on. | RUNNING. |
| 🟠 Orange | Not inherently blocked, just unassigned: no one is working on it or any of its dependencies at the moment. | (New state — a status report always has an active agent, but a handoff's whole purpose is to sit between agents.) |
| 🟢 Green | Completed. | DONE. |
An operator's scan then becomes: red cards need my attention, yellow cards are progressing without me, orange cards are the backlog an idle agent should claim next.
Current state at planning time (verified 2026-08-01, superseded by the Outcome above)
- A
HandoffRecordhas no status enum. Open vs. completed iscompletedAtMs == nullvs. set; completed handoffs leave every priority list immediately and are visible only under/archive(restorable, never deleted). - Dependencies are fully modeled: directed handoff→handoff links with cycle rejection, durable in the canonical file (
dependencies:frontmatter). - Today's "Blocked" badge is computed purely from incomplete dependencies (
blockingDependencyUrls). There is no manual blocked flag and no blocker-reason text — no way to express "waiting on a user merge approval" or "paused on quota". - There is no assignment/claiming concept. The nearest signal is a conversation attachment (
handoff-cli conversation attach <id> --thread <url>) linking a handoff to an AiCliHostSupervisor thread; the service computes a roll-up —ACTIVE(thread message within the last 30 minutes),IDLE, orNONE. Gaps: only supervisor-hosted agents have a thread URL to attach; nothing attaches automatically (most handoffs sit atNONEeven while being worked); and the agent's identity is only implied by the thread URL. - The durable file format allows exactly seven frontmatter keys (
id, url, title, summary, created, completed, dependencies); unknown keys are hard-rejected, and the canonical file round-trips through the git mirror into this repository'shandoffs/directory.
Design
The two new signals
1. A declared blocked-reason (durable) — the red signal. BLOCKED is inherently a judgment call ("the path to DONE depends on the user"), so it cannot be computed; it must be declared. Add a blocked-reason free-text field to the handoff record:
- Set and cleared via
handoff-cli update <id> --blocked-reason "<what user action is needed>"/--clear-blocked-reason. - Agents set it exactly when their status report goes BLOCKED (and the create-handoff skill sets it when a handoff is filed for a blocked effort); they clear it when the blocker is resolved and work resumes. Completing a handoff clears it implicitly.
- Durable in the canonical file (a new allowed frontmatter key), so it survives service restarts and reads meaningfully in the git mirror. This touches the four serialization sites:
HandoffRecord(+allowedKeys/toFileContent/parseFileContent), the embedded store's persistence, the RPC surface, and the git-mirror round-trip.
2. Claims with heartbeat (ephemeral) — the yellow signal. Generalize the conversation attachment into a first-class claim so any agent — supervisor-hosted or a plain terminal session — can register "I am working on this", and the WUI can show which agent holds each handoff:
handoff-cli claim <id> --agent <name>·handoff-cli heartbeat <id>·handoff-cli release <id>.- A claim is live while its heartbeat is within the activity window (proposed: 60 minutes — today's 30-minute conversation window flaps to Idle mid-work). Supervisor-thread conversations keep counting automatically via their existing message-activity roll-up; a live conversation is equivalent to a live claim.
- Claims are ephemeral service state (like conversations and the computed
blockedfield today): not written to the canonical file, not mirrored to git. A crashed agent's claim simply expires — no stale yellow. - Completing or releasing a handoff drops the claim. Claiming a red handoff does not clear its blocked-reason — the blocker must be explicitly cleared once actually resolved.
Color evaluation (precedence)
Per handoff, first match wins:
- Green —
completedAtMsis set. - Red (own) — its own
blocked-reasonis set. A declaration of "I need the user" beats a still-warm heartbeat; the declaration is the more deliberate signal. - Yellow — a live claim (or live conversation) exists on the handoff or anywhere in its dependency closure (transitive). An actively-worked dependent of a red dependency is therefore yellow — forward progress is being made on it, and the red card for the underlying blocker is still visible on the dependency itself.
- Red (inherited) — some dependency in the closure is red (its own blocked-reason, recursively). Nobody can or will move this until the user acts, so it surfaces red rather than orange.
- Orange — everything else: open, unblocked, unclaimed.
The service exposes this as a computed statusColor field (RED | YELLOW | ORANGE | GREEN) on HandoffRecord, alongside a statusDetail (the blocked-reason text, the claimant agent name(s), or the red/claimed dependency it inherits from) — computed like blocked today, excluded from canonical file serialization.
Consequence: incomplete dependencies alone no longer mean "Blocked". A handoff whose dependency is being worked is yellow; one whose dependencies sit untouched is orange. Red is reserved for declared (or inherited) user-intervention/stall blocks. The existing blocking-dependency links remain visible on the card as secondary detail.
WUI changes
- Each card on the main list gets its color as the left-border accent + a status badge: red shows the blocked-reason text (so the list reads as "what decisions am I being asked for"), yellow shows the claimant agent (or live conversation), orange is the unclaimed default, with the existing dependency links as secondary badges.
- The main list continues to show only open handoffs — completion still removes a card immediately, so the list shows only red/orange/yellow. Green renders wherever completed handoffs appear:
/archivecards, read-only detail pages, and dependency rows on detail pages (where a green dependency usefully signals it no longer holds anything up). - Dependency rows and the CLI
listSTATUS column adopt the same four-color vocabulary. - The detail page shows the latest full status report (rendered markdown, with its timestamp, reporting agent, and a history of recent reports), so opening a yellow or red handoff answers "what exactly is happening / what exactly is needed" without hunting down the agent's session. The card on the main list shows the latest report's freshness (e.g. "reported 12 min ago").
Ecosystem integration
The signals are only as good as the agents feeding them, so the agent-side skills are part of this workstream. Skill updates land in claude-code-skills alongside the CLI changes:
- status-report skill — required change: as part of producing each status report (every iteration of the recurring 20-minute loop, not just the final one), the skill uploads the full report text — not just a bit indicating it is running — to the handoff it is working through, via a new
handoff-cli report <id> --status <RUNNING|BLOCKED|DONE> --file <report.md>. The service stores the complete report (with timestamp and reporting agent), and the upload doubles as the status sync, so the WUI color always reflects the latest report: RUNNING → counts as the claim heartbeat (claiming first if not already held), keeping the handoff yellow while reports keep flowing; BLOCKED → also sets theblocked-reason(mirroring the report's Blockers line), turning it red; DONE → completes the handoff per the skill's existing terminal rules, turning it green. The report upload thereby is the heartbeat — a stopped loop lets the claim expire and the handoff correctly falls back to orange. - create-handoff skill: filing a handoff for a blocked effort sets its blocked-reason at creation; picking a handoff up claims it; resuming after the blocker is resolved clears the blocked-reason.
Resolved design decisions
- Red is declared, not derived — an explicit durable blocked-reason set by the blocked agent, plus transitive inheritance from red dependencies (when not actively claimed).
- Yellow comes from claims with heartbeats, generalizing conversation attachment so non-supervisor agents count, with claimant identity shown in the WUI.
- Precedence — completed > own blocked-reason > live claim in the dependency closure > inherited red > orange.
- Green stays out of the main list — completion removes the card immediately; green appears in the archive, detail pages, and dependency rows only.
Proposed defaults (correct as needed)
- Claim/conversation liveness window: 60 minutes.
- Exact shades are left to the WUI's existing dark-theme palette; orange must remain visually distinct from red on a card scan.
reopenrestores a handoff with whatever blocked-reason it had when completed cleared — i.e. reopened handoffs start orange until re-claimed or re-blocked.- Status reports are stored as service state (like conversations and claims — not written to the canonical file or the git mirror, which would otherwise gain a sync commit every 20 minutes per active handoff), retaining the most recent ~50 reports per handoff.