一键导入
display-name
Multi-session coordination via intent/whiteboard/<node>/: per-node boards + single-writer inboxes, claim ST scopes, broadcast, heartbeat, release
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Multi-session coordination via intent/whiteboard/<node>/: per-node boards + single-writer inboxes, claim ST scopes, broadcast, heartbeat, release
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| description | Multi-session coordination via intent/whiteboard/<node>/: per-node boards + single-writer inboxes, claim ST scopes, broadcast, heartbeat, release |
| chains_to | [] |
Coordinator for multiple Claude Code sessions -- and the human -- running concurrently against one Intent project. Each participant is a node with its own directory under intent/whiteboard/. Every file has exactly one writer; that single-writer rule is what makes the board contention-free and cleansable. The whiteboard is the live channel; intent/wip.md is the post-session snapshot.
Protocol 3.0 supersedes 2.0 (flat shared asks.md + per-stream files). v3.0 = per-node directories + a single-writer inbox model + the human as a first-class hv (hypervisor) node.
A node is a participant. The 2-letter moniker is the directory name, the routing key, and the handle. Nodes are per-project configuration: the project declares its roster (monikers, display names, roles) in its hand-authored intent/whiteboard/README.md. No roster is baked into this skill -- it discovers nodes by listing the immediate subdirectories of intent/whiteboard/.
A project that wants the human in the loop gives them a node, conventionally hv (the hypervisor): the human who adjudicates scope, sequences work, owns releases, and is where escalations land. The human is addressed as hv in all protocol language, never by name. The hypervisor node is human-driven -- it is read like any other node, but the human maintains it (or has it maintained on their behalf) rather than running pickup on a heartbeat.
hv is structurally a node like any other -- a <hv>/wip.md peers read at pickup, inboxes peers append to -- with three differences that follow from being human-driven:
hv is not driven by /in-session / pickup, so its session_id is optional and conventionally none. Peers therefore never match it on the "different session_id" active-peer test; they read it for its directives and route escalations to hv/inbox.<you>.md.hv heartbeat does not mark anything reclaimable -- the human is always authoritative -- so the 7-day reclaim rule does not apply to hv.wip.md body, hv may carry a ## Standing directives section: durable instructions every node honours (sequencing, scope rulings, release policy). Peers read it at pickup the way they read ## Decisions.pickup -- chained from /in-session; read own board + own inboxes + peer state, touch heartbeat.ask <node> <text> -- send a point-to-point message to another node.announce <text> -- broadcast one line to every peer (eg before touching a shared platform layer).decide <text> -- record a cross-node decision on your own board.claim <STxxxx> / unclaim <STxxxx> -- add/remove an ST from your board's claims.clear <sender> -- archive handled entries out of one of your own inboxes.archive -- roll your own DONE board content + handled inbox entries into your own history.touch -- refresh your heartbeat.release -- chained from /in-finish; set your status paused.status -- read-only one-line-per-node summary.If invoked with no subcommand, default to status.
intent/whiteboard/
README.md # protocol reference + the project's node roster
<node>/
wip.md # the node's live board: frontmatter + DOING + TODO + watch-outs + decisions
inbox.<sender>.md # one per OTHER node: messages FROM that sender (single-writer)
.history/
.gitkeep # tracks the otherwise-empty archive dir (git ignores empty dirs)
YYYYMMDD/ # the node's archived DONE work + handled inbox entries
Scaffolding a node is the deterministic job of intent claude ws new <node> (the provisioner -- ST0047), not a hand ritual: it creates <node>/, <node>/.history/.gitkeep (git does not track an empty directory), the node's wip.md, and an _(empty)_ inbox in both directions with every existing peer (<node>/inbox.<peer>.md + <peer>/inbox.<node>.md). The intent claude ws family (new / list / archive / hygiene) plus intent claude start <node> (launch a session bound to a node) own this mechanical lifecycle; this skill owns the judgement ops below. Both honour the one on-disk format described here.
Single-writer rule:
<node>/wip.md -- written only by <node>.<node>/inbox.<sender>.md -- appended only by <sender>; read + cleansed only by <node> (the owner).---
node: <moniker>
name: <display name>
role: <role> # eg hypervisor | control | interface | validation | author
session_id: <UUID|none>
heartbeat_at: <ISO 8601>
status: active | paused
focus: "<one-line current goal>"
claims: [STxxxx, ...]
---
# <Name> (<node>)
## DOING -- in-flight work (archived into .history/ when done)
## TODO -- queued / next
## Watch-outs -- durable cautions peers should know (standing; not archived)
## Decisions -- cross-node decisions, broadcast by being read at pickup
Only the frontmatter is required for protocol compliance; the body sections are the working content.
One inbox per ordered (sender -> recipient) pair: <recipient>/inbox.<sender>.md holds the messages <sender> has sent <recipient>. The sender is the sole writer (append-only); the recipient is the sole reader and owns its lifecycle (read, action, clear into history).
Inboxes are pre-seeded in both directions when a node is scaffolded (ws new writes the header + _(empty)_ sentinel for every existing peer pair). ask / announce also create an absent <recipient>/inbox.<you>.md on demand before appending -- so a hand-added node, or a board predating the provisioner, self-heals. Either way, a fresh inbox is its header line plus the empty sentinel:
# inbox: <sender> -> <recipient>
_(empty)_
The # inbox: <sender> -> <recipient> header restates the single-writer routing the path already encodes, so the file is self-describing when read alone. _(empty)_ is the "no live entries" sentinel: clear and archive leave the header + _(empty)_ behind when they remove the last handled entry, so an inbox is never an ambiguous zero-byte file.
Each entry appended by ask / announce:
## (YYYY-MM-DD HH:MM) [Re: <prior-anchor>] [FYI only -- no response needed.]
<text>
Required fields: the ## (YYYY-MM-DD HH:MM) timestamp heading (minute granularity -- it doubles as the anchor a reply threads against) and the <text> body. Recommended / optional: Re: <prior-anchor> (present only when threading a reply to a prior entry's timestamp) and FYI only -- no response needed. (present only when no reply is expected; absent means the sender expects a reply). A reply is a new entry in the opposite-direction inbox (<original-sender>/inbox.<you>.md), carrying Re: the entry it answers.
On pickup, determine which node this session is:
/in-whiteboard pickup vc), use it.wip.md carries this session's session_id.The moniker is durable; subsequent sessions of that node inherit it via the existing <node>/ directory.
intent/whiteboard/*/ to enumerate nodes. Determine your node (see discovery).<you>/wip.md (resume state) and all four <you>/inbox.*.md (incoming). Surface any non-empty inbox entries to the user.<peer>/wip.md frontmatter. For each peer with status: active AND heartbeat_at within 7 days AND a different session_id: surface "node X active (heartbeat , focus: )". Active but older than 7 days: "node X appears stale".<you>/wip.md frontmatter: session_id (this session, or unknown), heartbeat_at (now), status: active. Keep claims + body intact.Your inbox.<you>.md in <node>/ usually already exists (ws new pre-seeds it); if it is absent (a hand-added node), create it with its # inbox: <you> -> <node> header + _(empty)_ sentinel (see inbox shape). Append a message entry (see Message-entry format) -- the path encodes sender -> recipient, so the 2.0 to:/from: line is implicit:
## (YYYY-MM-DD HH:MM) [Re: <prior-anchor>] [FYI only -- no response needed.]
<text>
If the inbox already carries only _(empty)_, replace that sentinel with the first entry.
Touch your heartbeat.
A reply goes to <sender>/inbox.<you>.md (the inbox flips direction).
<peer>/inbox.<you>.md (all nodes except yourself).Use for 1-to-all signals -- eg "touching apps/lamplight/** for ST-X" (a shared platform-layer edit; the retired lamplight.md job), or a protocol/decision broadcast.
- (YYYY-MM-DD) <text> to your <you>/wip.md ## Decisions section (peers read it at pickup).STxxxx in your wip.md claims.wip.md claims: if an active peer already claims it, stop and surface the overlap for the hypervisor to arbitrate.<you>/inbox.<sender>.md, move the handled entries verbatim into <you>/.history/<YYYYMMDD>/inbox.<sender>.md, and remove them from the live inbox (leaving the header + _(empty)_ if none remain).Roll your OWN node's DONE content out of the live files into your own history, daily-or-more, so the live files stay lean (they are read on every pickup).
<you>/.history/<YYYYMMDD>/ exists (today, or the content's own date).<you>/wip.md: move DONE ## DOING items + superseded blocks into <you>/.history/<YYYYMMDD>/wip.md. KEEP frontmatter, live DOING/TODO, ## Watch-outs, and still-relevant ## Decisions.<you>/inbox.<sender>.md: move handled entries into history (same as clear).prettier --write the touched files if the project formats markdown.<you>/ directory, so there is no peer-collision hazard -- this is the key simplification over 2.0's shared-file archive. Commit via explicit pathspec (git commit --only <you>/...), never -A.wip.md heartbeat_at to now. No other change.wip.md status: paused; update heartbeat_at. Leave claims + body intact.The human may say "localfold" or "globalfold" (terms from Lamplight; defined in /in-finish). In whiteboard terms: localfold = tidy your OWN node before a compact -- migrate settled ## Decisions into wip.md, archive your own DONE content, then release. globalfold = the project-wide snapshot (intent/wip.md / restart.md / done.md), typically the coordinating / validation node's job, not a per-node op. Either way you only ever fold your own <you>/ directory.
<node>/wip.md frontmatter.<node>: <status>, focus=<focus>, claims=[...], heartbeat=<relative>. No writes.Roles are per-project. A common shape: one control node doing the heavy lifting, one validation node (the independent check, below), whatever else the project needs (interface, author, ...), and the hypervisor (hv) for the human. The project's README.md is the source of truth for the roster.
A validation node is the independent check that the other nodes' landed or claimed work is correct, complete, consistent, and faithful to what the hypervisor asked. If the project keeps a documentation function, documentation becomes the byproduct of verification -- you cannot faithfully document a system that does not do what it claims. Advisory authority only: it posts findings, the hypervisor adjudicates, the owning node fixes. It never mutates another node's code and never blocks its progress.
Sources -- the triangle:
~/.claude/projects/<project-dir>/<session_id>.jsonl (session_id is in that node's wip.md frontmatter; re-resolve each audit, it rotates on /compact or restart). Read it targeted (tail / grep / sub-agent sweep), never whole.~/.claude/plans/<name>.md (often cited in focus:).intent/st/** + code + tests.Method:
file:line from a real read; no invented line numbers; no "certainly" without having read the code.<owner>/inbox.<you>.md); a compounding risk (a false "done" the next unit would build on) escalates to hv/inbox.<you>.md. Never mutate another node's code.wip.md = the node; inbox.<sender>.md = the sender. The recipient owns its inbox lifecycle (reads, actions, clears into its own history).intent/wip.md is the post-session snapshot; <node>/wip.md is the live board.wip.md frontmatter), never glob paths.announce -> peers' inboxes. No shared file; a shared platform layer (eg apps/lamplight/**) is coordinated by announcing before you touch it./compact does NOT end a session -- status stays active; the next pickup touches the heartbeat..history/YYYYMMDD/ is append-only and never reloaded on pickup.hv in all protocol language, never by name.Concurrent sessions need a live coordination surface, and wip.md (the post-session snapshot) loses fidelity during a session. Protocol 2.0 supplied that with shared files (asks.md, per-stream files, lamplight.md), but those had N writers each: contention on every edit, cleanse that required cross-stream coordination, and unbounded growth that chewed context. 3.0 fixes all three by giving every file exactly one writer -- a per-node board you alone write, and per-sender inboxes each written by one peer and cleansed by you. Coordination stays live; contention and cleanse-pain go away.
| Rationalisation | Reality |
|---|---|
"One inbox.md per node is simpler than per-sender." | One file, N writers -- back to 2.0 contention. Per-sender is what makes it single-writer. |
"I'll edit a peer's wip.md to correct it." | Never. You write only your own node. Send an ask to its inbox. |
| "I'll keep a shared file for platform edits." | That is the retired lamplight.md. Use announce -- broadcast to inboxes, no shared file. |
"/compact ended the session, so I'll set status: paused." | No. /compact is transparent. Status stays active; /in-session re-fires pickup. |
| "I'll archive the whole board while I'm here." | You archive only your own <you>/ dir. Single-owner, collision-free -- that is the point. |
| "The node said it's done, so it's done." | A "done" claim is the trigger to verify, not the verdict. Read the as-built against the ask. |