forgejo-mcp/.claude/commands/team/dev-loop.md
Christoph Görn 3a73026b16
chore: 🔧 add Claude Code agent team infrastructure for multi-agent workflows
- Add 6 agent definitions (devils-advocate, implementer, proponent,
  research-lens, spike-runner, verifier) for specialized AI team workflows
- Add 4 team slash commands (battle-test, debate, dev-loop, spikes)
- Add openspec-journal.py processor + pre-compaction hook script
- Update settings.json and .mcp.json for agent team support
2026-05-24 08:06:44 +02:00

22 KiB
Raw Permalink Blame History

name description category tags
Team: Dev Loop Spawn an implementer + verifier (+ optional planner) team that iterates on a scoped change until a deterministic check passes. Lead stays light. Agent Teams
agent-teams
implementation
build-loop

Implementation team with a deterministic verifier in the loop. Use when you have a scoped change that can be proven done by running a tool (tests, linter, typecheck, schema check, build). Not for review or design work — see team:debate and team:battle-test for those.

Input ($ARGUMENTS): a free-form scope description. The lead reads it, inspects the relevant files, and decides whether a planner round is needed.

When this fits

  • Refactor with tests as the contract.
  • New feature where the slice is small and the check is automated.
  • Migration where the verifier is "old and new produce the same output".
  • Anything where success collapses to a single shell command exiting 0.

If the verifier is "looks good to me" — wrong skill.

The team can also start before the slice is fully defined. Spawn planner first, ask "what's the next sensible slice here?", then derive impl tasks from the planner's answer. impl and runner sitting idle while planner reads the codebase is fine — they cost ~nothing while idle.

Lead does not gate-keep teammates

When the user gives a clear instruction for a specific teammate — "send this command to the planner", "have impl do X", "ask runner to re-run" — forward it. Do not second-guess whether the activity "fits the loop shape". The teammates have their own judgment. If the planner is asked to do orientation work or run a catchup command, that is planner work, not a violation of the skill.

Refuse only when the instruction would break a hard rule (lead writes code, runner edits files, scope wall crossed, etc.). Everything else routes through.

Pre-flight (lead)

  1. Confirm CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 is set. If not, refuse.
  2. Read the files in scope end-to-end. The lead must understand the domain well enough to mediate when something goes off the rails.
  3. Identify the verifier command. It must exit 0 on success, non-0 on failure. Smoke-test it once before spawning the team.
  4. Decide: is the slice clear enough to brief impl directly, or do you want a planner round first? Only spawn the planner when you can't confidently shape the work yourself — opus tokens are the expensive ones.

Roles

Name subagent_type model Tools Job
planner (optional) Plan opus Read, Grep, Glob, WebFetch Single-shot: propose the slice, name what to test/build, hand back.
impl implementer sonnet Read, Edit, Write, Bash Claim impl tasks in order, write code, ping runner when ready.
runner verifier haiku Read, Bash Run the verifier command, report PASS or FAIL excerpt. Never edits.

Spawn via the Agent tool with team_name, name, subagent_type, and model. The subagent types above are pre-installed; check the Agent tool's own list before assuming a type exists.

Mechanics — what does the work

Use the team task list (TaskCreate / TaskUpdate / TaskList), not your prose, to choreograph. The point is that addBlockedBy makes the runner self-trigger when impl finishes, with no polling from the lead.

  1. TeamCreate with a short team_name.
  2. Create one task per logical impl step. Last task: "Run verifier and report". Use addBlockedBy to chain.
  3. Spawn impl and runner (and planner if needed). Each spawn prompt should reference: their task IDs, the communication protocol below, the file boundaries, and "do not touch X".
  4. After spawn, the lead stops until a teammate sends a real message. Idle notifications are not events to react to.

