Armature Coordinator
The coordinator manages execution flow — it does not implement features itself. Its job is to survey the story DAG, dispatch workers for each wave of ready tasks, and close the story when all tasks are done.
Prerequisites
If
armis not found, stop and resolve this before proceeding.Worker identity required. Run
arm worker-initonce per clone before claiming any tasks:arm worker-init --check || arm worker-initarm claimcallsresolveWorkerAndLog, which fails with "worker not initialized" if no worker ID is set in git config.Understand the story DAG before dispatching. Run:
arm list --parent STORY-ID # all tasks + statuses arm list --status blocked # diagnose any blockers arm doctor # repo health checkFix any
doctorerrors before claiming work.
DAG Hygiene Mandate
arm validate and arm doctor must exit clean at all times. This is non-negotiable.
Before dispatching any worker and after each wave completes, run:
arm validate # zero ERRORs; all issues cited
arm doctor # zero errors; no broken refs, orphaned ops, or cycles
If either exits non-zero, stop. Fix the reported issues before proceeding. Treat DAG decay the same way you treat failing tests — it is a blocker, not a warning to ignore.
Warnings from other stories must be resolved, not ignored. If arm doctor reports a D1 (commits referencing non-done issues) or D2 (stale claims) from unrelated work, clean them up before starting your coordination wave. DAG health is cumulative.
The Coordinator Loop
digraph coordinator_loop {
"arm ready" [shape=box];
"Empty?" [shape=diamond];
"Parallel?" [shape=diamond];
"Sequential wave" [shape=box];
"Parallel wave" [shape=box];
"Claim + render-context all" [shape=box];
"dispatch workers" [shape=box];
"wait + integrate" [shape=box];
"arm validate" [shape=box];
"transition story" [shape=box];
"push + PR" [shape=box];
"Done" [shape=doublecircle];
"arm ready" -> "Empty?";
"Empty?" -> "arm validate" [label="yes — all done"];
"Empty?" -> "Parallel?" [label="no"];
"Parallel?" -> "Sequential wave" [label="deps between tasks"];
"Parallel?" -> "Parallel wave" [label="independent tasks"];
"Sequential wave" -> "dispatch workers";
"Parallel wave" -> "Claim + render-context all";
"Claim + render-context all" -> "dispatch workers";
"dispatch workers" -> "wait + integrate";
"wait + integrate" -> "arm ready";
"arm validate" -> "transition story";
"transition story" -> "push + PR";
"push + PR" -> "Done";
}
Step-by-Step
1. Survey the Story and Create a Feature Branch
arm list --parent STORY-ID
arm doctor
git checkout -b feat/STORY-ID # create the story branch NOW, before any worker is dispatched
Identify which tasks are open and which have blocked_by dependencies. Group
tasks into waves — tasks within the same wave have no dependencies on each other
and can run in parallel. Tasks in different waves must run sequentially.
Create the feature branch before dispatching any worker. This is the shared
story branch, but workers do not commit to it directly: each worker commits to
its own per-task branch (task/TASK-ID) in an isolated worktree created by
arm claim --worktree (see Dispatch Protocol steps 4-5). The coordinator later
merges each completed task branch into feat/STORY-ID (see "After Workers
Return", section b). If the story branch does not exist before dispatch, there
is nothing for the coordinator to merge task branches into, and the story
cannot be reviewed via PR.
2. Find Ready Work
arm ready # unblocked, unclaimed tasks
If arm ready returns nothing and not all tasks are done, check for
dependency cycles or stalled in-progress tasks:
arm ready --explain # why each open task is NOT ready (blocked/claimed/missing dep)
arm list --status in-progress # claims that may have expired
arm list --status blocked # diagnose blockers
arm ready --explain prints a per-task diagnosis for every open task that
did not make it into the ready queue. Use it as the first step whenever the
queue looks unexpectedly empty.
3. Record Wave Manifest
Before dispatching any worker, record the wave manifest so the verification gate has a stable baseline to diff against:
WAVE_TASK_IDS="TASK-A TASK-B ..." # exact IDs in dispatch order
WAVE_BASE_SHA=$(git rev-parse HEAD) # commit HEAD at wave start
WAVE_BRANCH=$(git rev-parse --abbrev-ref HEAD) # story feature branch
# Classify wave type (determines which verification profile to run)
WAVE_TYPE=docs-skill-only # default; promoted below if code files present
Wave type auto-promotion rule: inspect the ready-task scope fields. If any
task touches files matching *.go, go.mod, go.sum, Makefile, cmd/**,
or internal/** outside of internal/skillsembed/, set WAVE_TYPE=code.
A wave is docs-skill-only only when every changed file is a SKILL.md,
references/*.md, or other non-compiled documentation.
# Collect scope files from arm render-context output for each task in WAVE_TASK_IDS,
# or use `git diff --name-only "$WAVE_BASE_SHA"..HEAD` after workers return.
# Example: auto-promote based on task scope fields before dispatch:
WAVE_SCOPE_FILES=$(arm ready --parent STORY-ID --format json | python3 -c "import sys,json; [print(f) for t in json.load(sys.stdin) for f in t.get('scope',[])]")
if echo "$WAVE_SCOPE_FILES" | grep -E '\.(go|mod|sum)$' | grep -q . || \
echo "$WAVE_SCOPE_FILES" | grep -E '^(Makefile|cmd/|internal/)' | grep -qvE 'internal/skillsembed'; then
WAVE_TYPE=code
fi
4. Dispatch Workers
For each wave of ready tasks:
Claim and get context for each task:
arm claim TASK-ID --ttl 120 --worktree arm render-context TASK-ID --format agent--worktreeis REQUIRED for worker dispatch. This is an invariant, not merely a best practice for permissions. The binding-resolution logic in the harness hook depends on the binding-identity invariant: each agent must operate under exactly one issue binding, and that binding must follow the artifact being touched, not the process touching it. Without a worktree, the hook's four-step resolution chain (file path → event cwd → session cwd → env var) has no per-task.gitdirectory to resolve from, breaking the isolation that makes per-task enforcement possible.When you pass
--worktreetoarm claim:arm claimauto-provisions a git worktree at.worktrees/<issue-id>on a task-specific branch- The task ID is written to the worktree's git-dir
armature-issue-idfile - Workers edit files inside the worktree (step 1 of binding resolution)
- The hook reads the binding from the file path being edited (step 1 succeeds)
- Events are evaluated under the correct task's policy
Without a worktree:
- No task-specific
.git/armature-issue-idfile exists - Step 1 of binding resolution finds no file and falls through to steps 2–4
- All events resolve to the session's binding (or env var) regardless of which agent's code is being changed
- Scope enforcement becomes meaningless; multiple agents cannot be parallelized safely
Do not pre-create the worktree with
git worktree add; letarm claim --worktreehandle creation (it sets up binding, branch, and default location correctly).Set
--ttlto exceed your expected worker runtime. Default is 60 minutes; use--ttl 240or higher for complex tasks. If the TTL expires while a worker is still running, the claim becomes stale and another coordinator may re-dispatch the same task. Workers send periodic heartbeats (arm heartbeat TASK-ID) to reset the TTL — the worker skill handles this — but the coordinator's initial TTL must cover the time until the first heartbeat.Dispatch each task to a worker agent using your platform's agent dispatch capability. Pass the full
render-contextoutput as the task specification.For parallel waves, assign each worker a log slot before dispatch:
export ARM_LOG_SLOT=<slot-number>
See Dispatch Protocol below for the full worker prompt format.
5. Parallel Dispatch (independent tasks in one wave)
Pre-claim all tasks in the wave, then dispatch workers concurrently. Each worker:
- receives the pre-claimed issue context
- implements and transitions to
done - does NOT run
arm claimagain
Claim collisions are handled at pre-claim time by the coordinator.
Dispatch Protocol
Transcript-free dispatch (normative). Dispatch every worker and reviewer with the rendered task spec and relevant file paths only — never an inherited transcript. Reviewers receive bundle paths, not inlined bundle content. Confirmation reviewers also receive the findings-scope file path (the consolidated remediating set) — that list is not inherited from a prior reviewer transcript. Remediation dispatches state what changed since the last pass; unchanged skills and bundles are not re-read or re-sent.
Effort defaults (normative). Reasoning effort defaults to medium for worker dispatch and task-level reviews. Assign high effort explicitly at planning time for concurrency, security, or cross-cutting refactor work, and auto-escalate to high when a task's remediation reaches cycle 2 (see the bounded review protocol in section a.2). Story-level final audits (the armature-auditor pass in Story Completion) remain high effort always.
Each worker's context package must contain:
Skill invocation (VERY FIRST instruction):
You are an armature worker. Invoke the `armature-worker` skill via the Skill tool before proceeding.This must appear before everything else — the skill loads the worker's operating procedure and pre-flight checks.
Log slot (second instruction, before any
armcommand):Before running any arm command, run: export ARM_LOG_SLOT=<assigned-slot>This must be the second line of the worker's prompt — immediately after the skill invocation.
Full
render-contextoutput — this is the worker's complete task spec. Do not summarize it; pass it verbatim.Pre-claimed notice — tell the worker the issue is already claimed and it must NOT run
arm claimagain:This issue has been pre-claimed. Do NOT run `arm claim`. Do NOT run `arm worker-init`.Repository location: Use the isolated git worktree created for this task by
arm claim --worktree, not the main repository:Working directory: .worktrees/TASK-IDTask-specific branch: The task-specific branch was created and is already checked out by
arm claim --worktree. Do NOT rungit checkout feat/STORY-ID(the shared story branch) — this causes collisions with parallel workers. Commit directly to the current branch:Working branch: (task-specific branch from render-context) — do not run `git checkout feat/STORY-ID`See
docs/conventions.md(branch naming section) in the armature repo for the full branch naming convention (feature branches, task branches, and ops branches).Commit instruction — instruct the worker to stage files explicitly using the task's
scopefield, notgit commit -am:Commit: git add <each file listed in scope> && git commit -m "feat(ISSUE-ID): ..."
Background agent Bash limitation: Background agents dispatched without an active terminal session cannot inherit the parent session's Bash permissions. Shell commands will block silently, causing the worker to hang indefinitely. To avoid this, prefer:
- Direct implementation — have the coordinator implement small, well-scoped tasks itself rather than dispatching a background agent.
- Foreground worktrees — create a git worktree manually and run the worker in a foreground terminal session so it inherits Bash permissions.
After Workers Return
Run this integration checklist after each wave completes:
a. Check task status
arm list --parent STORY-ID # confirm all wave tasks are done
arm list --status in-progress # any stragglers?
a.1. Worker Recovery — Unkept arm transition
If a worker returned but their task remains in-progress or done without running arm transition (e.g., the worker forgot or the agent timed out), manually transition the task:
# List all tasks still in-progress or done
arm list --parent STORY-ID --format json | grep -E '"status":\s*"(in-progress|done)"'
# For each task that should be transitioned, manually run:
arm transition TASK-ID --to done --outcome "CONCRETE_OUTCOME_DESCRIPTION"
The recovery step:
- Identify the gap — run
arm list --parent STORY-IDand look for tasks with"status": "in-progress"or"status": "done"that do not appear in the wave manifest or were not markedmergedin step (c) below. - Understand what the worker did — run
arm review commits TASK-ID --branch task/TASK-IDto find the delivery commits and review the scope files modified. Usegit diffto confirm the work is complete. - Write a concrete outcome — do not re-use generic phrases like "Done" or "Completed". Reference specific files changed, tests added, or commands verified. Example:
"Implemented TokenParser.Parse() method; all 8 token types pass new tests; coverage 82%". - Transition manually — run
arm transition TASK-ID --to done --outcome "..."with the specific outcome. This unblocks dependent tasks and prepares the issue for merge validation.
This is common when workers return from background dispatch without explicit handoff, or when TTL expiration causes a race with the heartbeat mechanism. Recovery is safe — arm transition is idempotent once an issue is already done.
a.2. Semantic Review (Reviewer Dispatch)
Bounded, consolidated review protocol (normative). Review is not an open-ended back-and-forth. Per task:
- One comprehensive initial review — the reviewer reports all findings in one pass, not the first defect found.
- If independent perspectives are used, they run in parallel, each
writing a distinct
.armature/review/path (issue + bundle prefix + reviewer token). Aggregate their chat findings into one list beforearm review record— never a serial review → fix → review → fix chain, and never one shared assessment file. - One consolidated remediation request covering every finding from step 1/2.
The first review runs after the worker has already transitioned to
done. Do not remediate on adone(ormerged) task.isBindingStaletreats any status other thanclaimedorin-progressas stale, so the harness hook passes through: no scope enforcement, no hook heartbeats, and no seconddonedelivery gate on the remediating HEAD. Before dispatching the remediator, reopen and reclaim (workflow step 5 below). After the last remediating commit, the worker runs the full gate and transitions todoneagain. Then refresh every stale review artifact (step 6) before confirmation — do not reuse pre-remediation$TASK_HEAD,$BUNDLE_FILE,$INDEX_OUTPUT, or$RESULT_FILE. Keep$FINDINGS_FILE(the remediating set); it is confirmation scope, not a stale bundle. - One narrow confirmation review, hard-scoped to only the findings that
were remediated; findings outside that scope are recorded but block
further progress only at critical severity. Refresh every stale
review artifact first (workflow step 6), then dispatch with the
same
$FINDINGS_FILEplus an explicit confirmation-scope instruction. A fresh reviewer given only a new bundle/index will repeat a comprehensive review.arm review recordbinds the assessment to the supplied bundle; a stale index still marks pre-remediation entries as head-anchored. - Cap: 3 remediation cycles per task (executable loop: workflow steps 5–7). After each confirmation, inspect the rating. Non-green repeats reopen / slotted-reclaim / remediate / confirm. After cycle 3 still non-green, escalate to the human (Constitution I7) and stop. A non-green confirmation never proceeds to a.3 or merge.
For each task that completed in the wave, dispatch semantic conformance review using task-scoped delivery bundles:
Task-Scoped Semantic Review — each task's review bundle must contain only that task's changes, not the cumulative wave diff. This ensures:
- Scope violations are detected correctly (task didn't modify unrelated files)
- Acceptance criteria are matched to the right task's delivery
- Code quality assessment applies to the right code
- Clear audit trail of which task changed what
Workflow:
Steps 1–7 run per task. At the start of each TASK_ID, unset CYCLE
and recover that task's highest a.2 cycle N/3 note (steps 4 and 5).
Do not carry CYCLE from a previous task. Inside the remedia loop
(steps 5–7 for the same TASK_ID), keep the in-memory CYCLE.
Capture per-task commit ranges — each task was completed in its own isolated worktree on branch
task/TASK-ID(Dispatch Protocol steps 4-5), so the task's commit range is simply that branch relative to the wave's base commit. No commit-message scanning or git-history reconciliation is required, because each task's commits already live on their own branch rather than interleaved on a shared one:declare -A TASK_COMMITS # TASK_ID -> "$WAVE_BASE_SHA..task/TASK-ID" for TASK_ID in $WAVE_TASK_IDS; do if ! git rev-parse --verify "task/$TASK_ID" >/dev/null 2>&1; then echo "ERROR: branch task/$TASK_ID not found. Did the worker commit before returning?" >&2 exit 1 fi TASK_COMMITS["$TASK_ID"]="$WAVE_BASE_SHA..task/$TASK_ID" doneImportant — ordering: at this point the task branches have not yet been merged into the story branch (that happens in step (b) below, which runs after this semantic review and the overlap audit in a.3). Do not substitute
HEADorfeat/STORY-IDfortask/$TASK_IDhere — until the merge in step (b), those refs do not contain the task's commits.Prepare per-task review bundles — use task-specific commit ranges, not wave-combined ranges:
# For each task, capture its delivery diff (task-scoped, not wave-scoped) TASK_BASE="<task's base commit from step 1>" TASK_HEAD="<task's head commit from step 1>" BUNDLE_FILE=$(mktemp) arm review prepare --issue TASK-ID \ --base "$TASK_BASE" --head "$TASK_HEAD" \ --output "$BUNDLE_FILE"This creates a JSON bundle file containing the issue's acceptance criteria, scope, and the diff of only that task's changed files. The bundle is written to
$BUNDLE_FILEfor later use in both the reviewer dispatch and assessment recording steps.
2.1. Activity Index (if bundle has activity section) — when the bundle includes execution evidence:
arm review prepare has no --activity-log or --activity-digest flags — it discovers
the worktree's activity log itself and attaches an activity section to the bundle
automatically when a log is present. Check for it after prepare:
HAS_ACTIVITY=$(jq -r 'if .activity then "yes" else "no" end' "$BUNDLE_FILE")
If HAS_ACTIVITY is yes, dispatch the armature-activity-indexer as a subagent
before dispatching the reviewer:
Dispatch armature-activity-indexer with:
- the bundle file: $BUNDLE_FILE (or at minimum, the bundle's activity.log_path,
activity.digest, activity.delivery_head_count, and activity.earlier_count fields —
read them out with jq if passing the whole file is inconvenient)
The indexer reads the log at activity.log_path, verifies its digest against
activity.digest, and returns an Activity Index JSON (schema_version, log_path,
log_digest, entry_count, delivery_head_count, earlier_count, entries[]) as its
final text output.
Capture the indexer's returned text into a temp file:
INDEX_OUTPUT=$(mktemp)
# The indexer subagent's returned text IS the Activity Index JSON.
# Write it directly to $INDEX_OUTPUT, e.g.:
# echo "$INDEXER_OUTPUT" > "$INDEX_OUTPUT"
# where $INDEXER_OUTPUT is the text returned by the indexer subagent.
The Activity Index is a finding aid only — it summarizes the activity log to help the reviewer locate raw entries by category and exit status. The index itself is never citable; citations must reference raw activity log entry IDs (0-based physical line numbers, e.g. "0", "1" — see the reviewer skill).
Dispatch the armature-reviewer agent — pass both the bundle and activity index (if available). Assign each reviewer a distinct token (
r1,r2, … or that reviewer'sARM_LOG_SLOT) and tell them to write.armature/review/<issue-id>-<bundle-id-8>-<reviewer-token>.json(see the reviewer skill). Do notgit addthose files (local recording input); do not delete existing assessments. Independent perspectives run in parallel under distinct tokens.Dispatch armature-reviewer with: - bundle file: $BUNDLE_FILE (the reviewer reads the bundle from the file) - activity index (if $HAS_ACTIVITY was "yes"): pass the contents of $INDEX_OUTPUT as additional context so the reviewer can route to raw entry IDs - reviewer token: r1 # distinct per parallel reviewer of this issueThe reviewer assesses whether the delivery conforms to the issue contract (acceptance criteria, scope adherence, code quality). For behavioral criteria, execution evidence from the activity log can lift indeterminate verdicts to satisfied or partially satisfied, but it never substitutes for diff citations on implementation criteria and never suppresses a not_satisfied the diff supports.
The reviewer's chat/text response is not the
ConformanceAssessmentJSON. It is rating + actionable findings + the path to the assessment file under.armature/review/(see the reviewer skill's bounded chat contract). After every reviewer returns, collect those paths. Do not write any reviewer's chat text to$RESULT_FILE—arm review recordwill reject a summary as if it were the assessment.Check each reviewer's response shape before collecting anything. A reviewer returns a recordable path only on the success shape. Two shapes deliberately carry no path, and both end in
Assessment: not returned:Validation: failed— the reviewer exhausted itsarm review validateretries. An assessment file may exist on disk, but it never validated.Validation: error—arm review validatefailed operationally (unreadable assessment or bundle path, bundle missing an issue ID, snapshot load failure, issue absent from state, Step 1 bundle preflight, or avalid: falsereport whose only suggestion is to re-runarm review prepare). Nothing recordable was assessed.
For either shape, do not reconstruct or guess a
.armature/review/<issue>-<bundle8>-<token>.jsonpath, do not add one toRESULT_FILES, and do not callarm review recordfor that reviewer. A path you assembled yourself is not a validated assessment, and recording one asserts a review that did not happen. Instead:Track any unrecovered no-path response. A nonempty
RESULT_FILESdoes not authorize recording when a sibling returnedValidation: failedor an unrepairedValidation: error.Validation: error→ repair what the reviewer reported (re-runarm review preparefor a fresh$BUNDLE_FILE; confirm the issue exists in state). After refreshing$BUNDLE_FILE, recomputeHAS_ACTIVITYfrom the new bundle and rebuild$INDEX_OUTPUT(same procedure as step 2.1). IfHAS_ACTIVITYisyes, re-dispatch armature-activity-indexer on this new$BUNDLE_FILEinto a fresh$INDEX_OUTPUT. Ifno, leave$INDEX_OUTPUTunset — do not pass the old index. Drop every path already inRESULT_FILES(those assessments are bound to the old bundle) and re-dispatch every reviewer whose result will be recorded, not only the failed one. Each re-dispatch is once. If the repaired reviewer returns the same shape again, mark it unrecovered and escalate rather than looping.Validation: failed→ the assessment is not recordable. Record the reported failures on the issue, mark that reviewer unrecovered, and escalate to a human (Constitution I7); do not treat the issue as reviewed.
arm note --issue "$TASK_ID" --msg "review not recorded: <shape> from <reviewer-token>; <reason>"If any unrecovered no-path response remains, stop before step 4 even when
RESULT_FILESis nonempty. Do not record the Green siblings, do not synthesize a rating, and do not treat a partial result as Green. Stop and escalate.# Each reviewer writes a DISTINCT path and returns it. Confirm each # file exists and is JSON. Do not share one .armature/review/ file. # Only paths from success-shape responses belong here. RESULT_FILES=(.armature/review/TASK-ID-bundle8-r1.json .armature/review/TASK-ID-bundle8-r2.json) UNRECOVERED=() # append each token that returned a no-path shape that was not repaired if [ ${#UNRECOVERED[@]} -ne 0 ]; then echo "ERROR: unrecovered no-path reviewer(s): ${UNRECOVERED[*]}; escalate (I7); do not record" >&2 exit 1 fi if [ ${#RESULT_FILES[@]} -eq 0 ]; then echo "ERROR: no reviewer returned a validated assessment; escalate (I7)" >&2 exit 1 fi FINDINGS_FILE=$(mktemp) # Union every reviewer's chat findings (not the JSON bodies) into # $FINDINGS_FILE. Deduplicate by finding text. Confirmation uses # this consolidated list as hard scope.Do not call
arm review recorduntil$FINDINGS_FILEholds that single consolidated list. Then record every path in$RESULT_FILES(step 4) — one findings list, every assessment durable.Record every assessment — persist after findings are consolidated.
CYCLEis per-issue. At the start of this task (once, not inside the remedia loop), unset any leftoverCYCLEand recover the highest persisteda.2 cycle N/3note for this$TASK_ID; default 0. RecoveredNis completed remedia cycles. Do not reuse another task'sCYCLE:unset CYCLE CYCLE=$(arm show "$TASK_ID" --format json | jq -r ' [.notes // [] | .[] | strings | capture("a\\.2 cycle (?<n>[0-9]+)/3")? | select(.) | .n | tonumber] | if length == 0 then 0 else max end ') CYCLE_FOR="$TASK_ID" # Derive ratings first, then record least-conservative → most-conservative # so the last AssessmentAttestation (what arm show reads) matches $RATING. # RESULT_FILE = most conservative path (red > yellow > green). Ties: first # of that rating. Prefer .rating if present; else derive from results # (any not_satisfied => red; else any partially_satisfied or # indeterminate => yellow; else green). greens=() yellows=() reds=() best_rank=0 RESULT_FILE="${RESULT_FILES[0]}" RATING="" for f in "${RESULT_FILES[@]}"; do test -s "$f" || { echo "ERROR: assessment missing or empty: $f; do not enter a.3" >&2; exit 1; } rating=$(jq -r ' if .rating then (.rating | tostring | ascii_downcase) elif (.results | type) == "array" then if any(.results[]; .status == "not_satisfied") then "red" elif any(.results[]; .status == "partially_satisfied" or .status == "indeterminate") then "yellow" else "green" end else empty end ' "$f") case "$rating" in red) rank=3; reds+=("$f") ;; yellow) rank=2; yellows+=("$f") ;; green) rank=1; greens+=("$f") ;; *) echo "ERROR: $f has no .rating/.results; read that reviewer's chat Rating: Green|Yellow|Red and set RATING; do not enter a.3" >&2 exit 1 ;; esac if [ "$rank" -gt "$best_rank" ]; then best_rank=$rank RESULT_FILE="$f" RATING="$rating" fi done if [ -z "$RATING" ]; then echo "ERROR: no assessment rating derivable; do not enter a.3" >&2 exit 1 fi SORTED_RESULT_FILES=("${greens[@]}" "${yellows[@]}" "${reds[@]}") for ASSESSMENT in "${SORTED_RESULT_FILES[@]}"; do arm review record --issue "$TASK_ID" --assessment "$ASSESSMENT" --bundle "$BUNDLE_FILE" \ || { echo "ERROR: review record failed for $ASSESSMENT; do not enter a.3" >&2; exit 1; } donePass each
--assessmentpath and--bundle "$BUNDLE_FILE"as file paths (not raw JSON content) so each recorded assessment is bound to the exact bundle (and its durable identity) the reviewer evaluated.Loop-control rating is
$RATINGfrom the conservative$RESULT_FILEabove. An empty$RATINGalready exited. Green: this task is done with a.2 — skip steps 5–7. Yellow or red: if recoveredCYCLEis already 3, escalate (step 7's cycle-3 branch) and do not remedia. Otherwise setCYCLE=$((CYCLE + 1))(recoveredNis completed; next remedia isN+1) and enter the remedia loop at step 5. Do not start a.3 or merge on a non-green rating.Reopen and reclaim before remediating. Semantic review runs after the worker has already transitioned to
done. The remediator must re-enter the live-claim lifecycle before writing. Do this before any remediating edit, and beforearm merged(merged is terminal —arm reopenrefuses it).applyHeartbeatrequiresop.WorkerID == issue.ClaimedBy. ClaimedBy is the slotted identity written at claim time (<worker-id>~<slot>). Claim as the remediator: setARM_LOG_SLOTon the same invocation asarm claim(prefix form below). Do notexportthen claim in a later tool call — a fresh-shell harness drops the export and claims unslotted. Prefix keeps later coordinator ops on the unslotted log with nounset.REMEDIATOR_SLOTmust be unique across the wave (I3). Default:rem-${TASK_ID}— self-contained, unique per task, valid inARM_LOG_SLOT(^[A-Za-z0-9_-]+$). Do not invent a sharedt1and do not rely on a remembered original dispatch slot. Two concurrent remediations must not share one slotted op log.If
CYCLEis not bound to this$TASK_ID(unset, or leftover from another task), unset it and recover with the step-4arm show/ jq snippet before claiming. Do not unset on remedia-loop re-entry for the same task — that would drop an in-memory increment. If recoveredCYCLEis already 3, skip to step 7's cycle-3 escalate branch — do not remedia again.if [ "${CYCLE_FOR:-}" != "$TASK_ID" ]; then unset CYCLE CYCLE=$(arm show "$TASK_ID" --format json | jq -r ' [.notes // [] | .[] | strings | capture("a\\.2 cycle (?<n>[0-9]+)/3")? | select(.) | .n | tonumber] | if length == 0 then 0 else max end ') CYCLE_FOR="$TASK_ID" if [ "$CYCLE" -ge 3 ]; then CLAIMED_BY=$(arm show "$TASK_ID" --field claimed_by) arm note --issue "$TASK_ID" --msg "a.2 cycle 3/3 recovered; escalated (I7); claim=${CLAIMED_BY:-unset} worktree=.worktrees/$TASK_ID" echo "ERROR: recovered cycle $CYCLE; escalate I7; do not remedia / enter a.3" >&2 exit 1 fi CYCLE=$((CYCLE + 1)) fi # Unique per wave task; rem-${TASK_ID} needs no prior slot memory (not literal t1). # Run this block as one shell invocation. ARM_LOG_SLOT is a one-shot prefix # on claim — do not split the prefix and arm claim across tool calls. REMEDIATOR_SLOT="rem-${TASK_ID}" arm reopen "$TASK_ID" ARM_LOG_SLOT="$REMEDIATOR_SLOT" arm claim "$TASK_ID" --ttl 120 --worktree CLAIMED_BY=$(arm show "$TASK_ID" --field claimed_by) BASE_ID=$(arm worker-init --check | awk '/^Worker ID:/{print $3}') EXPECTED="${BASE_ID}~${REMEDIATOR_SLOT}" if [ "$CLAIMED_BY" != "$EXPECTED" ]; then echo "ERROR: ClaimedBy=$CLAIMED_BY expected $EXPECTED (ARM_LOG_SLOT dropped before claim?)" >&2 exit 1 fi arm render-context "$TASK_ID" --format agentarm claim --worktreereuses the existing.worktrees/TASK-IDcheckout and rewrites itsarmature-issue-idbinding; do notgit worktree adda second tree. Then dispatch the remediator like any other pre-claimed worker (Dispatch Protocol) withexport ARM_LOG_SLOT=$REMEDIATOR_SLOTas its second instruction, stating only what changed. The remediator iterates on the fast gate, commits, runs the full/publish gate at the new delivery HEAD, and runsarm transition TASK-ID --to doneso the delivery gate evaluates that HEAD. Do not dispatch remediations onto adoneormergedtask.Confirmation after remediation — one protocol. After the remediator commits and transitions to
done,task/$TASK_IDhas a new delivery HEAD. Refresh every stale artifact, keep the remediating findings as scope, then dispatch confirmation (not another comprehensive review):unset RESULT_FILE INDEX_OUTPUT # Do not unset FINDINGS_FILE — it is the confirmation scope. TASK_COMMITS["$TASK_ID"]="$WAVE_BASE_SHA..task/$TASK_ID" TASK_HEAD=$(git rev-parse "task/$TASK_ID") BUNDLE_FILE=$(mktemp) arm review prepare --issue "$TASK_ID" \ --base "$TASK_BASE" --head "$TASK_HEAD" \ --output "$BUNDLE_FILE" HAS_ACTIVITY=$(jq -r 'if .activity then "yes" else "no" end' "$BUNDLE_FILE")If
HAS_ACTIVITYisyes, re-dispatch armature-activity-indexer on this new$BUNDLE_FILEinto a fresh$INDEX_OUTPUT(same procedure as step 2.1). Ifno, leave$INDEX_OUTPUTunset — do not pass the old index. Then dispatch:Dispatch armature-reviewer in confirmation mode with: - bundle file: $BUNDLE_FILE - activity index (if $HAS_ACTIVITY was "yes"): $INDEX_OUTPUT - findings scope: $FINDINGS_FILE (the consolidated remediating set from the initial review) - reviewer token: confirm-$CYCLE # expands; distinct from first-pass tokens - instruction: hard-scoped confirmation of those findings only; do not start a new comprehensive review. Out-of-scope findings are recorded but block only at critical severity.After the confirmation reviewer returns, apply the same response-shape branch as initial collection before assigning
$RESULT_FILE:- Success shape → assign the returned path (token
confirm-$CYCLE) and record it against the refreshed bundle. The refreshunsetdropped the first-pass path so it cannot be reused. Validation: error/Validation: failed/Assessment: not returned→ do not treat the chat as a filename, do nottest -sa guessed path, and do not callarm review record. Route as in step 3: repair-once (and re-dispatch every reviewer whose result will be recorded) forValidation: error;arm noteand I7 escalate forValidation: failed.
# Only after a success-shape response (distinct token confirm-$CYCLE). RESULT_FILE="<path from confirmation reviewer success chat>" test -s "$RESULT_FILE" || { echo "ERROR: confirmation assessment missing" >&2; exit 1; } arm review record --issue "$TASK_ID" --assessment "$RESULT_FILE" --bundle "$BUNDLE_FILE" \ || { echo "ERROR: review record failed for $RESULT_FILE; do not enter a.3" >&2; exit 1; }Reusing the pre-remediation bundle lets a green confirmation attest the old delivery SHA, fingerprints, and diff. Reusing the pre-remediation index omits post-remediation evidence and routes the reviewer to entries whose
head_shais not the new bundle head. Reusing the first-pass$RESULT_FILErecords the first-pass assessment against the new bundle. Omitting$FINDINGS_FILE(or relying on a prior reviewer transcript) turns confirmation into a second comprehensive review and restarts the discovery/remediation loop. Step 7 inspects this recorded confirmation — do not fall through to a.3 from here.- Success shape → assign the returned path (token
Inspect confirmation rating; loop or escalate. After the confirmation
arm review recordin step 6, derive$RATINGfrom that$RESULT_FILE(.resultsonly — assessments have no.rating), persist the cycle, and gate on the rating:RATING=$(jq -r ' if (.results | type) == "array" then if any(.results[]; .status == "not_satisfied") then "red" elif any(.results[]; .status == "partially_satisfied" or .status == "indeterminate") then "yellow" else "green" end else empty end ' "$RESULT_FILE") if [ -z "$RATING" ]; then echo "ERROR: confirmation rating empty; read chat Rating: Green|Yellow|Red then set RATING; do not enter a.3" >&2 exit 1 fi if [ "${CYCLE:-0}" -eq 0 ]; then CYCLE=1; fi # Persist the cycle just completed only. Recovery takes max N; a pending # next-cycle note would make a fresh context escalate after 2 remediations. arm note --issue "$TASK_ID" --msg "a.2 cycle $CYCLE/3 rating=$RATING" case "$RATING" in green) ;; yellow|red) if [ "$CYCLE" -ge 3 ]; then CLAIMED_BY=$(arm show "$TASK_ID" --field claimed_by) arm note --issue "$TASK_ID" --msg "a.2 cycle 3/3 $RATING; escalated (I7); claim=${CLAIMED_BY:-unset} worktree=.worktrees/$TASK_ID" echo "ERROR: cycle $CYCLE still $RATING; escalate I7; do not enter a.3 / arm merged" >&2 exit 1 fi CYCLE=$((CYCLE + 1)) echo "NON-GREEN: cycle $CYCLE $RATING; replace FINDINGS_FILE; repeat steps 5-6; do not enter a.3" >&2 exit 1 ;; *) echo "ERROR: unknown confirmation rating '$RATING'; do not enter a.3" >&2 exit 1 ;; esacNon-green always stops the snippet (
exit 1); linear fall-through to a.3 is only for green. Do not re-derive or re-increment after the snippet exits.- Green:
;;— this task is done with a.2 and may proceed to a.3. Do not remediate again. Residual risk: confirmation-green means the remediating set was fixed, not that the task is comprehensively clean. Remediating edits can introduce regressions that stay invisible unless they are critical (confirmation mode: out-of-scope findings block only at critical severity). That is the T1 trade-off; keep the loop as specified. - Yellow or red, and
CYCLEwas < 3: the snippet persisted the completed cycle, incrementedCYCLEin memory only, and exited 1. The agent remediates: replace$FINDINGS_FILEwith the confirmation's remaining findings (the new remediating set) and repeat steps 5–6. AtCYCLE2, auto-escalate remediator effort to high (Dispatch Protocol). - Yellow or red, and
CYCLEwas already 3: the snippet noted the surviving claim and worktree, then exited. The agent escalates to the human (Constitution I7). Do not enter a.3, do not merge this task branch, do not runarm mergedfor this task.
Confirmation-green tasks may proceed to a.3 / merge individually. Cycle-3 escalated tasks stay out of a.3,
arm merged, and branch merge pending the human (I7). The wave is not stalled on one- Green:
…(truncated)