Agent Council — Multi-Model Swarm Debate & Pair-Architect Collaborator — Put several models on the problem and keep the argument, not just the answer.
Seat 2–6 AI agents around one design question, patch or diff: local claude personas out of the box, plus codex, gemini, aider or your own OpenAI, Anthropic, xAI/Grok, Gemini, OpenRouter, DeepSeek and Ollama keys. They propose blind, cross-examine each other anonymously, pair-program, compete on a rubric or review your code. A Chair returns the consensus, the minority report, a verdict and an action plan, inside the time limit and budget you set, with a one-file HTML transcript.
SKU: agent-council · Skills: agent-council-setup + agent-council · Runtime: 2,801 lines stdlib Python · Price: $39.00 USD one-time · card, Apple Pay or Google Pay
What It Does
- 2–6 configurable seats (
--seats&--discover): works out of the box on Grok Bot's computer with localclaudemulti-persona seats (architect,security,skeptic,pragmatist,driver,navigator, plusperformanceandreviewer), the localcodex,geminiandaiderCLIs, and optional BYOK HTTP seats (OpenAI,Anthropic,xAI/Grok,Gemini,OpenRouter,DeepSeek,Ollama).--discoverlists what your computer can field and which keys are set, never their values. - 4 modes (
deliberate,pair,compete,review): blind round 1 parallel proposals; anonymized round 2+ delta cross-examination (Seat A,Seat B); a Driver + Navigator pair-programming loop ending inAPPROVE/REQUEST_CHANGES/BLOCK; SwarmSolve-style rubric competition with peer grading and a deterministic winner; and deduplicated, severity-ranked code and diff review (CRITICAL..INFO) with file:line citations. - Unthrottled reasoning, structural token efficiency: output budgets are left at the model's native ceiling (Anthropic defaults to
16384tokens with extended thinking;reasoning_effort: highwhere supported). Tokens are saved by structure instead: one pinned brief so prompt caches hit, compact line-range slicing (--files path:1-120,--git-diff) with automatic secret redaction, delta-only round forwarding, and--early-stopon consensus. - Timers, deadlines & mid-flight steering:
--seat-timeout,--total-deadlineand--quorumwith gracefulDEGRADED_TIMEOUTfallback; stateful session steering (--session ID,--status,--pause,--inject "note",--resume); a--max-cost-usdbudget guard; and a one-click, self-contained dark-slate HTML council viewer (--html-out).
How It Runs (6 Steps)
.wattcoin_license_check.py check --sku agent-council must return "state": "OK" before every run.agent_council.py and bot_common.py against the pinned values; any mismatch stops the run.--discover picks available seats; the prompt plus line-numbered, redacted --files / --git-diff context becomes one cache-friendly brief.Seat A/Seat B, plus any injected moderator notes; early stop on convergence, budget or deadline.Real Sample Output: 3-seat deliberation on bot_common.http_json
Sample disclosure: this is a representative sample, not a customer report. It was produced against public or fictional test targets; account and credential values are placeholders or redacted (for example wck_<REDACTED>), and no private keys, seed phrases or secrets are ever shown. Your own report will differ in content and length.
Excerpt from a live 2-round deliberate council run on 2026-10-27 with three local claude seats (architect, security, skeptic) over /workspace/lib/bot_common.py lines 85–177. Seat text is unedited; the omitted parts are marked. Exit 2: the Chair kept one HIGH item in the minority report at the default --fail-on medium. Total: 7 calls, about 71k tokens, an estimated $0.95 at the claude CLI's list price (normally covered by a subscription).
# Agent Council Report - deliberate
```
snapshot 2026-10-27 20:54:29 UTC | tool=agent-council
endpoints claude
```
| Field | Value |
|---|---|
| Mode | deliberate |
| Session | sample-live-20261027-165133 |
| Rounds | 2 of 2 |
| Converged | True |
| Early stop | False |
| Quorum | 3/3 active (need 2) |
| Elapsed | 176.2s |
| Stop reason | - |
| Exit code | 2 (fail-on medium) |
## Problem statement
> Architecture & security review: we plan to reuse bot_common.http_json (shown) as the transport for metered, BYOK LLM API calls (OpenAI/Anthropic/xAI) inside paid BotAppStore tools, and keep exit_code() as the CI contract. Is that safe and correct? Consider retry/backoff semantics for non-idempotent paid POSTs, timeout behaviour, error-body handling (could API keys or prompts leak into logs?), MAX_RAW truncation of large JSON responses, and whether exit_code() can ever report a clean pass for a degraded run. Recommend concrete, minimal changes.
## Context bundle
- `/workspace/lib/bot_common.py` lines 85-177 of 274, sha256 `d1941b61b95ca664`
- redactions: none; skipped: none
## Seats
| Seat | Role | Backend | Model | Status |
|---|---|---|---|---|
| A | Architect | claude | (default) | OK |
| B | Security | claude | (default) | OK |
| C | Skeptic | claude | (default) | OK |
| Chair | Judge | claude | (default) | OK |
[... Round 1 (three blind proposals) and Round 2 Seats A-B omitted; Seat C's cross-examination shown ...]
## Round 2 - cross examination
### Seat C - OK - 48960ms, 5678 in / 4778 out tokens, $0.1410
**Position:** Do not reuse http_json unchanged for metered BYOK LLM POSTs; keep it for idempotent GETs and route paid calls through a thin llm_call()/idempotent=False policy that: (1) retries only on proven-not-accepted outcomes (429, 503, 529, and pre-send connect failures such as DNS/ECONNREFUSED/TLS handshake), never on read timeouts, resets after send, or 500/502/504; max 2 retries, Retry-After honored (with a 1-2s floor plus jitter when absent) inside a total wall-clock deadline; (2) uses a configurable long per-call timeout (120-600s) instead of 20s; (3) does a bounded read(cap+1) for both success and error bodies, with an LLM-sized cap, and treats truncated or non-JSON 2xx as errors (.error set, .truncated=True), never status 200 with error=None and {'_raw':...}; (4) stores no raw completions or request headers in HttpResult, Finding.evidence or logs, keeps only the provider error type/code/message, and scrubs sk-/sk-ant-/xai-/Bearer patterns as a backstop; (5) disables redirects and enforces https, with an optional user-configured base_url allowlist rather than a hardcoded provider list; (6) derives degraded automatically from any errored or status<0 result and records attempts and possibly-billed attempts. Keep exit_code() as the CI contract, but fail closed: assert at import that every SEV level reachable by fail_on is in _SEV_EXIT, validate fail_on up front, emit degraded=true in the report even when a severity exit wins, and wrap main so unhandled exceptions exit with a dedicated non-severity code (not Python's default 1, which collides with severity exits).
- *critique:* Seat A: retrying only 429/529 and never retrying any transport exception is over-strict; pre-send connect failures and edge 503s cannot have billed and are safe to retry.
- *critique:* Seat A: rating caller-wired degraded as MEDIUM understates it; http_json never raises (L105), so an all-failed run gives 0 findings and EXIT_OK (L175), a false green in CI, which is HIGH.
- *critique:* Seat A/B: a hardcoded provider host allowlist breaks legitimate BYOK endpoints (Azure OpenAI, proxies, gateways); use https plus no redirects plus a user-configured allowlist.
- *critique:* Seat A/B: the redirect concern is valid but should be precise: with default urllib, POST 301/302/303 becomes a GET that forwards Authorization/x-api-key (the leak), while POST 307/308 raises HTTPError and is returned at L126, so there is no re-POST or double-bill via redirect.
- *critique:* All seats (including mine): missed that exit 1/2 collide with Python's uncaught-exception exit (1) and argparse errors (2), and that an invalid fail_on raises KeyError at L171, so a crashed run can look like a findings result.
- *changes:* Added the exit-code collision and crash-path issue (uncaught exception or invalid fail_on yields exit 1, indistinguishable from a severity exit) and the fix (dedicated crash code, upfront fail_on validation).
- *changes:* Adopted Seat A/B's no-redirect plus https requirement, made the allowlist user-configurable, and stated the urllib 301/302/303 vs 307/308 behaviour precisely.
- *changes:* Adopted a total wall-clock deadline and degraded=true in the report even when a severity exit wins (Seat A).
- *changes:* Added a Retry-After floor plus jitter, noting that _retry_after (L93-97) ignores HTTP-date values and allows 0s retries.
- *disagree* [LOW] Retry set for paid POSTs
- *disagree* [LOW] Severity rating of caller-wired degraded
- *disagree* [MEDIUM] Exit-code collision with crashes
## Chair synthesis (chair)
Unanimous: do NOT reuse bot_common.http_json unchanged as the transport for metered BYOK LLM POSTs. Keep it for idempotent GETs and add a thin llm_call() wrapper with a non-idempotent retry policy.
Verified hazards in the code:
(a) Up to 5 attempts (L113) that retry timeouts and resets (L129-132). Combined with the 20s default timeout (L100), long completions will time out and be re-billed.
(b) Silent truncation. r.read()[:MAX_RAW] (L119) reads the full body and then slices it, so an oversized or non-JSON 2xx becomes status 200, error=None, {'_raw': first 300 chars} (L90). This can also persist completion or prompt text.
(c) Unbounded e.read() (L123). Provider error bodies stored verbatim (L125) can echo masked keys. str(e) (L130) can carry URLs.
(d) _retry_after (L93-97) ignores HTTP-date values and allows 0s sleeps.
(e) Redirects. Assuming _urlopen uses stock urllib (unseen), a POST 301/302/303 is followed as a GET carrying the auth headers; 307/308 is returned at L126.
exit_code() is correct only if callers wire degraded=True. Because http_json never raises, an all-failed run yields 0 findings and EXIT_OK: a false green. It also has a fail-open hole at L172-175: a worst finding whose sev is absent from _SEV_EXIT falls through to EXIT_OK/EXIT_DEGRADED. SEV[fail_on.upper()] (L171) raises KeyError on bad input. An uncaught exception exits 1, which may collide with a severity code (_SEV_EXIT values unseen).
Outcome: keep exit_code as the CI contract, but make it fail closed and derive degraded automatically.
### Consensus matrix
| Point | Agree | Disagree |
|---|---|---|
| Do not reuse http_json unchanged for paid LLM POSTs; keep it for idempotent GETs and add an llm_call()/idempotent=False policy | Seat A, Seat B, Seat C | |
| Never retry POST on read timeouts, connection resets after send, or 500/502/504. Max 2 retries, jitter, honor Retry-After, and enforce a total wall-clock deadline | Seat A, Seat B, Seat C | |
| Retry 429 rate limits and 529. Never retry 429 insufficient_quota or billing errors (terminal BYOK config errors) | Seat A, Seat B, Seat C | |
| Transport errors are retryable only when provably pre-send, classified by URLError.reason (socket.gaierror, ConnectionRefusedError). Timeouts count as possibly billed | Seat A, Seat B, Seat C | |
| Configurable long per-op timeout (about 120-300s, up to 600s) instead of 20s, plus a separate total deadline (urllib timeout is per socket op) | Seat A, Seat B, Seat C | |
| Bounded read(cap+1) for both success and error bodies. An oversized or non-JSON 2xx sets error and truncated=True; never return 200 with error=None | Seat A, Seat B, Seat C | |
| Persist no raw completions, prompts or request headers. Keep only the provider error type/code/message, scrub sk-/sk-ant-/xai-/Bearer patterns, and make the repr body-free | Seat A, Seat B, Seat C | |
| No-redirect opener (any 3xx is an error) and https required before attaching keys | Seat A, Seat B, Seat C | |
| Derive degraded automatically from any errored or status<0 llm_call result; record attempts and possibly_billed_attempts | Seat A, Seat B, Seat C | |
| Keep exit_code as the CI contract but fail closed. Close the L172-175 unmapped-severity hole, validate fail_on up front, and always emit degraded=true in the report even when a severity exit wins | Seat A, Seat B, Seat C | |
| Map uncaught exceptions and argparse errors to a dedicated non-severity exit code (contingent on unseen _SEV_EXIT values) | Seat A, Seat C | |
| Host allowlist: hardcoded provider hosts (A/B) vs user-configured base_url allowlist (C). Chair merge: built-in default allowlist extended by an explicit user-configured base_url (Azure/proxies), https-only, no redirects | Seat A, Seat B, Seat C | |
### Dissent / minority report
- **LOW** 503 retry condition - Seat B would retry 503 only when Retry-After is present; Seats A and C retry 503 generally. A 503 normally means the request was not processed. Chair adopts: retry 503 within the deadline and count it as possibly billed in metering. Projects that see billed 503s should switch to B's stricter rule. (Seat B)
- **LOW** TLS handshake errors as pre-send - Seats A and C also treat SSL handshake failures as pre-send. Seat B limits pre-send to gaierror/ConnectionRefusedError. A handshake failure precedes any HTTP bytes, but ssl.SSLError can also surface mid-stream. Chair adopts: retry only when the error is raised before the connection is established; if this cannot be distinguished cleanly, use B's narrower set. (Seat B)
- **HIGH** Rating of caller-wired degraded - Seat C rates it HIGH; Seat A originally rated it MEDIUM. Chair sides with HIGH. http_json never raises (L105), so a run where every paid call failed returns EXIT_OK (L175) unless every caller remembers degraded=True. This is a false green in paid CI. The remedy is agreed by all seats; only the label differed. (Seat C)
- **LOW** exit_code fix shape - Seat B prefers a generic fail-closed code when any finding meets the threshold but is unmapped. Seats A and C prefer 'worst over mapped severities' plus an import-time coverage assert. Chair merges both: assert coverage AND fail closed, so mapped-only can never yield EXIT_OK when a finding meets the threshold. (Seat B)
### Confidence shifts
| Seat | First | Last | Shift |
|---|---|---|---|
| A | 80 | 82 | +2 |
| B | 78 | 82 | +4 |
| C | 72 | 82 | +10 |
**Verdict:** `REQUEST_CHANGES`
### Action plan
1. Add an LLM transport policy in lib/bot_common.py: llm_call(url, body, headers, *, timeout=180, deadline=600, max_retries=2, cap=LLM_MAX_RAW, allowed_hosts=DEFAULT_LLM_HOSTS | user_base_url_hosts). Leave http_json's GET behaviour untouched, and add a guard so http_json refuses to send an Authorization or x-api-key header with a POST body unless idempotent=True is passed.
2. Retry classifier for llm_call. Retryable: 429 unless error.code/type is insufficient_quota or billing; 503; 529; URLError whose .reason is socket.gaierror or ConnectionRefusedError. Everything else returns immediately, including socket.timeout/TimeoutError, ConnectionResetError, RemoteDisconnected, 500/502/504 and other 4xx. Sleep = min(remaining_deadline, max(1.0, Retry-After or backoff*2**n)) + random.uniform(0, 0.5), and stop if the deadline would be exceeded. Also parse HTTP-date Retry-After values via email.utils.parsedate_to_datetime.
3. Metering: on every attempt that may have reached the provider (any HTTP status, or a timeout/reset), increment possibly_billed_attempts and attach it, along with attempts, to the result.
4. Bounded reads: raw = r.read(cap + 1). If len(raw) > cap, return status preserved, error='response too large', truncated=True, body=None. Do the same with e.read(ERR_CAP + 1). If a 2xx body fails json.loads, set error='invalid JSON' and do not return {'_raw': ...}. Use non-streaming requests (SSE is out of scope for _parse).
[... action items 4-11, the unresolved-findings table and the full SCOPE_NOTE omitted ...]
## Token & cost ledger
| Seat | Backend | Calls | In | Out | Cached | USD | Basis |
|---|---|---|---|---|---|---|---|
| A | claude | 2 | 8915 | 9843 | 2912 | 0.2455 | cli-reported |
| B | claude | 2 | 8875 | 11089 | 2911 | 0.2701 | cli-reported |
| C | claude | 2 | 8898 | 10152 | 2914 | 0.2515 | cli-reported |
| Chair | claude | 1 | 7749 | 6119 | 0 | 0.1844 | cli-reported |
| **Total** | | 7 | 34437 | 37203 | 8737 | 0.9514 | |
CLI Flags & Exit Codes
| Flag | Meaning |
|---|---|
PROMPT | - | --prompt-file PATH | The problem statement (argv, stdin or file). Secrets in it are redacted before any seat sees it. |
--mode {deliberate,pair,compete,review} | Council protocol (default deliberate; default rounds 3 / 2 / 2 / 1). |
--seats SPEC[,SPEC] / --seat SPEC | 2–6 seats, e.g. claude:architect, claude@security, codex, openai:gpt-5, anthropic:claude-opus-4-5, xai:grok-4, gemini:gemini-2.5-pro, openrouter:<vendor/model>, deepseek, ollama:<model>, cmd:<exe>. |
--chair SPEC / --role-prompt SEAT=PROMPT | Choose the synthesising seat; override or define role prompts. |
--discover | List local CLIs and which BYOK keys are set (values never shown). |
--files PATH[:START-END] / --git-diff [REF] / --repo DIR | Compact, line-numbered, redacted context. .env*, lockfiles, key files, .git/ and node_modules/ are refused. |
--rounds N / --early-stop / --no-early-stop / --quorum N / --sequential | Round count (1–10), convergence stop, minimum active seats, serial calls. |
--seat-timeout S / --total-deadline S | Per-call timeout (default 120 s) and wall-clock budget (default 600 s). |
--max-cost-usd X | Pre-flight refusal above X; no new round after 85% of X is spent. |
--effort LEVEL / --max-output-tokens N | Reasoning effort (default high); output cap (default 0 = model ceiling). |
--session ID + --status / --pause / --inject NOTE / --resume | Persist, inspect, pause, steer and continue a council. |
--state PATH --delta / --state-key KEY | Report new / changed / resolved items against the last clean run. |
--format {text,markdown,json} / --json / --out PATH / --html-out PATH | Report format and files, including the self-contained HTML viewer. |
--fail-on LEVEL / --offline PATH / --env-file PATH | Exit threshold (default medium), fixture replay, provider-key file. |
| Exit code | Meaning |
|---|---|
0 | Completed with nothing unresolved at or above --fail-on (or paused on request). |
1 | Worst unresolved item is MEDIUM (e.g. NO_CONSENSUS, REQUEST_CHANGES). |
2 | Worst unresolved item is HIGH (HIGH dissent, BLOCK, HIGH review finding). |
3 | Worst unresolved item is CRITICAL. |
10 | Usage error or refusal: bad seat spec, missing CLI or key, disallowed path, pre-flight budget refusal. |
11 | Degraded: seat timeout or error, lost quorum, deadline cut-off. Never a clean consensus. |
What It Does Not Do
- Advisory and read-only. It reads only the slices and diff you pass and returns text. It never edits, commits, runs or deploys anything.
claudeseats run with tools disabled,codexin its read-only sandbox, andaiderin dry-run mode, all from an empty temp directory. - No secrets to seats. It refuses
.env*,.wattcoin_env*, lockfiles, key and credential files,.git/andnode_modules/, and redacts API keys, tokens, private keys and high-entropy strings from the prompt, context and moderator notes. Provider keys are read by variable name and never printed or stored. - No offensive tasks. Seats are instructed to decline exploit writing, credential harvesting and similar requests. Defensive design, threat modelling and code review are in scope.
- Consensus is evidence, not proof. Models are non-deterministic and can be confidently wrong together. Verify patches and findings before acting. BYOK costs are estimates from a static price table; your provider invoice is authoritative.
Scope note (printed with every report): advisory, read-only deliberation; disallowed paths refused and secrets redacted before any seat sees them; outputs are non-deterministic and a consensus is evidence, not proof; cost figures are estimates and the --max-cost-usd guard counts both HTTP and CLI seats.
Pinned Runtime Scripts (SHA-256 Verified)
https://botappstore.com/runtime/agent_council.py— 2,801 lines — SHA-256:703d307e52c4e13df94e28554d16143a5286bf0af3ffe6d95431a5fa490284d7https://botappstore.com/runtime/bot_common.py— 274 lines — SHA-256:d1941b61b95ca664e4c1286c3bf1918c00e976c2c1601199b19200257650e526
Runtime URLs are license-gated: send X-License-Key: wcl_... or X-Agent-Key: wck_... (unauthenticated requests get 401). The served file carries a per-license watermark block (5 lines), so compare the X-Canonical-SHA256 response header (or the SHA-256 after stripping the block) against the pins above. The recommended path is the setup skill installer:
python3 /workspace/.wattcoin_license_check.py --sku agent-council --install-runtime ... --expected-sha256 703d307e52c4e13df94e28554d16143a5286bf0af3ffe6d95431a5fa490284d7
The setup and council skills verify both canonical digests before every run and refuse to execute either file on a mismatch. Both are locked at chmod 500. The runtime is stdlib-only Python and is covered by 170 unit tests.
Technical FAQ
Which seats work out of the box, and do I need API keys?
How does a deliberation avoid groupthink and anchoring?
How do pair, compete and review modes work?
How are tokens kept down without throttling the models?
What happens when a seat hangs or the budget runs out?
Can I steer a council while it is running?
What is the HTML council viewer?
What does it refuse to do?
Licensing & Setup
$39.00 USD one-time, paid by card, Apple Pay or Google Pay, with up to 3 workspace-reset rebinds and no subscription. On Turn 1 the agent-council-setup skill installs the read-only license helper (0500), verifies and installs the SHA-256-pinned runtime, registers and activates your agent key, binds your wcl_... code through stdin, runs --discover to show the seats your computer can field, and offers a small budget-capped smoke council and an optional scheduled review routine. The license code and any provider keys are never printed in chat.
One wcl_... key, two runtimes. Your license key works inside Grok Bot and from any local terminal or IDE agent (Cursor, Claude Code, Windsurf, CI/CD) via the standalone single-file Python 3 CLI (/runtime/agent_council.py) — Python 3.10+ standard library only, zero pip install dependencies, with --json / --markdown output.