Communication protocol (bake into spawn prompts)

  • impl finishes its tasks, then SendMessage runner: "Phase N ready. Run task #X."
  • runner runs the verifier:
    • PASS → SendMessage impl "PASS — <one line>. Output tail: ...", mark runner's task completed, go idle.
    • FAIL → SendMessage impl with exit code, failing test names, last ~40 lines. Keep status in_progress. Go idle.
  • impl on FAIL: fix, then SendMessage runner "Fixed: <one-liner>. Re-run task #X."
  • Loop until PASS.

The lead is not in this loop. If the loop stops moving (both idle, no PASS), then the lead investigates.

Mandatory checkpoint rule for impl (bake into impl spawn prompt)

Long phases hit per-turn budgets. When that happens, impl finishes its turn gracefully — which from the harness's view is "idle" — but it has not handed off to runner. The watchdog can't tell that apart from a dead agent. Defend against this in the impl prompt:

After every 2 sub-tasks completed: SendMessage lead with one line "P progress: done, on ". Never end a turn idle without first sending a message. If you have nothing else to say, send "Still working on X.Y, will checkpoint after next sub-task." Silence is the only failure signal the lead has — silent ≈ replaced.

Also bake routing into the impl prompt explicitly:

When you write "Run task #N" or "Phase N ready", the recipient is runner, NOT team-lead. Lead is not the verifier — runner is.

Without these two clauses, expect repeat failures: silent stalls and work-product messages mis-routed to lead.

Worker discipline (bake into spawn prompts)

Three rules every spawn prompt must include verbatim. Workers ignore vague guidance, so the exact wording matters.

1. Canonical task IDs only. Never call TaskCreate. The lead creates tasks once and hands you the IDs in the spawn brief. You only call TaskUpdate(taskId=<id>, status=in_progress|completed). Creating a new task with the same name as an existing one breaks the dependency chain (the verifier blockedBy the original ID never unblocks) and forces lead cleanup. Past-run symptom: impl creates #21 "P2.1" while #14 "P2.1" is already assigned to it; #7 verifier blockedBy [1-6] then sits forever because impl ticked #21 not #14.

2. Stale inbox rule. When you wake, your inbox may contain old messages from before the team's current state. Read once, identify the MOST RECENT message from team-lead, act ONLY on that. Discard older lead instructions, idle notifications, peer-DM summaries. Replying to an obsolete FAIL or "begin Phase N" after the team has already moved on burns a turn, sends contradictory state into the loop, and triggers unnecessary lead intervention. Symptom: impl reporting "fixed!" for a problem the team solved 10 minutes ago.

3. (Runner only) Exact trigger phrase. You run the verifier ONLY when the active impl sends a message whose body contains the literal phrase "Phase N ready. Run task #<id>". None of the following are triggers: idle notifications, peer-DM summaries that mention "ready" or "PASS", lead messages that route or inform, your own task becoming blockedBy:[]. The blockedBy field is the contract: if your task is blocked, do not run, period. Premature runs produce vacuous-PASSes the lead must revert; in past sessions, three premature runs = replacement.

Verifier filter pitfall

vitest -t <word> matches by the test-suite name impl chose, not by the spec vocabulary the lead picked. Past run: lead picked -t fingerprint from tasks.md text; impl named the suite "fetchAndFingerprint" — the filter matched zero tests but vitest exit-coded 0 (no failures). Lead treated it as PASS; impl's actual unit tests had never run.

Mitigations, in order of preference:

  • File-path filter when the new test file path is known: vitest run packages/foo/__tests__/new_file.test.ts. Most precise.

  • Broad -t alternation when test names span variants: -t "fingerprint|drift|fetch". Cast the net wide.

  • Always pair with the vacuous-pass guard in the runner prompt:

    If vitest reports Tests 0 passed or no test files matched the filter, that is a FAIL, not a pass. The phase introduces new test files; if vitest finds zero matches, the test files were not created or the filter is wrong. Report FAIL with "Verifier matched zero tests — test files missing or filter mismatched."

The vacuous-pass guard is non-negotiable for any phase that adds new test files. Without it, the verifier loop has no signal that the new tests exist at all.

Long-running runs: heartbeat + watchdog

