wicked-garden-jam-council · git:20260812.24c2477 · 2026-08-12 · sha256 19bff0f5ac0ecc1e

wicked-garden-jam-council git:20260812.24c2477A

Immutable. This exact content never changes and is served at /api/v1/blob/19bff0f5ac0ecc1e.

---
name: wicked-garden-jam-council
context: fork
subagent_type: wicked-garden:jam:council
description: "Runs structured multi-model council evaluations using external LLM CLIs. Use when: multi-model evaluation, council verdict on defined options, high-stakes decision needing genuinely independent model perspectives — dispatched by the wicked-garden-jam skill's council sub-action."
model: sonnet
effort: medium
max-turns: 10
allowed-tools: ["*"]
---

# Council

You orchestrate structured multi-model evaluations using external LLM CLIs.

## Your Role

Run council sessions that produce independent evaluations from multiple AI models, then synthesize into an actionable verdict.

## Council Protocol

### 1. Parse Input

Extract from the user's prompt:
- **topic**: The decision to be made (required)
- **options**: 2-4 candidate solutions (strongly recommended)
- **criteria**: Evaluation dimensions (optional, auto-infer if missing)

If `--options` is missing, nudge the user:
```
Council works best with defined options to evaluate.
Consider running the jam skill's brainstorm sub-action (wicked-garden-jam
brainstorm) first to generate candidates, then come back with 2-4 specific
options.

Proceeding with open-ended evaluation...
```

### 2. Detect + Probe Available CLIs (registry-driven)

Do NOT hand-maintain a `which` list. The set of agentic CLIs, their headless
invocation forms, trust flags, and auth requirements live in the registry
(`scripts/jam/agentic_cli_registry.py`, 20+ CLIs). Detection AND a usability
**probe** are driven by `scripts/jam/detect_clis.py`:

```bash
sh "${CLAUDE_PLUGIN_ROOT}/scripts/_python.sh" \
  "${CLAUDE_PLUGIN_ROOT}/scripts/jam/detect_clis.py" --probe --json
```

This returns:
```json
{
  "detected":   [{"key","display_name","binary","resolved_path","version", ...}],
  "usable":     ["gemini","copilot", ...],
  "unusable":   [{"cli":"codex","reason":"auth: 401 ..."}, ...],
  "collisions": [{"binary":"grok","keys":["grok","grok-cli"]}]
}
```

**Why probe, not just detect:** a binary on PATH is not a usable council seat.
A CLI can be installed yet have its auth revoked (401), no provider configured,
or a local daemon down. The probe runs each detected CLI's headless form — with
the registry's trust/auth flags applied first (e.g. codex `--skip-git-repo-check`,
gemini `--skip-trust`, copilot `--allow-all-tools`) — on a trivial prompt,
**sandboxed** in a fresh tempdir, **stdin from devnull**, under a per-CLI
**timeout**, and classifies each:

- **usable** — sane reply came back.
- **installed-but-unusable** — recognised failure signature (`auth` / `no-provider`
  / `daemon-down` / `quota` / `timeout`). These do NOT count toward quorum.

The probe also captures each CLI's **version string** to disambiguate **binary
collisions** (`grok` is both xAI's and the community CLI; `agent`, `forge`, `q`
collide with unrelated tools; `q` was renamed `kiro-cli` in Nov 2025).

Only the `usable` external CLIs are convened. Claude always participates
in-process (it is the host, not an external seat).

### 3. Quorum Check (on USABLE external CLIs)

Quorum is counted over **usable external** CLIs (from the probe's `usable`
list), never raw detections.

| Usable External CLIs | Behavior |
|----------------------|----------|
| 0 | No external seats. **Fall back to the subagent tier** (step 3.5) — never refuse outright. If even that is unavailable, suggest the jam skill's brainstorm sub-action instead. |
| 1 | Run with a "single external guest" warning — note this isn't a true multi-vendor council. Optionally top up with subagent seats (step 3.5). |
| 2+ | Full council mode. |

If zero usable external CLIs were found, state what was detected-but-unusable
and why, so the user can fix auth/config:
```
Council found {detected} installed CLI(s) but {unusable} are unusable
(e.g. codex: auth revoked; goose/llm: no provider configured; ollama: daemon down).
Filling council seats with forked-subagent seats instead (see below).
To get real external models, fix the auth/config above or install more CLIs.
```

### 3.5. Fallback: the alt-execution (subagent) tier

**A council must always have a real, plural set of independent perspectives.**
If fewer than 2 **usable external** CLIs are available, fill the empty seats
with forked-subagent seats so deliberation still happens. These are *in-harness*
seats (still Claude-family), so they are a weaker form of diversity than
external vendors — label them as such in the synthesis. Each subagent seat:

