claude-code-session-broker
Use this skill for Claude Code V2 parity. Claude Code does not expose the same
Codex thread tools to Arcgentic, so V2 parity is broker-backed: the broker keeps
the same four-role state contract and uses native Claude Code tooling
(subagents via Agent/SendMessage/ListAgents), hooks, or explicit
copy-back, depending on what the host supports — see "Broker priority"
below for the exact three transports and their order.
Relevant host capabilities:
Contract
V2 still has exactly five role identities:
Orchestrator
Planner
Developer
Test
Auditor
Do not create round-numbered role identities. Store round identity in state and
prompt payloads.
Broker priority
Use the strongest available transport, checked in this order:
- Native tooling (tier 0) — if this session's own tool list includes
Agent, SendMessage, and ListAgents, use them directly (see
"Procedure — tier 0" below). For a first-time dispatch to a role
(kind: "create"), dispatch is synchronous for a foreground Agent
call (you get the role's output the moment the call returns — no
external event to wait for) or notification-driven for a background
Agent call (a task-notification arrives with the role's output when
it finishes); either way, Agent's result carries a resumable
agentId you record as the broker thread-id. For a repeat dispatch
to a role that already has a recorded thread (kind: "reuse" — e.g. a
needs_fix loop back to Developer), skip Agent entirely and use
SendMessage against that already-recorded agentId instead; its
reply arrives asynchronously, like a background Agent call's
notification.
- Hook-backed broker (fallback) — use when tier 0's three tools are
not present in this session (see "Procedure — hook fallback" below).
- Explicit copy-back (last resort) — when neither of the above is
available: the role session returns
RoleReturnSignal in its own
output, and a human or the orchestrator manually runs
arcgentic v2-return-signal with that JSON. No automation attempts
this on its own; do not pretend it succeeded silently.
All three transports write the same state shape via the same CLI
commands (v2-session-plan, v2-record-session, v2-dispatch-role,
v2-return-signal) — only how the role's prompt gets delivered and its
output gets collected differs.
Procedure — tier 0 (native tooling)
Check once per session, before dispatching anything: does your own tool
list include Agent, SendMessage, and ListAgents? If yes, use this
procedure. If no, skip to "Procedure — hook fallback" below.
Get the dispatch plan:
arcgentic v2-session-plan \
--state .agentic-rounds/state.yaml \
--host claude-code-broker \
--user-request '<current user request>'
If the JSON's orchestrator_status is sleeping, stop immediately —
a role is already dispatched and pending; do not dispatch another.
If orchestrator_status is active, read actions[0]. Its prompt
field is the complete, ready-to-send role prompt (it already contains
the arcgentic-role-return footer instructions — do not edit it, do
not add or remove content). Its kind field is either "create" (no
thread is recorded for this role yet) or "reuse" (this role already
has a recorded thread from an earlier dispatch — e.g. Developer's
needs_fix loop back to Developer, Auditor's audit_in_progress
retry, or a new round's Planner dispatch after the previous round
closed). reuse is normal, common V2 routing, not an edge case —
branch on kind in step 4 below.
Dispatch, branching on actions[0].kind:
kind: "create": before calling Agent, your own working
directory must already be the target project root. Agent has no
working-directory parameter of its own — a dispatched agent
inherits your shell's cwd and has no other way to learn where the
project is, so if the orchestrator's shell has not already cd'd
into the project root, the role prompt's relative file paths (e.g.
.agentic-rounds/state.yaml, docs/plans/...) will resolve
against the wrong directory. (This does not apply to kind: "reuse" below — that dispatches via SendMessage to an
already-running agent, which already has its own working directory
from when it was first created.)
single-session-subagent mode: call the Agent tool with
prompt = actions[0].prompt, run_in_background: false
(foreground — you get the result directly in this same turn), and
subagent_type: "general-purpose". Do NOT use arcgentic's own
planner/developer/auditor/etc. agent types for this — an
arcgentic-installed project ships those agent types (see
agents/planner.md, agents/developer.md, agents/auditor.md at
the repo root) and their names match the V2 role names, but they
implement a different, incompatible V1/v0.2 contract (V1's
planner agent produces "18/12/10-section handoff docs", not a
V2 role-prompt/return-signal exchange). Dispatching a role through
one of those types would run the wrong contract.
multi-session-subthread mode: call Agent with the same
prompt but run_in_background: true.
- Either way,
Agent's return carries a real, resumable agentId
— that is what you record as the broker thread-id in step 5,
not actions[0].thread_id (for a create action that field is
only a placeholder, not a real agent id).
kind: "reuse":
- Do NOT call
Agent — that would create a brand-new agent and
orphan this role's existing context. actions[0].thread_id is
already the real, previously-recorded agentId for this role.
- Call
SendMessage with to = actions[0].thread_id and
message = actions[0].prompt.
SendMessage delivers asynchronously — it does not hand you the
reply inline the way a foreground Agent call does. Treat this
like a background dispatch: end your turn after step 6 and wait
for the reply to arrive as a message from that agent.
Record the session — kind: "create" only, using the agentId
Agent returned as the broker thread-id:
arcgentic v2-record-session \
--state .agentic-rounds/state.yaml \
--host claude-code-broker \
--role <planner|developer|test|auditor> \
--thread-id <agentId>
kind: "reuse": skip this step. The role is already recorded from its
earlier dispatch — that recorded thread is exactly why this action was
reuse instead of create. Re-running v2-record-session with the
same thread-id would be harmless/idempotent, but it records nothing
new, so there is no reason to run it.
Record the dispatch, using the thread-id from step 4 (the new
agentId for create, or actions[0].thread_id for reuse):
arcgentic v2-dispatch-role \
--state .agentic-rounds/state.yaml \
--host claude-code-broker \
--role <planner|developer|test|auditor> \
--thread-id <thread-id-from-step-4>
This puts the Orchestrator to sleep waiting for this role's reply. In
background mode (a multi-session-subthread create dispatch, or any
reuse dispatch — those go through SendMessage, which is always
asynchronous), end your turn here: you'll resume via the
task-notification (background Agent) or the peer's reply message
(SendMessage). In foreground mode (a single-session-subagent
create dispatch via a foreground Agent call), continue directly to
the next step in this same turn — you already have the role's output
from step 4.
Collect the role's output:
- Foreground
Agent call: its return value IS the role's output —
continue directly to step 8 in the same turn, no waiting.
- Background
Agent call: wait for the task-notification. When it
arrives, its content is the role's output. Do not poll ListAgents
for completion — the notification is the completion signal.
SendMessage (reuse dispatch): wait for the agent's reply
message. When it arrives, its content is the role's output —
validate it for the footer exactly like a fresh Agent call's
return value (step 8, next).
Validate the output contains exactly one
```arcgentic-role-return ... ``` fenced JSON footer (or the
ARCGENTIC_ROLE_RETURN ... END_ARCGENTIC_ROLE_RETURN marker form).
If it's missing or malformed, use SendMessage to resume the same
agent (by its agentId) with a corrective instruction: "Your last
response was missing the required arcgentic-role-return footer.
Re-send your summary with exactly one such footer, formatted as
instructed." Repeat step 8 with the resumed agent's reply. This is
the same fail-closed contract the hook fallback enforces via
decision: block — here it's enforced by you, the orchestrator,
checking directly, since there is no external hook watching this
session.
Record the signal, which wakes the Orchestrator:
arcgentic v2-return-signal \
--state .agentic-rounds/state.yaml \
--signal-json '<the footer JSON from step 8>'
Go back to step 1 and dispatch the next role.
Procedure — hook fallback
Use this procedure only when tier 0's Agent/SendMessage/ListAgents
are not available in this session.
Install project-local Claude Code hooks once:
arcgentic claude-code-broker install-hooks \
--settings .claude/settings.local.json \
--state .agentic-rounds/state.yaml
The installed Stop/SubagentStop hook calls:
arcgentic claude-code-broker handle-stop \
--state .agentic-rounds/state.yaml
The hook reads Claude Code's last_assistant_message, extracts the
arcgentic-role-return footer, runs the same V2 return validation,
updates .agentic-rounds/state.yaml, and writes a broker inbox
record under .agentic-rounds/claude-code-broker/inbox/.
Initialize or read V2 host state:
arcgentic v2-session-plan \
--state .agentic-rounds/state.yaml \
--host claude-code-broker \
--user-request '<current user request>'
If orchestrator_status is sleeping, stop immediately. The broker
is waiting for pending_role; do not dispatch another role.
If orchestrator_status is active, create or resume only the
single role context in actions, then record its broker id:
arcgentic v2-record-session \
--state .agentic-rounds/state.yaml \
--host claude-code-broker \
--role <planner|developer|test|auditor> \
--thread-id <broker-session-id>
Inject only that role's prompt. The developer does not receive
auditor reasoning. The auditor does not receive developer chat
transcript. The planner owns phase decisions.
After injecting the role prompt, put the Orchestrator to sleep and
end the Orchestrator turn:
arcgentic v2-dispatch-role \
--state .agentic-rounds/state.yaml \
--host claude-code-broker \
--role <planner|developer|test|auditor> \
--thread-id <broker-session-id>
Require every role turn to end with RoleReturnSignal JSON.
Record the signal. This wakes the Orchestrator and clears the
pending dispatch:
arcgentic v2-return-signal \
--state .agentic-rounds/state.yaml \
--signal-json '<RoleReturnSignal JSON>'
Re-run v2-session-plan --host claude-code-broker and dispatch the
next role.
Hook guidance
When hooks are available, configure stop hooks to extract the role's final
response and pass it back to the orchestrator as context. The hook must not
invent a PASS/NEEDS_FIX outcome. It only transports the role's own
RoleReturnSignal.
The bundled hook runtime uses official Claude Code Stop/SubagentStop input
fields, especially last_assistant_message and stop_hook_active. If the
Orchestrator is sleeping and the role output lacks a valid footer, the hook
blocks once with a corrective reason. If stop_hook_active is already true, it
does not block again, preventing hook recursion.
Fail-closed rules
- If a role returns prose without valid
RoleReturnSignal, do not advance.
- If the broker cannot identify which role produced a signal, do not advance.
- If the Orchestrator is sleeping, do not dispatch more work until the pending
role returns.
- If a role tries to rename itself outside the four fixed titles, reject it.
- If Claude Code transport is unavailable, fall back to explicit copy-back
rather than pretending automation succeeded.
1---2name: claude-code-session-broker3description: Use when running Arcgentic V2 in Claude Code and fixed Planner, Developer, and Auditor role sessions must be coordinated through a broker.4---56# claude-code-session-broker78Use this skill for Claude Code V2 parity. Claude Code does not expose the same9Codex thread tools to Arcgentic, so V2 parity is broker-backed: the broker keeps10the same four-role state contract and uses native Claude Code tooling11(subagents via `Agent`/`SendMessage`/`ListAgents`), hooks, or explicit12copy-back, depending on what the host supports — see "Broker priority"13below for the exact three transports and their order.1415Relevant host capabilities:1617- Subagents provide isolated role contexts within a session, and back18 tier 0's `Agent`/`SendMessage`/`ListAgents` dispatch:19 <https://code.claude.com/docs/en/sub-agents>20- Hooks can observe stop events and final assistant output, and back the21 hook-backed broker fallback: <https://code.claude.com/docs/en/hooks>22- Agent Teams (<https://code.claude.com/docs/en/agent-teams>) coordinate23 across separate sessions when enabled, but they are a different host24 feature from this skill's three transports below. Tier 0 supersedes25 Agent Teams for V2 dispatch — do not treat Agent Teams as a live,26 separate transport option here.2728## Contract2930V2 still has exactly five role identities:3132- `Orchestrator`33- `Planner`34- `Developer`35- `Test`36- `Auditor`3738Do not create round-numbered role identities. Store round identity in state and39prompt payloads.4041## Broker priority4243Use the strongest available transport, checked in this order:44451. **Native tooling (tier 0)** — if this session's own tool list includes46 `Agent`, `SendMessage`, and `ListAgents`, use them directly (see47 "Procedure — tier 0" below). For a first-time dispatch to a role48 (`kind: "create"`), dispatch is synchronous for a foreground `Agent`49 call (you get the role's output the moment the call returns — no50 external event to wait for) or notification-driven for a background51 `Agent` call (a task-notification arrives with the role's output when52 it finishes); either way, `Agent`'s result carries a resumable53 `agentId` you record as the broker `thread-id`. For a repeat dispatch54 to a role that already has a recorded thread (`kind: "reuse"` — e.g. a55 `needs_fix` loop back to Developer), skip `Agent` entirely and use56 `SendMessage` against that already-recorded `agentId` instead; its57 reply arrives asynchronously, like a background `Agent` call's58 notification.592. **Hook-backed broker (fallback)** — use when tier 0's three tools are60 not present in this session (see "Procedure — hook fallback" below).613. **Explicit copy-back (last resort)** — when neither of the above is62 available: the role session returns `RoleReturnSignal` in its own63 output, and a human or the orchestrator manually runs64 `arcgentic v2-return-signal` with that JSON. No automation attempts65 this on its own; do not pretend it succeeded silently.6667All three transports write the same state shape via the same CLI68commands (`v2-session-plan`, `v2-record-session`, `v2-dispatch-role`,69`v2-return-signal`) — only how the role's prompt gets delivered and its70output gets collected differs.7172## Procedure — tier 0 (native tooling)7374Check once per session, before dispatching anything: does your own tool75list include `Agent`, `SendMessage`, and `ListAgents`? If yes, use this76procedure. If no, skip to "Procedure — hook fallback" below.77781. Get the dispatch plan:7980 ```bash81 arcgentic v2-session-plan \82 --state .agentic-rounds/state.yaml \83 --host claude-code-broker \84 --user-request '<current user request>'85 ```86872. If the JSON's `orchestrator_status` is `sleeping`, stop immediately —88 a role is already dispatched and pending; do not dispatch another.89903. If `orchestrator_status` is `active`, read `actions[0]`. Its `prompt`91 field is the complete, ready-to-send role prompt (it already contains92 the `arcgentic-role-return` footer instructions — do not edit it, do93 not add or remove content). Its `kind` field is either `"create"` (no94 thread is recorded for this role yet) or `"reuse"` (this role already95 has a recorded thread from an earlier dispatch — e.g. Developer's96 `needs_fix` loop back to Developer, Auditor's `audit_in_progress`97 retry, or a new round's Planner dispatch after the previous round98 closed). `reuse` is normal, common V2 routing, not an edge case —99 branch on `kind` in step 4 below.1001014. Dispatch, branching on `actions[0].kind`:102103 - `kind: "create"`: before calling `Agent`, your own working104 directory must already be the target project root. `Agent` has no105 working-directory parameter of its own — a dispatched agent106 inherits your shell's cwd and has no other way to learn where the107 project is, so if the orchestrator's shell has not already `cd`'d108 into the project root, the role prompt's relative file paths (e.g.109 `.agentic-rounds/state.yaml`, `docs/plans/...`) will resolve110 against the wrong directory. (This does not apply to `kind:111 "reuse"` below — that dispatches via `SendMessage` to an112 already-running agent, which already has its own working directory113 from when it was first created.)114 - `single-session-subagent` mode: call the `Agent` tool with115 `prompt` = `actions[0].prompt`, `run_in_background: false`116 (foreground — you get the result directly in this same turn), and117 `subagent_type: "general-purpose"`. Do NOT use arcgentic's own118 `planner`/`developer`/`auditor`/etc. agent types for this — an119 arcgentic-installed project ships those agent types (see120 `agents/planner.md`, `agents/developer.md`, `agents/auditor.md` at121 the repo root) and their names match the V2 role names, but they122 implement a different, incompatible V1/v0.2 contract (V1's123 `planner` agent produces "18/12/10-section handoff docs", not a124 V2 role-prompt/return-signal exchange). Dispatching a role through125 one of those types would run the wrong contract.126 - `multi-session-subthread` mode: call `Agent` with the same127 `prompt` but `run_in_background: true`.128 - Either way, `Agent`'s return carries a real, resumable `agentId`129 — that is what you record as the broker `thread-id` in step 5,130 not `actions[0].thread_id` (for a `create` action that field is131 only a placeholder, not a real agent id).132133 - `kind: "reuse"`:134 - Do NOT call `Agent` — that would create a brand-new agent and135 orphan this role's existing context. `actions[0].thread_id` is136 already the real, previously-recorded `agentId` for this role.137 - Call `SendMessage` with `to` = `actions[0].thread_id` and138 `message` = `actions[0].prompt`.139 - `SendMessage` delivers asynchronously — it does not hand you the140 reply inline the way a foreground `Agent` call does. Treat this141 like a background dispatch: end your turn after step 6 and wait142 for the reply to arrive as a message from that agent.1431445. Record the session — `kind: "create"` only, using the `agentId`145 `Agent` returned as the broker `thread-id`:146147 ```bash148 arcgentic v2-record-session \149 --state .agentic-rounds/state.yaml \150 --host claude-code-broker \151 --role <planner|developer|test|auditor> \152 --thread-id <agentId>153 ```154155 `kind: "reuse"`: skip this step. The role is already recorded from its156 earlier dispatch — that recorded thread is exactly why this action was157 `reuse` instead of `create`. Re-running `v2-record-session` with the158 same `thread-id` would be harmless/idempotent, but it records nothing159 new, so there is no reason to run it.1601616. Record the dispatch, using the `thread-id` from step 4 (the new162 `agentId` for `create`, or `actions[0].thread_id` for `reuse`):163164 ```bash165 arcgentic v2-dispatch-role \166 --state .agentic-rounds/state.yaml \167 --host claude-code-broker \168 --role <planner|developer|test|auditor> \169 --thread-id <thread-id-from-step-4>170 ```171172 This puts the Orchestrator to sleep waiting for this role's reply. In173 background mode (a `multi-session-subthread` `create` dispatch, or any174 `reuse` dispatch — those go through `SendMessage`, which is always175 asynchronous), end your turn here: you'll resume via the176 task-notification (background `Agent`) or the peer's reply message177 (`SendMessage`). In foreground mode (a `single-session-subagent`178 `create` dispatch via a foreground `Agent` call), continue directly to179 the next step in this same turn — you already have the role's output180 from step 4.1811827. Collect the role's output:183 - Foreground `Agent` call: its return value IS the role's output —184 continue directly to step 8 in the same turn, no waiting.185 - Background `Agent` call: wait for the task-notification. When it186 arrives, its content is the role's output. Do not poll `ListAgents`187 for completion — the notification is the completion signal.188 - `SendMessage` (`reuse` dispatch): wait for the agent's reply189 message. When it arrives, its content is the role's output —190 validate it for the footer exactly like a fresh `Agent` call's191 return value (step 8, next).1921938. Validate the output contains exactly one194 ` ```arcgentic-role-return ... ``` ` fenced JSON footer (or the195 `ARCGENTIC_ROLE_RETURN ... END_ARCGENTIC_ROLE_RETURN` marker form).196 If it's missing or malformed, use `SendMessage` to resume the same197 agent (by its `agentId`) with a corrective instruction: "Your last198 response was missing the required `arcgentic-role-return` footer.199 Re-send your summary with exactly one such footer, formatted as200 instructed." Repeat step 8 with the resumed agent's reply. This is201 the same fail-closed contract the hook fallback enforces via202 `decision: block` — here it's enforced by you, the orchestrator,203 checking directly, since there is no external hook watching this204 session.2052069. Record the signal, which wakes the Orchestrator:207208 ```bash209 arcgentic v2-return-signal \210 --state .agentic-rounds/state.yaml \211 --signal-json '<the footer JSON from step 8>'212 ```21321410. Go back to step 1 and dispatch the next role.215216## Procedure — hook fallback217218Use this procedure only when tier 0's `Agent`/`SendMessage`/`ListAgents`219are not available in this session.2202211. Install project-local Claude Code hooks once:222223 ```bash224 arcgentic claude-code-broker install-hooks \225 --settings .claude/settings.local.json \226 --state .agentic-rounds/state.yaml227 ```228229 The installed Stop/SubagentStop hook calls:230231 ```bash232 arcgentic claude-code-broker handle-stop \233 --state .agentic-rounds/state.yaml234 ```235236 The hook reads Claude Code's `last_assistant_message`, extracts the237 `arcgentic-role-return` footer, runs the same V2 return validation,238 updates `.agentic-rounds/state.yaml`, and writes a broker inbox239 record under `.agentic-rounds/claude-code-broker/inbox/`.2402412. Initialize or read V2 host state:242243 ```bash244 arcgentic v2-session-plan \245 --state .agentic-rounds/state.yaml \246 --host claude-code-broker \247 --user-request '<current user request>'248 ```2492503. If `orchestrator_status` is `sleeping`, stop immediately. The broker251 is waiting for `pending_role`; do not dispatch another role.2522534. If `orchestrator_status` is `active`, create or resume only the254 single role context in `actions`, then record its broker id:255256 ```bash257 arcgentic v2-record-session \258 --state .agentic-rounds/state.yaml \259 --host claude-code-broker \260 --role <planner|developer|test|auditor> \261 --thread-id <broker-session-id>262 ```2632645. Inject only that role's prompt. The developer does not receive265 auditor reasoning. The auditor does not receive developer chat266 transcript. The planner owns phase decisions.2672686. After injecting the role prompt, put the Orchestrator to sleep and269 end the Orchestrator turn:270271 ```bash272 arcgentic v2-dispatch-role \273 --state .agentic-rounds/state.yaml \274 --host claude-code-broker \275 --role <planner|developer|test|auditor> \276 --thread-id <broker-session-id>277 ```2782797. Require every role turn to end with `RoleReturnSignal` JSON.2802818. Record the signal. This wakes the Orchestrator and clears the282 pending dispatch:283284 ```bash285 arcgentic v2-return-signal \286 --state .agentic-rounds/state.yaml \287 --signal-json '<RoleReturnSignal JSON>'288 ```2892909. Re-run `v2-session-plan --host claude-code-broker` and dispatch the291 next role.292293## Hook guidance294295When hooks are available, configure stop hooks to extract the role's final296response and pass it back to the orchestrator as context. The hook must not297invent a PASS/NEEDS_FIX outcome. It only transports the role's own298`RoleReturnSignal`.299300The bundled hook runtime uses official Claude Code Stop/SubagentStop input301fields, especially `last_assistant_message` and `stop_hook_active`. If the302Orchestrator is sleeping and the role output lacks a valid footer, the hook303blocks once with a corrective reason. If `stop_hook_active` is already true, it304does not block again, preventing hook recursion.305306## Fail-closed rules307308- If a role returns prose without valid `RoleReturnSignal`, do not advance.309- If the broker cannot identify which role produced a signal, do not advance.310- If the Orchestrator is sleeping, do not dispatch more work until the pending311 role returns.312- If a role tries to rename itself outside the four fixed titles, reject it.313- If Claude Code transport is unavailable, fall back to explicit copy-back314 rather than pretending automation succeeded.