| name | dispatch-session |
| description | Use when spawning a new agent session via Agent of Empires (AoE). Provides the canonical command patterns for spawning planner, implementer, reviewer, and merge-resolver sessions with the correct system prompts, worktrees, groups, and per-session event monitors. Required for the orchestrator agent. |
Dispatch Session
Your job: spawn a sub-agent session via aoe add, deliver its task as the first user message via aoe send, and wire up a dedicated event Monitor so you get notified the moment it's done.
Core principles:
- Full task in the first user message. The system prompt is the persona, unchanged from the plugin files. The full task context (task ID reference + verbatim task block from PLAN.md) goes in the initial message via
aoe send, so a human attaching to the pane can read the conversation and understand what the agent is doing without opening PLAN.md.
- One Monitor per session. Every dispatch ends with a
Monitor tool call on aoe events watch --session <id>. That lets you react to a specific session's session.idle event (fired with reason: turn_completed when the agent finishes a turn) independently instead of demultiplexing a group-wide event stream.
- Polling safety net. Every 10 minutes you run a reconciliation pass via
aoe list to catch any session whose event you may have missed (e.g. events bus glitch, Monitor dropped silently).
- Adversarial review = three parallel Agent subagent calls. Blind Hunter, Edge Case Hunter, and Acceptance Auditor launch as Claude Agent subagents with deliberately different information sets, made in one assistant message so they execute concurrently. You wait for all three tool results before rendering a combined verdict. (AoE sessions are reserved for roles that benefit from persistent panes — planner, implementer, merge-resolver, QA.)
- Always root implementer worktrees at the integration branch. Pass
--worktree-from feature/${TASK_SLUG} to every aoe add -w … -b call. Without it, aoe branches off the main repo's HEAD (which is usually main), and the worktree absorbs any commits main has that aren't on the integration branch — forcing you into cherry-pick land instead of clean merges. Create the integration branch once at orchestrator startup via git branch feature/${TASK_SLUG} <main-or-wherever> if it doesn't already exist.
Prerequisites
This skill assumes:
- AoE is installed and on PATH (
aoe --version works, includes aoe events watch --session)
- The plugin is installed at the conventional path
(
~/.claude/plugins/cache/tpm-workflow/tpm-workflow/) OR accessible via
TPM_WORKFLOW_PATH
- The AoE profile in use has worktrees enabled (the
tpm template sets this)
- A task group has been decided (e.g.
${TASK_SLUG})
PLUGIN_PATH="${TPM_WORKFLOW_PATH:-$HOME/.claude/plugins/cache/tpm-workflow/tpm-workflow}"
test -f "${PLUGIN_PATH}/agents/implementer.md" || echo "WARN: plugin not found at ${PLUGIN_PATH}"
The dispatch pattern
Every dispatch is three steps:
aoe add … to spawn the session. Capture the session ID.
aoe send <session-id> "<full initial message>" to deliver the task.
- Start a dedicated Monitor via
Bash run_in_background=true on
aoe events watch --session <session-id> --filter session.idle,session.failed,session.waiting + Monitor tool on that background task.
Step 3 is not optional. Without it the orchestrator has no live channel telling it when the session is done. Polling alone would lose minutes of wall-clock time; polling without Monitor means busy-checking.
Serial dispatch for large waves
Rule. When a single dispatch round launches more than a handful of AoE sessions (empirically: >5 parallel claude boots), call aoe add plus aoe session start serially with a 2–3 second sleep between each start. Do not background-and-wait them. aoe send of the initial message CAN be parallelized afterward; only the claude boot suffers.
Why. The claude process boot races on shared resources: auth token refresh, the settings file read, and the first-time bypass-permissions handshake. When more than 5 claude processes boot simultaneously inside AoE panes, the race manifests as a portion (sometimes all) of the panes dying immediately with status 1 and the tmux message Pane is dead. The agent never even reads its first user message — it dies during boot.
Concretely, for a wave with >5 tasks:
SIDS=()
for TASK_ID in "${WAVE_TASKS[@]}"; do
SID=$(
aoe add "${REPO_ROOT}" \
--title "impl-${TASK_ID}" \
...
| awk '/^ ID:/ {print $2}'
)
aoe session start "${SID}"
sleep 2
SIDS+=("${SID}")
done
for SID in "${SIDS[@]}"; do
aoe send "${SID}" "${INITIAL_MESSAGE[$SID]}" &
done
wait
For waves with ≤5 tasks, either pattern (serial or parallel aoe session start) is safe; the boot race only triggers above that threshold. The rule also applies to any other fan-out of >5 AoE sessions in one go (e.g. a legacy reviewer fan-out that goes through AoE instead of the Agent subagent path).
Building the initial message
Before aoe send, extract the ### ${TASK_ID} block from .tpm/PLAN.md. Read PLAN.md, locate the header, copy from there to (but excluding) the next ### or ## or EOF. Include the block verbatim (indentation preserved) as the suffix of the message:
<one-line directive>
Here is the full task:
<verbatim task block>
For reviewers, the same block is included in each of the three dispatches.
Dispatching the Planner
SESSION_ID=$(
aoe add "${REPO_ROOT}" \
--title "planner-${TASK_SLUG}" \
--group "${TASK_SLUG}" \
--tool claude \
--cmd "claude --system-prompt \"\$(cat '${PLUGIN_PATH}/agents/planner.md')\" --dangerously-skip-permissions" \
| awk '/^ ID:/ {print $2}'
)
aoe send "${SESSION_ID}" "Plan this task: ${TASK_DESCRIPTION}. Output PLAN.md to .tpm/PLAN.md."
Then start the Monitor on this session (see Per-session Monitor below).
Dispatching an Implementer
SESSION_ID=$(
aoe add "${REPO_ROOT}" \
--title "impl-${TASK_ID}" \
--group "${TASK_SLUG}" \
--worktree "tpm/${TASK_SLUG}/${TASK_ID}" \
--new-branch \
--worktree-from "feature/${TASK_SLUG}" \
--tool claude \
--cmd "claude --system-prompt \"\$(cat '${PLUGIN_PATH}/agents/implementer.md')\" --dangerously-skip-permissions" \
| awk '/^ ID:/ {print $2}'
)
aoe send "${SESSION_ID}" "Implement task ${TASK_ID} from ../.tpm/PLAN.md. Worktree is your CWD. Write SUMMARY.md to .tpm/SUMMARY.md when done.
Here is the full task:
${TASK_BLOCK}"
Dispatching the Review Session
Rule. Reviews run in a dedicated AoE session, not as Agent subagents of the orchestrator. The orchestrator dispatches one review session per task, monitors it via session.idle, and stays free to handle other events while the review runs. The review session internally dispatches the individual reviewer layers as Agent subagents (for information asymmetry) and aggregates verdicts into .tpm/REVIEW.md.
Why. Agent subagents block the orchestrator for the entire review duration (often 5+ minutes per task in prod tier). During that time the orchestrator cannot react to other implementers completing, handle session.failed events, or run reconciliation. A dedicated AoE session keeps the orchestrator responsive and gives the review a visible, inspectable pane in the TUI.
The review session uses agents/adversarial-reviewer.md as its persona. The initial message from the orchestrator specifies the tier and instructs the reviewer on which layers to dispatch. The persona file's subagent types (tpm-workflow:blind-hunter, tpm-workflow:edge-case-hunter, tpm-workflow:acceptance-auditor, tpm-workflow:principal-engineer, tpm-workflow:end-user-simulator) are available inside the review session for Agent tool calls.
Dispatch pattern
REVIEW_ID=$(
aoe add "${REPO_ROOT}" \
--title "review-${TASK_ID}" \
--group "${TASK_SLUG}" \
--cmd "claude --system-prompt \"\$(cat '${PLUGIN_PATH}/agents/adversarial-reviewer.md')\" --dangerously-skip-permissions" \
| awk '/^ ID:/ {print $2}'
)
The review session does NOT get its own worktree. It reads the implementer's worktree via absolute paths passed in the initial message.
Building the initial message
The orchestrator's aoe send message must include:
- The tier (
fast, standard, or prod)
- The worktree path (absolute)
- The task block (verbatim from PLAN.md)
- The diff base (
feature/${TASK_SLUG} or main)
- For prod tier: explicit instruction to dispatch Principal Engineer and End-User Simulator layers
aoe send "${REVIEW_ID}" "Review task ${TASK_ID} at tier ${MODE}.
Worktree: ${WORKTREE_PATH}
Diff base: feature/${TASK_SLUG}
Run: git -C ${WORKTREE_PATH} diff feature/${TASK_SLUG}..HEAD
Task block:
${TASK_BLOCK}
Instructions per tier:
- fast: Do a single-pass review covering all three layers yourself. Write .tpm/REVIEW.md directly.
- standard: Dispatch three Agent subagents in parallel (blind-hunter, edge-case-hunter, acceptance-auditor) with the information-asymmetry contract. Aggregate into .tpm/REVIEW.md.
- prod: Dispatch five Agent subagents in parallel (standard trio + principal-engineer + end-user-simulator). Aggregate into .tpm/REVIEW.md with advisory findings in a separate section.
Write the final aggregated review to ${WORKTREE_PATH}/.tpm/REVIEW.md."
Then start the per-session Monitor (see Per-session Monitor below).
How the review session works internally
The review session (running adversarial-reviewer.md persona) handles the tier logic:
Fast tier: The reviewer does all three layers itself in a single pass, writes .tpm/REVIEW.md directly. No subagent dispatch.
Standard tier: The reviewer dispatches three Agent subagent calls in a single message for parallel execution:
- Blind Hunter (
tpm-workflow:blind-hunter) — diff only, no codebase access. Writes REVIEW-blind.md.
- Edge Case Hunter (
tpm-workflow:edge-case-hunter) — diff + codebase, no spec. Writes REVIEW-edge.md.
- Acceptance Auditor (
tpm-workflow:acceptance-auditor) — diff + task block, executes verification commands. Writes REVIEW-acceptance.md.
Each subagent prompt preserves the information-asymmetry contract: the blind hunter cannot see the spec or codebase, the edge case hunter cannot see the plan, the acceptance auditor runs actual verification commands. Subagent prompts must use absolute paths (subagents inherit the review session's CWD, not the worktree).
After all three return, the reviewer aggregates: any REJECTED = overall REJECTED; else any CHANGES_REQUESTED = overall CHANGES_REQUESTED; else APPROVED. Writes combined .tpm/REVIEW.md.
Prod tier: Same as standard, plus two additional subagents in the same parallel dispatch:
- Principal Engineer (
tpm-workflow:principal-engineer) — blocking, philosophy audit. Writes REVIEW-principal.md.
- End-User Simulator (
tpm-workflow:end-user-simulator) — advisory, end-to-end exercise. Writes END-USER-REPORT.md.
Aggregation includes Principal Engineer as blocking; End-User Simulator findings are appended as advisory.
After the review session completes
The orchestrator's Monitor fires session.idle with reason: turn_completed. The orchestrator:
- Checks that
${WORKTREE_PATH}/.tpm/REVIEW.md exists (if not, mid-turn pause, keep listening).
- Reads REVIEW.md frontmatter (
limit: 20) for the aggregated verdict.
- Acts on the verdict: APPROVED = merge, CHANGES_REQUESTED = dispatch revision, REJECTED = escalate.
- Stops the Monitor via
TaskStop.
- Removes the review session:
aoe remove "review-${TASK_ID}" (review sessions are ephemeral, like implementer sessions post-merge).
Dispatching a Merge Resolver
MERGE_ID=$(
aoe add "${REPO_ROOT}" \
--title "merge-${TASK_ID}" \
--group "${TASK_SLUG}" \
--tool claude \
--cmd "claude --system-prompt \"\$(cat '${PLUGIN_PATH}/agents/merge-resolver.md')\" --dangerously-skip-permissions" \
| awk '/^ ID:/ {print $2}'
)
aoe send "${MERGE_ID}" "Resolve merge conflict for ${WORKTREE_PATH} into feature/${TASK_SLUG}. Conflicting files are in git status. Summaries at ${WORKTREE_PATH}/.tpm/SUMMARY.md (incoming) and the integration branch.
Here is the incoming task:
${INCOMING_TASK_BLOCK}
And the existing work's task(s):
${EXISTING_TASK_BLOCKS}"
Dispatching a QA Validator
QA_ID=$(
aoe add "${REPO_ROOT}" \
--title "qa-${TASK_SLUG}" \
--group "${TASK_SLUG}" \
--tool claude \
--cmd "claude --system-prompt \"\$(cat '${PLUGIN_PATH}/agents/qa-validator.md')\" --dangerously-skip-permissions" \
| awk '/^ ID:/ {print $2}'
)
aoe send "${QA_ID}" "Validate feature ${TASK_SLUG}. Run end-to-end checks. Write QA-REPORT.md to .tpm/QA-REPORT.md.
Feature description: ${TASK_DESCRIPTION}
All task blocks from the plan:
${ALL_TASK_BLOCKS}
Merged task SUMMARY.md paths:
${SUMMARY_PATHS}"
Per-session Monitor (step 3 of every dispatch)
After every aoe add + aoe send, start a Monitor dedicated to that session:
aoe events watch --session "${SESSION_ID}" --filter session.idle,session.failed,session.waiting
Then call the Monitor tool with the returned task ID. Name the monitor with the session's role + task so you can tell them apart:
Monitor(description="impl task-02 completion events", task_id=)
Each time an event line arrives, the Monitor fires a notification with the JSON payload. Parse it:
session.idle (with reason: turn_completed) → the agent finished its turn and is sitting at the prompt; read its output file (SUMMARY / REVIEW-*.md / QA-REPORT.md) frontmatter and dispatch the next step. The reason field is the discriminator — only turn_completed means "turn ended", other reasons indicate different kinds of idleness.
session.failed → diagnose via aoe session logs <id>, decide: respawn, replan, escalate.
session.waiting → the agent is asking for human input; surface to the user (don't auto-answer).
Ignore session.idle events where the expected output file isn't yet present on disk — the sweeper can emit idle mid-turn if the agent pauses between tool calls.
The Monitor's lifetime is bounded by its session's terminal event. Call TaskStop on the Monitor's task ID after handling the terminal event (session.idle with reason: turn_completed, or session.failed) — not later, not "when you get around to it". The rule is simple: TaskStop the Monitor once its terminal event is handled, then move on to the follow-up action. Forgetting this step is the most common hygiene bug in an orchestrator session — leaked Monitors accumulate until their 1-hour timeout_ms expires, burning notification slots and obscuring the real in-flight set.
Periodic polling (10-minute reconciliation tick — mandatory)
This loop is mandatory, not an optional safety net. Start it once per feature, before arming any per-session Monitor, and let it run until Step 10 cleanup. Events can be missed — the sweeper runs every 2s by default, but a sweep-time skip, a crash, or a quirk in Monitor's delivery can leave you holding the orchestrator's hand while a sub-session quietly finished. The loop also carries a secondary duty: each 10-minute tick is a scheduled moment to sweep accumulated defunct Monitors and audit for orphaned sessions — standard state hygiene the orchestrator owes the TUI. Add a cron-style reconciliation:
aoe list --profile "${PROFILE}" --json
For each row in the output whose status is stopped and whose group matches ${TASK_SLUG}, check whether you already handled it (cross-reference with STATE.md). If you didn't, handle it now — treat it as a retroactive session.idle. Skip any task whose STATE.md session_id is (deleted) — those sessions were intentionally removed after merge (Step 6b) and will not appear in aoe list.
Implementation tip: put this in a separate background task that sleeps 600s between runs:
while true; do
sleep 600
aoe list --profile "${PROFILE}" --json
done
Attach a Monitor to it too, so each 10-minute reconciliation line becomes a notification.
After a Session Goes Idle (turn completed)
Once the session.idle event fires with reason: turn_completed and the expected output file exists on disk, the session has finished its turn. Always read the frontmatter only unless you need details — body is for human consumption.
head -20 ${WORKTREE_PATH}/.tpm/SUMMARY.md
Use Read with limit: 20 if you're inside a Claude session.
Cleanup
Session cleanup happens in two phases:
Phase 1: After merge (Step 6b)
Once a task's branch has been reviewed, approved, and merged to the integration branch, immediately remove the implementer's AoE session — but keep the worktree on disk:
aoe remove "impl-${TASK_ID}" || echo "WARN: aoe remove failed for impl-${TASK_ID}"
Do not pass --delete-worktree. The worktree is the audit trail (SUMMARY.md, REVIEW-*.md, git history) and the orchestrator still needs it for git operations.
Error handling: If aoe remove fails, log a warning but still mark session_id as (deleted) in STATE.md. The marker is the reconciliation loop's skip signal; without it, the loop treats the missing session as a retroactive session.idle.
After removal (or failed removal), update STATE.md: set session_id to (deleted) while keeping worktree_path and branch intact. The reconciliation loop (Step 4) skips tasks whose session_id is (deleted) — they won't appear in aoe list and that's expected.
Phase 2: After user validates (Step 10)
Planner and QA sessions are the only AoE sessions still alive at this point (implementer sessions were removed in Phase 1). Clean them up along with the worktrees:
aoe remove "planner-${TASK_SLUG}" || echo "WARN: could not remove planner session"
aoe remove "qa-${TASK_SLUG}" || echo "WARN: could not remove QA session"
for WT_PATH in ${ALL_WORKTREE_PATHS[@]}; do
git worktree remove "${WT_PATH}" --force 2>/dev/null \
|| echo "WARN: could not remove worktree ${WT_PATH}"
done
The orchestrator session itself is never auto-deleted.
Tier-specific dispatches
The blocks below cover dispatch patterns that only apply to certain tiers. The orchestrator consults skills/tier-workflow/SKILL.md to decide WHICH of these blocks to invoke based on the resolved mode: field in .tpm/STATE.md. This skill provides the mechanism only — never the policy.
Tier-specific review instructions
The review session's initial message specifies the tier. The tier determines which layers the reviewer dispatches internally. These are the layers per tier (the review session handles this logic, not the orchestrator):
Fast: Single-pass, no subagents. Reviewer covers blind + edge + acceptance in one pass. Writes .tpm/REVIEW.md directly.
Standard: Three parallel Agent subagent calls (blind-hunter, edge-case-hunter, acceptance-auditor). Writes per-layer files, then aggregates into .tpm/REVIEW.md.
Prod: Five parallel Agent subagent calls (standard trio + principal-engineer + end-user-simulator). Principal Engineer is blocking; End-User Simulator is advisory. Aggregates into .tpm/REVIEW.md with advisory findings in a separate section.
The review session's persona (adversarial-reviewer.md) documents the information-asymmetry contracts for each layer and the subagent dispatch pattern. The orchestrator does not need to know these details; it dispatches one review session and reads the aggregated verdict from .tpm/REVIEW.md.
Anti-patterns
- Filtering on
session.completed for interactive agents — that fires on tmux-stopped, not on turn-end. Use session.idle with reason: turn_completed.
- Skipping the per-session Monitor — a group-wide Monitor works too, but then you must demultiplex JSON to react to specific sessions. Per-session is cleaner and matches the orchestrator's mental model.
- Skipping the 10-minute polling — relying solely on Monitor means one missed event stalls the workflow indefinitely.
- Leaking per-session Monitors — the Monitor is a lightweight watch on one session's terminal event, not a session-long heartbeat. Always
TaskStop it after the terminal event is handled. Leaked Monitors pile up in the background-task inbox, drown real notifications in stale ones, and only clear when their 1-hour timeout_ms expires.
- Launching reviewers as Agent subagents of the orchestrator — this blocks the orchestrator for the entire review duration. Dispatch one AoE review session per task instead; the review session handles subagent dispatch internally.
- Starting many AoE sessions concurrently (parallel
aoe session start calls) — claude panes die during the boot race (auth token refresh, settings file read, first-time bypass-permissions handshake are shared resources that don't tolerate >5 concurrent accesses). Serial start with a 2s gap avoids the issue. See "Serial dispatch for large waves" above.
- Dispatching a single "adversarial-reviewer" session — the V1 plugin had this. It runs all three layers sequentially in the same session, which loses the information-asymmetry benefit and can't parallelize. Always dispatch three separate reviewers.
- Stuffing the task block into the system prompt — the system prompt is the persona. Task context goes in the first user message via
aoe send.
- Sending only a task ID — a human attaching to the pane can't see what the agent is doing. Always include the full task block inline after the lead directive.
- Spawning without a group — breaks
--group event filtering and bulk cleanup.
- Reusing a worktree across tasks — defeats the point of isolation.
- Removing worktrees when deleting sessions —
aoe remove without --delete-worktree is the correct call at Step 6b. The worktree stays for audit and git operations; only the session entry is removed.
- Keeping implementer sessions alive after merge — stale session rows clutter the TUI. Remove them at Step 6b; the worktree and STATE.md row are sufficient for traceability.
- Dispatching Principal Engineer without the philosophy-discovery skill referenced in the initial message — the agent won't know how to locate philosophy sources (CLAUDE.md, .claude/skills/**/SKILL.md) in the target repo and will either hallucinate conventions or produce a vacuous APPROVED verdict. Always include the philosophy-discovery skill reference in the prompt so the agent has a concrete discovery procedure.