Skill Author — Integrating the CAFleet-Orchestrated Pattern
You are about to write a new skill that drives a Director and one or more spawned members through the CAFleet message broker. This guide walks you through every sub-system you need to wire up, explains why each step exists, and finishes with a worked example you can read end-to-end. It is a teaching document — not a paste-this template — because the canonical rules drift quickly and skills that copy a prefab spawn prompt rot the fastest.
Read this whole document before you start writing your SKILL.md. The rules are short individually but the failure modes when any of them is missed are loud, and most of them have already bitten earlier authors.
1. What "CAFleet-orchestrated" means
A CAFleet-orchestrated skill is one where the Director (the main Claude session running your slash command) bootstraps a fresh CAFleet fleet, spawns one or more members as separate claude (or codex) processes inside dedicated tmux panes, and coordinates the work through the CAFleet message broker (cafleet message send / cafleet message poll / cafleet message ack). Members are real, isolated coding-agent processes — not in-process subagents — and the broker delivers each message as a 2-line keystroke preview into the recipient's tmux pane.
The shape always looks like this:
User
+-- Director (main Claude — runs cafleet fleet create / member create; coordinates via the broker)
+-- member-1 (claude pane)
+-- member-2 (claude pane)
+-- ...
Use this pattern when:
- The work needs parallelism that the harness cannot provide on its own — multiple members drafting in parallel, multiple researchers investigating sub-topics, multiple verifiers running on different slide ranges.
- The work needs role specialization — Director / Drafter / Reviewer / Programmer / Tester are roles that justify separate processes with separate role files.
- The work needs persistent inter-member memory — the broker stores every message in SQLite and the audit-file path under
${BASE}/prompts/is a permanent record.
Do not use this pattern when:
- The work fits inside one
Agenttool subagent run. A subagent is cheaper and faster than a full CAFleet team. - The work is a single-shot transformation (file edit, search, summarize). A normal slash command running inside the Director's session is enough.
- You only need the harness
TaskListfor tracking — you do not need cross-pane coordination.
If you are unsure: write the simpler version first. The CAFleet team pattern is overkill for most tasks and the orchestration overhead (cafleet doctor, cafleet fleet create, cafleet member create, cafleet monitor, member delete, fleet delete) costs the user real seconds and real cognitive load.
2. The five-part integration checklist
Every CAFleet-orchestrated skill must wire up these five sub-systems, in this order, in its SKILL.md. The first three are setup; the fourth is the per-member spawn pattern; the fifth is teardown. Skip any one of them and the skill will fail silently in production.
2.1 Resolve the task-scoped BASE
Before any other work, the Director resolves the task-scoped output directory by following the cafleet skill's reference/base-dir.md task-scope resolution procedure with a TASK_NAME derived from the skill's per-task convention. The procedure uses only git rev-parse --show-toplevel (via Bash) and writes nothing at resolution time — there is no cafleet CLI subcommand for it.
<task-relpath> is a path under the inferred repo root that describes the per-task folder. The two recognized buckets are:
researches/<topic-slug>— for research-style skills (one folder per research run).design-docs/<NNNNNNN>-<slug>— for design-doc-style skills (one folder per design document, with a 7-digit zero-padded number prefix per.claude/rules/design-doc-numbering.md).
The procedure:
- Walks up from CWD (
git rev-parse --show-toplevel) to infer the repo root. - Joins
<task-relpath>against the repo root and resolves it to the absolute task folder. - Yields
base = <abs task-folder>; the folder is created lazily on the first consumer write.
Use the resolved base as ${BASE} for the rest of the run. Every scratch / audit / figure / spawn-prompt-render write the skill performs MUST live under ${BASE} — never /tmp, never the repo root.
The procedure's positional branch also accepts an absolute path. If the path lies strictly under the inferred repo root, it is used verbatim as the task folder — the resolver does NOT walk ancestors or match skill-specific bucket patterns. If the path lies outside the repo root (or equals the repo root), the resolver yields the literal sentinel <unset> for ${BASE}. Consumer-strips contract: because the resolver does not fold child paths, each consuming skill MUST canonicalize its argument to the actual task-folder path (relative or absolute) BEFORE resolving. For a relative argument: strip trailing filenames like /design-doc.md, strip leading bucket prefixes like design-docs/, then prepend its own bucket. For an absolute argument: apply only the trailing-filename strip — it is used verbatim as the task folder when it lies strictly under the repo root (no bucket prepend), otherwise it yields <unset>. When ${BASE} is <unset>, the skill MUST guard every BASE-derived write with an explicit ${BASE} != <unset> check, omit the BASE: line from any spawn prompt entirely, and never fall back to /tmp. The standardized loud-error message is Error: BASE is <unset>; refusing to fall back to /tmp.
When CWD has no .git ancestor (typical when CWD is $HOME or under a coding agent's user-level config directory, per base-dir.md's table) AND a TASK_NAME is supplied, the resolution fails with cannot resolve task-scope base-dir: no .git ancestor found from CWD <cwd>. cd to the repo root and retry. — surface this error to the user and stop.
2.2 Bootstrap a CAFleet fleet (monitor included)
The Director first writes the monitor member's spawn prompt to ${BASE}/prompts/monitor-<UTC-compact>.md (the standard pre-spawn audit convention, § 2.4), then creates the fleet inside a tmux pane:
cafleet fleet create --name "<fleet-name>" --coding-agent <backend> \
--monitor-file ${BASE}/prompts/monitor-<UTC-compact>.md --monitor-model {monitor_model} --json
The CLI atomically (1) creates a fleets row, (2) registers a root Director bound to the current tmux pane, and (3) registers the fleet's monitor member and spawns its coding-agent pane. Any failure rolls everything back — no fleet row, no Director row, no monitor row, no pane — and the command is retryable as-is. Capture fleet_id, director.member_id, and monitor.member_id from the JSON response and substitute them as literal id strings into every subsequent cafleet ... call.
Never store these IDs in shell variables (export FLEET=...). The Claude Code harness's permissions.allow matches Bash invocations as literal command strings; an exported shell variable you reference yourself breaks the literal match and forces per-invocation permission prompts that interrupt the agent loop.
If the user is not inside a tmux session, cafleet fleet create exits 1 with Error: cafleet fleet create must be run inside a tmux session and writes nothing. Surface this and stop — do NOT try to start a tmux session yourself.
2.3 Wait for the monitor gate
CAFleet members do not auto-poll. The broker delivers a 2-line inline preview into the recipient's pane via tmux.send_inline_preview keystroke; that preview is the trigger that wakes the recipient. If the keystroke is missed (pane buffered, recipient mid-Bash, etc.), the message just sits in INPUT_REQUIRED until the recipient runs cafleet message poll themselves.
Every CAFleet-orchestrated skill runs the monitor-member heartbeat — it is a session-level requirement, not a per-skill choice. The cafleet fleet create bootstrap (§ 2.2) registers and spawns the monitor member; the monitor inherits the Director's backend by construction. At startup the monitor member sends ready, launches cafleet monitor <fleet-id> as a backend-resolved long-lived execution in its own pane, confirms the loop's startup line — monitor loop started (fleet <fleet_id>, tick <tick>s, pid <pid>) — and sends the gate signal monitor live to the Director. That message gates the first ordinary member create (the CLI's monitor-first guard backstops a Director that skips the wait). cafleet member create --role monitor is the mid-run recovery path for re-spawning a dead monitor — never the bootstrap path. Once per wake interval (default 600 s; --interval / CAFLEET_MONITOR_WAKE_INTERVAL, 0 disables the wake) the loop keystrokes one Esc-safeguarded fleet-level wake into the monitor member's own pane; on each wake the monitor scans the fleet's panes, classifies them, pings a confirmed-quiet member once per quiet period, and messages the Director only when something needs attention. The loop never keystrokes an ordinary member's pane or the Director's. The heartbeat protocol is backend-neutral, while each backend's overlay defines how the monitor member hosts and observes its long-lived execution.
2.4 Spawn members with cafleet member create --file <abs path>
For each member you spawn, follow the two-step render-to-file pattern:
Render the spawn prompt locally. Substitute every
[INSERT …]marker with the concrete value, and keep the four{fleet_id}/{member_id}/{director_member_id}/{coding_agent}identity placeholders as written —cafleet member createrunsstr.formatover the prompt at spawn and renders them to literals (§ 3.2). Any literal brace in the prompt body must be doubled ({{/}}) to survive.format(); leave no other stray single braces. By the time you write the file, every[INSERT …]marker must be replaced with its concrete literal value.Write the rendered text to
${BASE}/prompts/<role>-<UTC-compact>.mdwhere<UTC-compact>isdatetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")(Python). Create${BASE}/prompts/on first write via(Path(BASE) / "prompts").mkdir(parents=True, exist_ok=True). On same-second collisions, append_2,_3, … to the filename until it is unique — never overwrite. The pre-spawn file IS the audit artifact. There is no second post-spawn re-render; the file is both the CLI input and the permanent record of what was spawned, carrying the four{...}identity placeholders pre-substitution (which is expected).Spawn with
--filepointing at the absolute path of the rendered file (the acting Director is auto-resolved from the fleet row — no identity flag):cafleet member create --fleet-id <fleet-id> \ --name "<member-name>" \ --description "<one-sentence purpose>" \ --file ${BASE}/prompts/<role>-<UTC-compact>.md \ --jsonCapture the printed
member_idfrom the JSON response and substitute it for the member's id in every subsequentcafleet ...call the Director makes that targets it. (The member itself learns its own id from the literalYOUR MEMBER ID:line the CLI rendered into its prompt — § 3.2.)
Use --file for every spawn — § 3.4 explains the command too long cliff; the inline positional PROMPT is the documented fallback only for the ${BASE} == <unset> case (§ 3.5, § 4).
Path-by-reference for role files
Do NOT inline a role definition (5–15 KB markdown file describing a member's accountability, communication protocol, role-specific workflow, escalation rules) into the spawn prompt. Instead, reference the role file by absolute path:
ROLE DEFINITION: Open <abs path to roles/<role>.md> with the Read tool BEFORE any other action.
The spawned member opens its role file with Read on its first turn. The role file lives in your skill's roles/ directory and is stable, so this is safe. This pattern keeps the spawn prompt small (under the tmux limit), makes role updates take effect without a respawn, and concentrates role-specific accountability in a single canonical file rather than smearing it across the spawn prompt.
2.5 Tear down per the Shutdown Protocol
When the work is done, the Director MUST tear down in this exact order:
- Delete the monitor member first (first-out).
cafleet member delete <monitor-member-id>— the pane kill takes the loop process down with it, ending the wake source before any other member disappears. The killed loop leaves a stalemonitor_runtimerow that reads as dead on both liveness axes;fleet delete(step 3) removes it unconditionally. cafleet member delete <id>for every remaining member. This sends the backend exit keystroke to the member's pane and waits up to 15 s for the pane to disappear. Surviving member coding-agent processes are NOT auto-closed bycafleet fleet delete— callmember deleteper member.cafleet fleet delete <fleet-id>. Soft-deletes the fleet (setsdeleted_at), deregisters every active member in the fleet (root Director + remaining members), and physically deletes every associatedmember_placementsrow. Messages are preserved.fleet deletemakes any still-running loop self-terminate on its next tick, so step 1 is belt-and-suspenders.
Order matters. Delete the monitor member before the others so a wake cannot land mid-teardown. If you call fleet delete before member delete, the member panes orphan (the claude process keeps running but has no broker to talk to).
3. Spawn-prompt anatomy
Every member spawn prompt follows the same skeleton. Read this section as the canonical anatomy — it explains every section, why it is there, and the substitution rules.
You are <role> in a <skill> team (CAFleet-native).
ROLE DEFINITION: Open [INSERT abs path to roles/<role>.md] with the Read tool BEFORE any other action. That file is your authoritative role definition. Re-read it whenever you are unsure of protocol.
Load these skills at startup:
- the cafleet skill — for the broker primitives and bash-via-Director routing
- <other skills as needed>
FLEET ID: {fleet_id}
DIRECTOR MEMBER ID: {director_member_id}
YOUR MEMBER ID: {member_id}
BASE: [INSERT abs BASE path the Director resolved via the `cafleet` skill's `reference/base-dir.md`]
CODING AGENT: {coding_agent}
<role-specific assignment text — e.g. CURRENT DATE, USER REQUEST, OUTPUT PATH, YOUR TASK ID>
<role-specific instructions — every IMPORTANT: line and poll-handling line>
On spawn, as your first Bash call, send the ready signal: cafleet message send --from-member-id {member_id} --to-member-id {director_member_id} "ready"
<start cue>
There is no COMMUNICATION PROTOCOL command-example block: the member learns the poll/send/ack command shapes from the cafleet skill and its role file, and takes its ids from the literal identity lines above.
3.1 The identity block
FLEET ID: {fleet_id}
DIRECTOR MEMBER ID: {director_member_id}
YOUR MEMBER ID: {member_id}
BASE: <abs task-folder path>
CODING AGENT: {coding_agent}
These lines are the member's grounding identity. The brace tokens are the CLI's str.format placeholders — cafleet member create renders each to a literal at spawn (§ 3.2), so the member reads e.g. FLEET ID: 7 / YOUR MEMBER ID: 11 and substitutes those integers into its cafleet ... commands. The BASE: line is the resolved task-scoped BASE the Director computed in § 2.1 — the member uses this verbatim and MUST NOT re-resolve BASE on its own (members that re-resolve risk drift from the Director's resolved BASE). The CODING AGENT: line names the member's backend so it can read its overlay (§ 8).
3.2 Identity via str.format substitution
cafleet member create runs str.format over the resolved spawn prompt, substituting exactly four placeholders to literals at spawn time:
{fleet_id}— the spawned member's fleet id.{member_id}— the spawned member's own id, allocated by cafleet during the spawn (so the Director cannot know it at render time — this is exactly why the spawn prompt carries the placeholder rather than a Director-rendered literal; only the CLI can fill it).{director_member_id}— the spawned member's Director id.{coding_agent}— the resolved backend name (claude/codex/opencode).
An unknown placeholder raises a UsageError listing the four supported names; a malformed brace expression raises the "double literal braces" UsageError (both exit 2, with the just-registered member rolled back). Any literal brace in prompt text must be doubled ({{ / }}) to survive .format().
This substitution is the sole identity-delivery mechanism — no identity environment variable is injected into the pane (the only forwarded env var is CAFLEET_DATABASE_URL). The member takes the literal integers from its prompt's identity lines and passes them explicitly:
- a poll is
cafleet message poll <my-member-id>; - a self-attributed send is
cafleet message send --from-member-id <my-member-id> --to-member-id <director-member-id> "...".
A Director may also embed the literal fleet_id and director_member_id (which it knows at render time) directly instead of the placeholders; the member's own id always comes from the CLI-rendered {member_id}. The cafleet skill documents the convention for spawned members.
3.3 The [INSERT ...] render-time substitution rules
[INSERT …] markers are placeholders for you (the skill author writing the SKILL.md) to direct the Director on what to substitute when rendering. The Director performs every [INSERT …] substitution in step 1 of § 2.4 (local rendering) before writing the file. By the time the file lands at ${BASE}/prompts/<role>-<UTC-compact>.md, no [INSERT …] marker should remain. The four {...} identity placeholders are NOT [INSERT …] markers — they are the CLI's str.format tokens, rendered at spawn time (§ 3.2), so leave them as written and leave no other stray single braces around them.
A common mistake is to leave a literal [INSERT abs path to roles/<role>.md] in the rendered file because the Director forgot to compute the absolute path. The member then opens a file that does not exist on its first turn. Spot-check the rendered files in ${BASE}/prompts/ before continuing.
3.4 The command too long cliff and --file
tmux split-window accepts the spawn prompt as a single positional argument. Linux's ARG_MAX (the execve() argument-list size limit) and tmux's own command parser combine to fail with command too long once the shell-quoted prompt grows past a few KB. Even with role content not inlined, a prompt with multiple [INSERT …] substitutions + the identity block + role-specific assignment text often exceeds the limit.
--file <abs path> sidesteps this. cafleet reads the file inside its own process and writes the text to the new pane through a separate path that is not size-limited. Use --file for every spawn. The inline positional PROMPT is the documented fallback only for the ${BASE} == <unset> case where the file write is impossible.
3.5 The ${BASE} == <unset> skip semantics
When the base-dir resolution yields the unset outcome (absolute-path argument outside the repo root, or equal to the repo root itself), ${BASE} is the literal sentinel string <unset>. The skill MUST:
- Skip the audit-file write. Do not try to write
<unset>/prompts/<role>-<UTC-compact>.md— that is a literal path with a<in it, which most filesystems reject. The guard isif BASE != "<unset>". - Omit the
BASE:line from the spawn prompt. The spawn prompt does NOT include the literal stringBASE: <unset>— the line is dropped entirely. The member's existence-check naturally treats audit-file features as disabled. - Fall back to the inline positional
PROMPTfor thecafleet member createcall (the file-write path is gone, so the only way to get the prompt to cafleet is inline). Be aware that this risks thecommand too longfailure mode for prompts above the tmux limit; surface that as a hard error to the user, not as a silent retry. - Loud-error on unguarded BASE-derivation. If a code path under
${BASE} == <unset>reaches an unguardedPath(BASE) / …computation, abort with the standardized error:Error: BASE is <unset>; refusing to fall back to /tmp.
The member, after spawn, emits a single CAFleet message back to the Director as a parens-free anchorless status:
audit-disabled no BASE in spawn prompt
The phrasing deliberately omits parentheses so the Director reading the broker log does not misinterpret it as a malformed <verb> (<pointer>) hop.
3.6 The ready-signal line
The ready-signal line is part of the skeleton's fixed frame, not a role-specific slot: every spawn prompt carries it as written in the anatomy skeleton above (§ 3), between the role-specific instructions and the start cue. Do not vary its wording per role and do not fold it into a start cue.
Its {member_id} / {director_member_id} tokens are two of the CLI's four str.format identity placeholders (§ 3.2), so cafleet member create renders the line into a copy-pastable command with literal integers at spawn — the member executes it verbatim as its first Bash call.
The resulting ready message is the only evidence that the coding agent inside the pane actually booted (pane placement proves only that the pane exists), and the Director dispatches the member's first task on that signal. A spawn prompt missing the line is a spawn defect — fix the prompt and re-spawn.
4. Audit-file write protocol
Every spawn-prompt render is also the spawn-prompt audit artifact. The protocol:
Path:
${BASE}/prompts/<role>-<UTC-compact>.mdwhere<UTC-compact>isdatetime.now(UTC).strftime("%Y%m%dT%H%M%SZ").<role>matches the lowercased value of the--nameflag passed tocafleet member create(e.g.,manager,scout-1,researcher-04,programmer).Mkdir on first write:
(Path(BASE) / "prompts").mkdir(parents=True, exist_ok=True). Usepathlib, notsubprocess.run(["mkdir", ...]). This is enforced by the no-bypass write protocol.Same-second collisions: When two spawns happen in the same UTC second (rare but possible — a Director spawning multiple Researchers in a single tick), append
_2,_3, … to the filename until it is unique. Never overwrite. The audit record must be permanent.The pre-spawn file IS the audit artifact. There is no second post-spawn re-render. After
cafleet member createsucceeds, the file at${BASE}/prompts/<role>-<UTC-compact>.mdis the permanent record of what was spawned and is never touched again. It carries the four{...}identity placeholders pre-substitution — that is expected; the CLI renders them at spawn.Every write under
${BASE}. Audit files, scratch notes, figure artifacts, intermediate working files — every output the skill produces lands under${BASE}or a consumer-supplied absolute path. Never/tmpunless${BASE}itself is/tmp/cafleet(which is a legitimate base-dir choice when the user picked it via the{decision_surface}prompt).${BASE} == <unset>is a hard stop, not a fallback. See § 3.5.
This protocol is the most easily violated rule in CAFleet-orchestrated skills. Authors routinely forget the same-second collision rule and overwrite the previous member's audit file when spawning two members in a tick. Authors routinely forget the <unset> sentinel and crash with a Path-with-<-in-it error. Read the rules above before writing any file write code.
5. Coordination protocol summary
Inter-member communication uses the verb + pointer schema. This is the longest section in this guide because it is the most easily miswired surface.
5.1 The cafleet message body shape
Every cafleet message body looks like:
<verb> (<pointer>) [— <optional summary up to 80 codepoints, ≤ 3-item enumeration>]
The body MUST be short. Substantive content (rationale, evidence, file lists, test names, error stacks) lives as inline COMMENT(role) markers in the document being edited at the same pointer the cafleet body references. The body is the routing hop; the marker is the substance.
5.2 The canonical 6-verb list
There are exactly six verbs:
| Verb | Meaning | Sender | Recipient |
|---|---|---|---|
ready |
The pointer is ready for the recipient to act on (next step assigned, or feedback awaiting addressing). | Director | Member |
complete |
The pointer is done from the sender's side; the recipient should review or proceed. | Member | Director |
addressed |
The recipient has applied the requested fix at the pointer; the sender should re-verify. | Member | Director |
blocked |
The sender cannot proceed at the pointer because something is missing / ambiguous; an inline COMMENT(role) marker carries the explanation. |
Member | Director |
escalating |
The sender suspects a defect outside their authority (test defect, spec ambiguity) and is handing off to the Director for arbitration. | Member | Director |
approved |
The Director gives final authorization for a milestone (typically approved (doc) for a finished design doc, or per-step approval). |
Director | Member |
Do NOT invent new verbs. Do NOT use English synonyms ("done", "ack", "ok", "fixed"). The 6-verb list is the entire protocol; broker log parsers look for these literal words.
5.3 The pointer forms
Three pointer shapes:
| Pointer | When |
|---|---|
paragraph-<HeadingPath> |
A specific section of the document — e.g. paragraph-Implementation > Step 5, paragraph-Specification > 3. Anchor schema. Use the literal heading text from the document with > separators. |
<file>:<line> |
A specific file location — e.g. cafleet/src/cafleet/broker/messaging.py:142, docs/docs/concepts/overview.md:178. |
doc |
The whole document; used for top-level milestones (complete (doc), approved (doc)) and document-wide blocks (blocked (doc) — test framework ambiguous). |
5.4 The pointer-marker pairing rule
When a sender includes substantive content in a COMMENT(role) marker, the marker MUST live at the same pointer the cafleet body references. Examples:
blocked (paragraph-Implementation > Step 5)+COMMENT(programmer): test X expects Y but design doc says Zatparagraph-Implementation > Step 5in the design doc.ready (cafleet/src/cafleet/broker/messaging.py:142)+COMMENT(director): use pathlib.Path.mkdir(parents=True, exist_ok=True), not subprocess.run(["mkdir", ...])at line 142 of the file.
The recipient reads the cafleet body, navigates to the pointer, reads the standing marker, applies the fix or arbitration, removes the marker, and replies with the next-step verb (addressed (...) for member-side fixes, ready (...) for director-side arbitration handoffs).
5.5 The role taxonomy
The marker role identifier in COMMENT(<role>) is one of: user-relay (user-derived clarifications baked into the doc), director, programmer, tester, reviewer, verifier, analyzer, drafter. New roles for new skills SHOULD be added to this list; do not abbreviate. The user-relay role is reserved for the cafleet-design-doc skill's interview-workflow user-derived clarifications and its execute-workflow test-framework arbitration; do not use it for arbitrary relayed content.
5.6 Anchorless status
A small handful of statuses do not pair with a pointer because the condition is global. These are emitted as parens-free anchorless strings:
audit-disabled no BASE in spawn prompt— emitted by a member whose spawn prompt lacks theBASE:line entirely (the${BASE} == <unset>branch).- (Other anchorless statuses are documented per-skill; do not invent new ones casually.)
The phrasing deliberately omits parentheses so a parser does not misinterpret it as a malformed <verb> (<pointer>) hop.
5.7 Acking messages
After acting on a polled message, the recipient MUST cafleet message ack it. Un-acked messages stay in INPUT_REQUIRED and re-surface on every subsequent message poll cycle, polluting the recipient's context with stale work.
cafleet message ack <message-id>
The <message-id> is the full id returned by cafleet message poll <my-member-id> --json. The default text-mode poll output truncates the body; pass --json when you need the untruncated envelope.
6. Worked example — summarize-pr
This is a tiny end-to-end CAFleet-orchestrated skill called summarize-pr (single Director + one ordinary member named Summarizer). The example uses fake <slug>, <fleet-id>, etc. and is read-only — it is illustrative, not a template you copy. Read it to understand how all five sub-systems fit together; then write your own skill from scratch.
Skill purpose
The user invokes /summarize-pr <pr-number>. The Director:
- Fetches the PR diff via
gh pr diff <pr-number>. - Bootstraps the fleet (the
fleet createcommand spawns the monitor member), waits formonitor live, then spawns a Summarizer member to digest the diff, identify the top 3 risk areas, and write a 200-word summary to a file. - Reviews the summary, asks the user for approval, then tears down.
Resolved task-relpath
The skill's task convention is researches/pr-<pr-number> (PR summaries are research-shaped — one folder per PR with the diff + summary inside).
# Resolve task-scope BASE for researches/pr-1234 (cafleet reference/base-dir.md procedure, built-in tools):
# git rev-parse --show-toplevel → /repo
# task folder → /repo/researches/pr-1234 (auto-created)
${BASE} = /repo/researches/pr-1234. The Director writes the diff to ${BASE}/diff.patch (a non-audit working file) and the summary will land at ${BASE}/summary.md (also a working file, not under prompts/).
Fleet bootstrap
The Director writes the monitor spawn prompt to ${BASE}/prompts/monitor-20260213T143000Z.md first, then bootstraps the fleet, Director, and monitor member in one atomic command:
cafleet fleet create --name "summarize-pr-1234" --coding-agent claude \
--monitor-file ${BASE}/prompts/monitor-20260213T143000Z.md --monitor-model haiku --json
# → {"fleet_id": 7, "director": {"member_id": 8, ...}, "monitor": {"member_id": 9, ...}}
Substitute 7, 8, and 9 literally into every subsequent call the Director makes.
Supervision model
Like every CAFleet-orchestrated skill, summarize-pr has its monitor member live before the Summarizer spawn (§ 2.3): the bootstrap above spawned it, and the Director waits for its ready, then its monitor live gate signal — the monitor member launches cafleet monitor 7 in its own pane and confirms the startup line itself. Each periodic wake lands in the monitor member's pane; it health-checks the Summarizer and messages the Director only on events — the heartbeat backstop that surfaces a stall even when the Summarizer never replies.
Wait for the monitor live message before spawning the Summarizer (the CLI's monitor-first guard backstops).
Render the Summarizer spawn prompt
The Director creates this spawn prompt body (with the [INSERT …] marker substituted before writing); the four {...} identity placeholders are left as written for the CLI to render at spawn:
You are the Summarizer in a summarize-pr team (CAFleet-native).
ROLE DEFINITION: Open [INSERT abs path to roles/summarizer.md] with the Read tool BEFORE any other action. That file is your authoritative role definition.
Load these skills at startup:
- the cafleet skill — for the broker primitives and bash-via-Director routing
FLEET ID: {fleet_id}
DIRECTOR MEMBER ID: {director_member_id}
YOUR MEMBER ID: {member_id}
BASE: /repo/researches/pr-1234
CODING AGENT: {coding_agent}
INPUT FILE: /repo/researches/pr-1234/diff.patch
OUTPUT FILE: /repo/researches/pr-1234/summary.md
When you see cafleet message poll output with a message from the Director, capture the id: from each entry as the task id and ack it via cafleet message ack, then act on the instructions.
On spawn, as your first Bash call, send the ready signal: cafleet message send --from-member-id {member_id} --to-member-id {director_member_id} "ready"
Read INPUT FILE, write a 200-word summary highlighting the top 3 risk areas to OUTPUT FILE, then send complete (doc) to the Director.
The Director writes this rendered text to /repo/researches/pr-1234/prompts/summarizer-20260516T003344Z.md (UTC-compact timestamp) — the audit artifact, carrying the identity placeholders pre-substitution.
Spawn the Summarizer
cafleet member create --fleet-id 7 \
--name "summarizer" \
--description "Digests a PR diff into a 200-word risk summary" \
--file /repo/researches/pr-1234/prompts/summarizer-20260516T003344Z.md \
--json
# → {"member_id": 11, ...}
Capture 11 as the Summarizer's id for the rest of the run (the Summarizer itself reads its own id from the YOUR MEMBER ID: 11 line the CLI rendered into its prompt).
Coordination
The Summarizer reads the diff, writes the summary, and sends (from inside its pane, using the literal ids from its prompt):
cafleet message send --from-member-id 11 --to-member-id 8 \
"complete (doc) — summary 198 words, 3 risk areas"
The Director polls, acks, reads ${BASE}/summary.md, presents it to the user via the {decision_surface} prompt. If the user approves, the Director tears down. If the user requests revisions, the Director sends:
cafleet message send --from-member-id 8 --to-member-id 11 \
"ready (doc)"
(with a COMMENT(director): <revision request> marker at the top of summary.md) and waits for addressed (doc).
Teardown
# Delete the monitor member FIRST (the pane kill takes the loop down),
# then the Summarizer, then the fleet.
cafleet member delete 9
cafleet member delete 11
cafleet fleet delete 7
Order matters: delete the monitor member first (first-out — the pane kill ends the wake source), then the ordinary Summarizer, then the fleet (see § 2.5).
What this example demonstrates
- All five integration sub-systems fire (resolve BASE → bootstrap fleet → spawn the monitor member → spawn the member → monitor-first-out teardown).
- The audit file at
${BASE}/prompts/summarizer-<ts>.mdlives under the task folder, not the repo root. - The cafleet body uses the verb + pointer schema (
complete (doc),ready (doc),addressed (doc)). - The substantive revision request rides as a
COMMENT(director)marker in the document, not in the cafleet body. - Teardown is in the correct order.
7. Common failure modes
These are the failures that have bitten earlier authors. Read them before writing your skill, not after debugging.
7.1 Forgetting to ack messages
Symptom: every cafleet message poll returns the same message over and over, the Director's context fills with stale "ready" hops, and the Director's on-tick health check flags the recipient as not-progressing.
Fix: every message you act on, ack it. Acking moves the task from INPUT_REQUIRED to COMPLETED and removes it from subsequent poll output.
7.2 Inlining role-file content into the spawn prompt
Symptom: cafleet member create exits non-zero with Error: tmux split-window failed: command too long, the member registration is rolled back, no member pane appears.
Fix: use --file (always) and reference the role file by absolute path inside the spawn prompt, not by inlining the content.
7.3 Shell-variable-substituting the Director's own literal ids
Symptom: every cafleet ... call the Director makes triggers a permission prompt that interrupts the agent loop. The user complains that the skill is "asking me about every single command."
Fix: in the Director's own commands, substitute the literal ids printed by cafleet fleet create / cafleet member create directly. The Claude Code harness's permissions.allow matches Bash invocations as literal command strings; an exported shell variable you reference yourself (export MEMBER_ID=…; cafleet member show $MEMBER_ID) breaks the literal match. Never export IDs and reference them via $VAR.
7.4 Writing audit files under the repo root
Symptom: git status shows untracked prompts/ directory at the repo root after running the skill. Operators add /prompts/ to .gitignore. Per-task evidence is scattered across the repo root instead of co-located with the task folder.
Fix: resolve BASE via the cafleet skill's reference/base-dir.md task-scope procedure with a <task-relpath> (per § 2.1). The resolved base IS the task folder; ${BASE}/prompts/ lives inside the task folder. Do NOT resolve the shared-root BASE (no task-relpath) and then write ${BASE}/researches/<slug>/prompts/... — that pattern produces the stale repo-root artifacts.
7.5 Forgetting to omit the BASE: line under ${BASE} == <unset>
Symptom: a member's spawn prompt contains the literal string BASE: <unset>, the member tries to compute Path(BASE) / "prompts" / "...", and the file write fails with OSError: [Errno 22] Invalid argument (most filesystems reject < in paths) or the path appears literally in git status as <unset>/prompts/....
Fix: when ${BASE} == <unset>, drop the BASE: line from the spawn prompt body entirely. The member's existence-check (grep '^BASE:' on its own prompt) naturally treats audit-file features as disabled. The member emits the parens-free anchorless status audit-disabled no BASE in spawn prompt once.
7.6 Falling back to /tmp when BASE resolution fails
Symptom: scratch and audit files appear under /tmp/<random>/prompts/... instead of under ${BASE}. The user cannot find their per-task evidence after the run.
Fix: never fall back to /tmp silently. The <unset> sentinel is a hard stop, not a fallback. If ${BASE} is <unset>, abort with the standardized error Error: BASE is <unset>; refusing to fall back to /tmp (or, for spawned members, follow the skip + inline-fallback branch in § 3.5).
7.7 Calling cafleet fleet delete before cafleet member delete
Symptom: orphan claude processes lingering in tmux panes after the skill completes. The user closes the panes manually. On the next cafleet fleet create, the panes are rebound and the orphan members re-emerge.
Fix: tear down in this exact order — cafleet member delete the monitor member first (first-out), then cafleet member delete for every remaining member, then cafleet fleet delete. See § 2.5.
7.8 Spawning ordinary members before monitor live
Symptom: ordinary members are spawned before the monitor member has sent its monitor live gate signal, so the heartbeat backstop is not yet running when they begin work — or the spawn fails outright on the CLI's monitor-first guard (fleet <fleet-id> has no active monitor member; spawn one with --role monitor first).
Fix: the cafleet fleet create bootstrap spawns the monitor member (§ 2.2); wait for its ready then monitor live messages before any ordinary cafleet member create. Re-spawn with cafleet member create --role monitor only after a monitor death.
7.9 Leaving stray single braces in the spawn prompt
Symptom: cafleet member create exits 2 with Error: Unknown placeholder '<name>' in custom prompt. Supported placeholders: {fleet_id}, {member_id}, {director_member_id}, {coding_agent}. Double literal braces ({{, }}) to keep them as text. or Error: Malformed custom prompt: ..., and the just-registered member is rolled back.
Fix: the only single-brace tokens allow
…(truncated)