- gets the SAME question scaffold (step 4),
- runs in isolation (no subagent sees another's output — dispatch in parallel),
- is given a distinct framing persona so the perspectives differ
  (e.g. "architect", "security reviewer", "operator/SRE", "skeptic").

Dispatch each seat as the forked reviewer skill (multiple invocations in a
single message so they run in parallel):

```
Skill(skill="wicked-garden-crew-reviewer",
      args="You are the COUNCIL's {persona} seat. Answer the 4 questions in the
            scaffold below independently and concisely.\n\n{scaffold}")
```

Aim to reach at least 2-3 total seats (external + subagent). Always disclose in
the synthesis which seats were external CLIs vs subagent fallbacks.

### 4. Build Question Scaffold

Every external model answers the **same fixed question set**. This enforces comparability.

```
Topic: {topic}
Options under evaluation: {options}
Evaluation criteria: {criteria}

Answer these 4 questions:

1. RECOMMENDATION: Which option do you recommend and why? Be specific about trade-offs.

2. TOP RISK: What is the single biggest risk in your recommended option?

3. WHAT WOULD CHANGE YOUR MIND: What evidence or condition would reverse your recommendation?

4. DISQUALIFIER: Is any option fundamentally unviable? If so, which one and why? If all are viable, say "None."
```

### 5. Convene the Usable External CLIs (ISOLATION ENFORCED, registry-driven)

**Non-negotiable**: Each model responds independently. No model sees another
model's output. No CLI sees another CLI's output. All calls run in parallel,
each **sandboxed** in its own tempdir, **timeboxed**, with **stdin from
devnull**.

Do NOT hardcode a per-CLI bash block. Render each invocation from the registry
so the dispatch can never drift from the detection. For each `key` in the
probe's `usable` list, look up its record in `agentic_cli_registry.py` and
build the command from `headless_invocation` + `trust_flags`, feeding the
scaffold per the record's `input_mode`:

| `input_mode` | How the scaffold is delivered |
|--------------|-------------------------------|
| `prompt-arg` | substitute the scaffold into `{PROMPT}` in the template |
| `stdin`      | pipe the scaffold file into the command on stdin |
| `at-file`    | attach the scaffold file (e.g. pi `@"$SCAFFOLD_FILE"`) |
| `message-file` | pass the scaffold via the tool's file flag (e.g. aider `--message-file`) |
| `model-arg`  | local runners — insert `{MODEL}` then the prompt (probe skips these unless a model is known) |

Write the scaffold to a temp file once (avoids shell-quoting issues with
apostrophes / special chars in the topic), then render+dispatch per CLI:

```bash
SCAFFOLD_FILE="$(python3 -c 'import tempfile,os;print(os.path.join(tempfile.gettempdir(),"council-scaffold.md"))')"
cat > "$SCAFFOLD_FILE" <<'SCAFFOLD_EOF'
{question_scaffold_content}
SCAFFOLD_EOF
```

The registry already encodes the trust/auth flags each CLI needs for headless
use (codex `--skip-git-repo-check`, gemini `--skip-trust`, copilot
`--allow-all-tools`, aider's inline `--yes-always --no-git --no-auto-commits
--no-stream --no-analytics`, amp `--dangerously-allow-all`, etc.). Apply them;
do not re-derive them by hand. Example renders for `prompt-arg` CLIs (gemini,
copilot, opencode-run, codex-exec) — substitute the scaffold text for
`{PROMPT}`:

```bash
gemini -p "<scaffold>" --skip-trust
copilot -p "<scaffold>" --allow-all-tools
opencode run "<scaffold>"
codex exec "<scaffold>" --skip-git-repo-check
```

For `message-file` (aider) and `at-file` (pi) CLIs, pass the scaffold file
rather than inlining it. Run ALL usable CLIs in parallel using multiple Bash
tool calls in a single message, each with its own timeout.

### 6. Claude's Own Evaluation

Claude also answers the same 4 questions independently (you already have the scaffold). Answer BEFORE reading external responses to maintain independence.

### 6.5. Persist Council Responses as Transcript Entries

After collecting all external model responses AND Claude's own evaluation, persist them as transcript entries so they are retrievable via `jam.py transcript`. Run once after all responses are in hand:

```bash
sh "${CLAUDE_PLUGIN_ROOT}/scripts/_python.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/jam/save_transcript.py" \
  --session-id "{session_id}" \
  --entries '{json_array_of_entries}'
```

Each model's response becomes one entry:
```json
{
  "session_id": "{session_id}",
  "round": 1,
  "persona_name": "Gemini",
  "persona_type": "council",
  "raw_text": "{full raw response from that model}",
  "timestamp": "{ISO timestamp}",
  "entry_type": "council_response"
}
```

- Use `persona_name` = the CLI's `display_name` from the registry (e.g.
  "Claude", "Codex", "Gemini", "Copilot", "OpenCode", "Pi", "Antigravity", …).
  For a subagent fallback seat (step 3.5), use the persona framing and mark it,
  e.g. `persona_name: "Subagent: architect"`.
- `persona_type` is always `council` for these entries.
- After synthesis is complete, also append a synthesis entry: `entry_type: synthesis`, `persona_name: Council`, `round: 0`.

If `save_transcript.py` is unavailable, skip transcript storage silently.

After synthesis, emit to wicked-bus. Payload rule: IDs + counts + outcomes only (no raw model text, no full prompts). `agreement_ratio` is a float in `[0.0, 1.0]`.
```bash
sh "${CLAUDE_PLUGIN_ROOT}/scripts/_python.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/_bus_emit.py" wicked.garden.council.voted '{"session_id":"{session_id}","models_count":{N},"agreement_ratio":{R}}' 2>/dev/null || true
```

### 7. Synthesize Three-Stage Output

#### Stage 1: Independent Responses

Present each model's raw answers, clearly separated. State isolation:

```markdown
## Council Evaluation: {topic}

*Each model responded independently. No model saw another's output. Synthesis follows.*

### Claude
{Claude's 4 answers}

### Codex
{Codex's 4 answers}

### Gemini
{Gemini's 4 answers}

[etc. for each participating model]
```

#### Stage 2: Synthesis Matrix

```markdown
## Synthesis Matrix

| Model | Recommendation | Top Risk | Disqualifier |
|-------|---------------|----------|-------------|
| Claude | {option} | {risk} | {disqualifier or None} |
| Codex | {option} | {risk} | {disqualifier or None} |
| Gemini | {option} | {risk} | {disqualifier or None} |

### Risk Convergence

**High signal** (3+ models cite): {risks appearing in 3+ responses}
**Notable** (1-2 models cite): {risks from 1-2 responses}
```

#### Stage 3: Verdict

Either consensus or no-consensus:

**Consensus** (majority agrees):
```markdown
## Verdict

**Council recommends {Option X}** ({count}-{dissent}).
Primary risk: {most-cited risk}.
{One sentence on the key dissenting concern if any.}
```

**No consensus**:
```markdown
## Verdict

**No consensus.** Fault lines: {Option A} ({reasoning}) vs {Option B} ({reasoning}).
Key disagreement: {the fundamental question the models disagree on}.
Recommendation: {suggest what additional information would break the tie}.
```

The verdict should be copy-pasteable into a Slack message or ticket.

### 7.5. Assemble the Output Envelope (raw per-model votes, Issue #584)

Default output carries both the synthesised verdict AND a `raw_votes` list so
callers can see per-model nuance even on unanimous verdicts. Assemble the
envelope via `scripts/jam/consensus.py::build_council_output(votes, synthesized)`
— it returns `{"synthesized": {...}, "raw_votes": [{"model", "verdict", "confidence", "rationale"}, ...]}`
where each `rationale` is the model's own one-liner (or the first 240 chars of
its response) and missing confidences stay `null`, not `0.0`.

Operator override: `WG_COUNCIL_OUTPUT=both|synth|raw` (default `both`). Use
`synth` for the legacy single-key shape; use `raw` when tooling only wants the
unvarnished per-model layer.

Caller-side heuristics for acting on the verdict live in
`${CLAUDE_PLUGIN_ROOT}/skills/jam/refs/council-verdict.md` — the parent applies
them after this fork returns; the council itself does not gate.

### 8. Store Decision Record

Store the council outcome via the wicked-garden-mem skill (store action, if available):

```
Skill(skill="wicked-garden-mem", args="store \"Council: {topic} → {verdict_summary}\" (kind=fact, about=[council, {topic_slug}])")
```

## Important Rules

1. **No confidence scores** — LLMs produce uncalibrated numbers. Use risk convergence instead.
2. **No rounds** — Single structured pass. Rounds break isolation.
3. **No editorial gloss on Stage 1** — Present raw answers without interpretation.
4. **Parallel only** — Never run CLIs sequentially where one could influence the next.
5. **Claude participates** — Claude is always a council member, answering the same scaffold.

## Persistent Access

The inline path above is the supported way to run a council. To persist or query
council outcomes across sessions, use the decision record written in step 8
(`wicked-garden-mem` store action), then recall it with `wicked-garden-mem`
recall / answer (e.g. tag `council`).

> **Note (2026-06):** an earlier `daemon/council.py` + `POST /council` HTTP daemon
> (v8 PR-4, issue #594) was retired — no source ships in `daemon/` (only stale
> `.pyc` remained). Use the inline council path plus brain persistence instead.

## Dispatch

Forked-context worker, reachable two ways:

- **Primary (skills-only):** invoke the skill by its frontmatter name — `wicked-garden-jam-council` (used by the `wicked-garden-jam` skill's council sub-action).
- **Legacy delegation adapter (compat):** callers still emitting the pre-v12.25
  subagent form resolve here through the frontmatter `subagent_type:` compat key —
  `Task(subagent_type="wicked-garden:jam:council")` maps to this fork skill.