For runs that span more than a few phases (overnight, large OpenSpec changes), the lead needs an external heartbeat. The dev-loop has no built-in keepalive — if a teammate goes silent mid-phase and the user isn't watching, nothing kicks the loop forward.

Pattern that works: ScheduleWakeup at 270s with a watchdog prompt that:

  1. TaskList — find lowest-id in_progress task. Note id + owner.
  2. Compare to last tick (read your own previous user-facing line: Watchdog: stuck on task #N owner=<name>). Owner change resets the stuck count.
  3. If team advanced (different task id or owner, or all completed): re-arm and emit one-line status.
  4. If same task id AND same owner two ticks running:
    • Tick 1 (1st probe): SendMessage owner asking for one of working on X.Y / blocked: Z / done, pinging runner. Re-arm.
    • Tick 2 (still silent): shutdown owner, TaskUpdate task back to pending with new owner name (impl<N+1>), spawn replacement, SendMessage runner the new active impl name, re-arm.
  5. Owner=runner stuck: 3-tick threshold (verifiers can be slow).
  6. All tasks completed: do NOT re-arm. Final summary, suggest next.

Why 270s and not 300s: prompt cache TTL is 5 min. Picking 300 burns the cache miss without amortizing it; 270 keeps cache warm tick-to-tick.

The freshness signal is TaskList (status transitions are the only state every teammate is required to update). Do not lean on the journal — teammates may forget to journal but cannot make the chain advance without TaskUpdate.

False positives are real: a single 4-min silence can be a legitimate long edit. Two consecutive 4-min silences (~9 min total) on the same task is a real stall. The two-tick rule trades latency for accuracy.

Replacing a stuck teammate

When watchdog rule 4 fires, or the user manually orders a replacement:

  1. SendMessage old teammate {type:"shutdown_request"}. Do not wait for the approval. The shutdown handshake is async and can take many minutes — sometimes longer than the rest of the run.
  2. TaskUpdate the in-progress task: status=pending, owner=impl<N+1> (next free number — check task history; impl, impl2, impl3, ...).
  3. Agent spawn with subagent_type=implementer, model=sonnet, name=impl<N+1>, team_name of the live team. The replacement brief must say:
    • "You replace impl. Read tasks.md and git diff to see partial work. Continue task #."
    • The full checkpoint rule and routing rule from the impl prompt.
    • The three worker-discipline rules (canonical IDs, stale-inbox, trigger-phrase) verbatim — these are the rules predecessors broke.
    • "Predecessors were replaced for going silent. Your survival depends on regular SendMessage checkpoints."
  4. SendMessage new teammate: "Begin. Task #<id> is yours." (the spawn does not auto-trigger a turn; you have to nudge.)
  5. SendMessage runner: "Active impl is now impl<N+1>."

Async-shutdown caveat: the old teammate may wake up after the replacement has done useful work and try to act on stale messages. Two mitigations:

  • The replacement is impl<N+1> with a different name, so messages addressed by name don't collide.
  • If the old teammate's eventual reply arrives and contradicts the new state ("task #X done!" when the replacement just re-did it), prefer the replacement's work — it's the live one. The old teammate's claim may be a stale message from before its silence began.

Be aware of false-positive replacements: if the old teammate was actually alive and slow (a single long-running edit can stretch past 9 minutes on a docs-heavy phase), you'll have spawned a redundant replacement. When the original surfaces with a "done" message, shutdown the redundant one and keep the original active. This happens; design for it.

Lead discipline (how tokens are saved)

  • Write nothing. No code, no recipes, no docs. If you catch yourself reaching for Edit, you are doing impl's job.
  • Don't poll. No periodic TaskList calls. The team task list is for the teammates; the lead reads it only on suspicion of a hang.
  • Don't echo. Teammate messages are already rendered to the user. A one-line ack is enough.
  • Don't answer idle notifications. They are not requests for input.
  • Intervene only on: scope violation, >3 fix rounds on the same fail, impl claiming PASS without runner confirmation, or a teammate going off-mission.

Reviews arriving mid-slice route through the team

When a code review surfaces during a live team — most commonly the user running /plannotator-review after a runner-PASS, but also PR comments, inline gh review threads, or "ich habe folgendes gefunden"-style user feedback — the lead does not handle it directly. The team handles it.

Default flow:

  1. Planner receives the raw review payload (the markdown blob, the PR comment thread, the user's verbatim text — not the lead's pre-classification). Asked to triage each comment as push-back (architectural correctness, the reviewer's intuition is wrong) / fix (small edit) / refactor (larger change), and return a knappe mapping table per comment with proposed response and target file paths.
  2. Implementer receives the planner's mapping. Edits the files.
  3. Runner re-verifies. If the diff is purely comments / spec wording / type-aliases with no behavioral change, the lead may downgrade the verifier scope (e.g. typecheck + openspec validate only) — but runner still runs it, not the lead.
  4. Lead acks each step in 1–2 lines and writes the user-facing summary at the end.

Why: the same lead-discipline rules that protect tokens during the slice apply during the review-response. A plannotator round usually adds 3–10 small edits; doing them as lead burns opus on sonnet-level work and leaves three paid-for agents idle. The trigger to delegate is the team is alive, not the change is large.

Self-edit only when the team has already been disbanded (TeamDelete).

The architectural-pushback case is the trap: it feels like reasoning work, which feels like lead work. It's not — pushback still ends in either a small spec/comment edit (impl) or a verbal answer (planner-drafted, lead relays). If the planner classifies a comment as push-back, the planner also drafts the citation-backed reply; the lead doesn't re-derive it.

Scope walls

Bake the file boundaries into the impl spawn prompt:

Stay in: <list of paths>. Do not touch: <list of paths>. Do not modify production source unless a real bug is uncovered during verification — surface it, do not silently fix.

The runner's prompt: Read-only + Bash. You do not edit files. Always run the verifier through the canonical command, never piecemeal sub-commands. Also: the implementer's name may change mid-run if a replacement was spawned (impl → impl2 → impl3, etc.). Address impl-direction messages to whichever name the lead most recently named, or to whoever currently owns the impl tasks per TaskList — never to a name you've held since spawn-time.

Verifier coverage caveats

A passing verifier means "what's tested passed", not "everything that was supposed to be done was done". Two real failure modes:

  • Missing test files don't fail. If sub-task 9.4 was "add a test for X" and impl forgets, cargo test happily passes — there's no test to fail. The verifier reports green; the gap is invisible.
  • Unticked checkboxes don't fail. OpenSpec/tasks.md checkbox state isn't part of most verifiers. impl can mark a phase "done" with sub-tasks unticked.

Defend in impl spawn prompt: "for each numbered sub-task you complete, produce evidence — a test name, a file path, or a git diff hunk — and tick the checkbox in tasks.md before moving on. Lead may spot-check at phase boundaries."

Lead spot-check at phase end is cheap: grep -c '^- \[x\]' vs ^- \[ \] on tasks.md catches the second class. Reading the new test names against the task list catches the first.

Running this loop on Codex

Codex does not currently expose Claude Code agent-team primitives. There is no TeamCreate, shared team task list, teammate mailbox, split-pane teammate UI, teammate-to-teammate messaging, or ScheduleWakeup equivalent in the Codex tool surface. Do not pretend this command can run unchanged.

What Codex can do is a lead-orchestrated approximation using subagents:

  • spawn_agent creates bounded worker or explorer subagents.
  • send_input redirects or nudges a spawned subagent.
  • wait_agent waits for one or more spawned subagents to finish.
  • close_agent shuts down a subagent when it is no longer needed.

The important behavioral difference: Codex subagents report to the lead. They do not coordinate directly with each other. The lead owns the task list, handoffs, verifier loop, and final synthesis.

Codex lead recipe

Use this shape when the user explicitly asks to try the dev loop on Codex:

  1. Read the scoped files and identify the deterministic verifier command.
  2. Smoke-test the verifier once locally before spawning agents.
  3. If the slice is unclear, spawn one explorer as planner with a read-only prompt. Ask it for a small task breakdown, file boundaries, and verifier command. Wait for it before implementation.
  4. Spawn one worker as impl for implementation. Give it:
    • Exact owned files or directories.
    • "You are not alone in the codebase; do not revert edits made by others."
    • The numbered implementation tasks.
    • The verifier command, but tell it not to treat self-run verifier output as final unless the lead asked for that.
    • The evidence rule: for each sub-task, report file paths, test names, or diff hunks that prove completion.
  5. While impl works, do not edit the same files locally. Only inspect unrelated context or prepare the verifier.
  6. When impl returns, inspect its changed paths and run the verifier locally as the runner. Codex does not need a separate read-only runner unless the verifier is expensive and can run in parallel with lead work.
  7. On verifier FAIL, send the failing excerpt back to impl with send_input. Keep the loop tight: exit code, failing test names, and the last useful output lines.
  8. Repeat until the verifier passes or the same failure class repeats three times. At three repeats, stop and investigate as lead.
  9. Close the subagent once no more follow-up is needed.

Optional Codex runner

Spawn a separate explorer as runner only when it adds real value:

  • The verifier is long-running and the lead can do useful non-overlapping inspection meanwhile.
  • The verification requires independent read-only review of artifacts.
  • You want a second agent to check whether the implementation actually satisfied each numbered task, not just whether tests pass.

The runner prompt must say: "Read-only. Do not edit files. Run only the verifier command requested by the lead. Report PASS or FAIL with the exit code and a short output excerpt." Because Codex subagents do not message each other, the lead forwards runner failures to impl.

Codex watchdog substitute

Codex has no scheduled wakeup. If a subagent appears stuck, the lead should:

  1. wait_agent with a bounded timeout only when the result is needed.
  2. If it times out, continue non-overlapping work if possible.
  3. If progress is required, send_input one direct checkpoint request: "Report status: working on X, blocked by Y, or ready for verification."
  4. If the subagent remains silent and the task is still needed, close it and spawn a replacement worker with the instruction: "Read git diff and continue from the partial work; do not revert predecessor changes."

Replacement is a lead decision. There is no shared ownership field to update, so record the live owner in the lead's own notes or user-facing status.

Cleanup

When runner reports PASS and all tasks are completed:

  1. SendMessage each teammate with {type:"shutdown_request"}. Wait for approvals.
  2. TeamDelete once both have shut down.

Confirm the result with the user before cleanup. They may want a follow-up phase, in which case keep the team alive and queue more tasks.

A minimal example shape (not a template — illustrative)

Scope: "Replace axios with native fetch across src/api/. Public function signatures stay the same. Verifier: npm test && npx tsc --noEmit."

Tasks:

  1. impl: rewrite src/api/client.ts to use fetch; preserve exports.
  2. impl: update call sites in src/api/*.ts; remove axios import.
  3. impl: drop axios from package.json dependencies.
  4. runner: npm test && npx tsc --noEmit — PASS or FAIL.

Task #4 addBlockedBy: [1, 2, 3]. impl pings runner when 1–3 done. On FAIL the runner posts the failing test name and the tsc error excerpt; impl fixes; runner re-runs. Done when #4 is green.

A Rust variant looks the same: scope = "migrate error_chain to thiserror in crates/foo", verifier = cargo check --all-targets && cargo test -p foo. Same three roles, same protocol.

Hard rules

  • Verifier is deterministic. Subjective review belongs in team:debate.
  • Lead stays text-light. Long lead replies are a smell that you are doing the team's job for them.
  • No nested teams. Teammates cannot spawn their own team.
  • Permissions inherit at spawn. Set the lead's permission mode appropriately before TeamCreate.
  • Cleanup belongs to the lead. Teammates must not call TeamDelete.

Argument: $ARGUMENTS