Răsfoiți Sursa

Merge pull request #362 from alvinunreal/feat/remove-council-master-356

refactor(council): remove council-master agent, let council synthesize directly
Alvin 3 luni în urmă
părinte
comite
f811de7062

+ 0 - 1
.slim/codemap.json

@@ -34,7 +34,6 @@
     "scripts/generate-schema.ts": "007f340e39adf6c3fd76feda72b71df1",
     "scripts/verify-opencode-host-smoke.ts": "a87fdb08b123501edf81618a49bc421d",
     "scripts/verify-release-artifact.ts": "83259be1926459412809013ce16e6fbb",
-    "src/agents/council-master.ts": "f013987f32d30178efb3bfa3030b3ce8",
     "src/agents/council.ts": "afa3ed4c40b2f91bdc907b850a7e68d3",
     "src/agents/councillor.ts": "e12284c5f632ba071e97924608375f9c",
     "src/agents/designer.ts": "6b5786ea6de1fb41b367103824762c9d",

+ 1 - 1
README.md

@@ -267,7 +267,7 @@ If any agent fails to respond, check your provider authentication and config fil
   </tr>
   <tr>
     <td colspan="2">
-      <b>Default Setup:</b> <code>Config-driven</code> — council master + councillors are chosen from your configured preset
+      <b>Default Setup:</b> <code>Config-driven</code> — councillors are chosen from your configured preset and the council agent synthesizes their responses
     </td>
   </tr>
   <tr>

+ 8 - 16
docs/configuration.md

@@ -103,22 +103,14 @@ All config files support **JSONC** (JSON with Comments):
 | `fallback.timeoutMs` | number | `15000` | Time before aborting and trying next model |
 | `fallback.retryDelayMs` | number | `500` | Delay between retry attempts |
 | `fallback.chains.<agent>` | string[] | — | Ordered fallback model IDs for an agent |
-| `fallback.retry_on_empty` | boolean | `true` | Treat silent empty provider responses (0 tokens) as failures and retry. Set `false` to accept empty responses |
-| `council.master.model` | string | — | **Required if using council.** Council master model |
-| `council.master.variant` | string | — | Council master variant |
-| `council.master.prompt` | string | — | Optional synthesis guidance for the master |
-| `council.presets` | object | — | **Required if using council.** Named councillor presets |
-| `council.presets.<name>.<councillor>.model` | string | — | Councillor model |
-| `council.presets.<name>.<councillor>.variant` | string | — | Councillor variant |
-| `council.presets.<name>.<councillor>.prompt` | string | — | Optional role guidance for the councillor |
-| `council.presets.<name>.master.model` | string | — | Override global master model for this preset |
-| `council.presets.<name>.master.variant` | string | — | Override global master variant for this preset |
-| `council.presets.<name>.master.prompt` | string | — | Override global master prompt for this preset |
-| `council.default_preset` | string | `"default"` | Default preset when none is specified |
-| `council.master_timeout` | number | `300000` | Master synthesis timeout (ms) |
-| `council.councillors_timeout` | number | `180000` | Per-councillor timeout (ms) |
-| `council.master_fallback` | string[] | — | Fallback models for the council master |
-| `council.councillor_retries` | number | `3` | Max retries per councillor and master on empty provider response (0–5) |
+ | `fallback.retry_on_empty` | boolean | `true` | Treat silent empty provider responses (0 tokens) as failures and retry. Set `false` to accept empty responses |
+ | `council.presets` | object | — | **Required if using council.** Named councillor presets |
+ | `council.presets.<name>.<councillor>.model` | string | — | Councillor model |
+ | `council.presets.<name>.<councillor>.variant` | string | — | Councillor variant |
+ | `council.presets.<name>.<councillor>.prompt` | string | — | Optional role guidance for the councillor |
+ | `council.default_preset` | string | `"default"` | Default preset when none is specified |
+ | `council.timeout` | number | `180000` | Councillor timeout (ms) |
+ | `council.councillor_retries` | number | `3` | Max retries per councillor on empty provider response (0–5) |
 | `todoContinuation.maxContinuations` | integer | `5` | Max consecutive auto-continuations before stopping (1–50) |
 | `todoContinuation.cooldownMs` | integer | `3000` | Delay in ms before auto-continuing — gives user time to abort (0–30000) |
 | `todoContinuation.autoEnable` | boolean | `false` | Automatically enable auto-continue when session has enough todos |

+ 34 - 142
docs/council.md

@@ -18,13 +18,13 @@ Multi-LLM consensus system that runs several models in parallel and synthesises
 
 ## Overview
 
-The **Council agent** sends your prompt to multiple LLMs (councillors) in parallel, then passes all responses to a **council master** that synthesises the optimal answer. Think of it as asking three experts and having a senior referee pick the best parts.
+The **Council agent** sends your prompt to multiple LLMs (councillors) in parallel, then the council agent itself synthesises the optimal answer from all councillor responses.
 
 ### Key Benefits
 
 - **Higher confidence** — consensus across models reduces single-model blind spots
 - **Diverse perspectives** — different architectures catch different issues
-- **Graceful degradation** — if the master fails, the best councillor response is returned
+- **Graceful degradation** — the council agent synthesises from whatever councillor results came back
 - **Configurable presets** — different council compositions for different tasks
 
 ### How It Works
@@ -41,12 +41,11 @@ User prompt
     └──────────────┴──────────────┘
-            Council Master
-            (synthesis model)
-            🔒 no tools
+          Council Agent
+          (synthesises)
-           Synthesised response
+         Synthesised response
 ```
 
 ---
@@ -60,7 +59,6 @@ Edit `~/.config/opencode/oh-my-opencode-slim.json` (or `.jsonc`):
 ```jsonc
 {
   "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
     "presets": {
       "default": {
         "alpha": { "model": "openai/gpt-5.4-mini" },
@@ -95,29 +93,21 @@ Configure in `~/.config/opencode/oh-my-opencode-slim.json` (or `.jsonc`):
 ```jsonc
 {
   "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
     "default_preset": "default",
     "presets": {
       "default": { /* councillors */ }
     },
-    "master_timeout": 300000,
-    "councillors_timeout": 180000
+    "timeout": 180000
   }
 }
 ```
 
 | Setting | Type | Default | Description |
 |---------|------|---------|-------------|
-| `master` | object | — | **Required.** Council master configuration (see below) |
-| `master.model` | string | — | **Required.** Model ID in `provider/model` format |
-| `master.variant` | string | — | Optional variant for the master model |
-| `master.prompt` | string | — | Optional guidance for the master's synthesis (see [Role Prompts](#role-prompts)) |
 | `presets` | object | — | **Required.** Named councillor presets (see below) |
 | `default_preset` | string | `"default"` | Which preset to use when none is specified |
-| `master_timeout` | number | `300000` | Master synthesis timeout in ms (5 minutes) |
-| `councillors_timeout` | number | `180000` | Per-councillor timeout in ms (3 minutes) |
-| `master_fallback` | string[] | — | Optional fallback models for the master. Tried in order if the primary model fails or times out |
-| `councillor_retries` | number | `3` | Max retries per councillor and master on empty provider response (0–5). Each retry creates a fresh session |
+| `timeout` | number | `180000` | Per-councillor timeout in ms (3 minutes) |
+| `councillor_retries` | number | `3` | Max retries per councillor on empty provider response (0–5). Each retry creates a fresh session |
 
 ### Councillor Configuration
 
@@ -129,40 +119,11 @@ Each councillor within a preset:
 | `variant` | string | No | Model variant (e.g., `"high"`, `"low"`) |
 | `prompt` | string | No | Role-specific guidance injected into the councillor's user prompt (see [Role Prompts](#role-prompts)) |
 
-### Per-Preset Master Override
-
-Each preset can optionally override the global master's `model`, `variant`, and `prompt` using a reserved `"master"` key:
-
-```jsonc
-{
-  "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
-    "presets": {
-      "fast-review": {
-        "master": { "model": "openai/gpt-5.4" },
-        "alpha": { "model": "openai/gpt-5.4-mini" },
-        "beta":  { "model": "google/gemini-3-pro" }
-      }
-    }
-  }
-}
-```
-
-| Field | Type | Required | Description |
-|-------|------|----------|-------------|
-| `presets.<name>.master.model` | string | No | Overrides the global master model for this preset |
-| `presets.<name>.master.variant` | string | No | Overrides the global master variant for this preset |
-| `presets.<name>.master.prompt` | string | No | Overrides the global master prompt for this preset |
-
-**Merge behaviour:** Each field uses nullish coalescing — if a field is omitted in the preset override, the global value is used. If no `"master"` key exists in the preset, the global master is used as-is.
-
-**Reserved key:** `"master"` inside a preset is reserved for this override and is not treated as a councillor name. Any councillor named `"master"` will be ignored.
-
 ### Constraints
 
 - Councillors run as **agent sessions with read-only codebase access** — they can read files, search by name (glob), search by content (grep), search by AST pattern (codesearch), and query the language server (LSP). They cannot modify files, run shell commands, or spawn subagents. This makes council responses grounded in actual code rather than guessing.
-- The council master also runs as an agent session with zero permissions — synthesis is purely analytical.
-- Councillor and council-master agents can be configured (model, temperature, MCPs, skills) via the standard `agents.councillor` and `agents.council-master` preset overrides.
+- The council agent itself synthesises the final answer from councillor results using its own model.
+- Councillor agents can be configured (model, temperature, MCPs, skills) via the standard `agents.councillor` preset override.
 
 ---
 
@@ -175,7 +136,6 @@ Use a single councillor when you want a second model's take without overhead:
 ```jsonc
 {
   "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
     "presets": {
       "second-opinion": {
         "reviewer": { "model": "openai/gpt-5.4" }
@@ -185,7 +145,7 @@ Use a single councillor when you want a second model's take without overhead:
 }
 ```
 
-**When to use:** Quick sanity check from a different model. The master still reviews the single response and can refine it.
+**When to use:** Quick sanity check from a different model.
 
 ### 2-Councillor: Compare & Contrast
 
@@ -194,7 +154,6 @@ Two councillors with different models:
 ```jsonc
 {
   "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
     "presets": {
       "compare": {
         "analyst":  { "model": "openai/gpt-5.4" },
@@ -214,7 +173,6 @@ The default setup — three diverse models:
 ```jsonc
 {
   "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
     "presets": {
       "default": {
         "alpha": { "model": "openai/gpt-5.4-mini" },
@@ -235,7 +193,6 @@ As many councillors as you need — the system runs them all in parallel:
 ```jsonc
 {
   "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
     "presets": {
       "full-board": {
         "alpha":   { "model": "anthropic/claude-opus-4-6" },
@@ -245,12 +202,12 @@ As many councillors as you need — the system runs them all in parallel:
         "echo":    { "model": "openai/gpt-5.4-mini" }
       }
     },
-    "councillors_timeout": 300000
+    "timeout": 300000
   }
 }
 ```
 
-**When to use:** High-stakes design reviews or complex architectural decisions where maximum model diversity matters. Increase `councillors_timeout` since there are more responses to collect.
+**When to use:** High-stakes design reviews or complex architectural decisions where maximum model diversity matters. Increase `timeout` since there are more responses to collect.
 
 ### Multiple Presets
 
@@ -259,7 +216,6 @@ Define several presets and choose at invocation time:
 ```jsonc
 {
   "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
     "default_preset": "balanced",
     "presets": {
       "quick": {
@@ -291,7 +247,7 @@ Define several presets and choose at invocation time:
 
 ### Role Prompts
 
-Both councillors and the master accept an optional `prompt` field that injects role-specific guidance into the user prompt. This lets you steer each participant's behaviour without changing the system prompt.
+Councillors accept an optional `prompt` field that injects role-specific guidance into the user prompt. This lets you steer each participant's behaviour without changing the system prompt.
 
 **Councillor prompt** — prepended to the user prompt before the divider:
 
@@ -301,29 +257,15 @@ Both councillors and the master accept an optional `prompt` field that injects r
 <user prompt>
 ```
 
-**Master prompt** — appended after the synthesis instruction:
-
-```
-<synthesis instruction>
-
----
-**Master Guidance**:
-<role prompt>
-```
-
 #### Example: Specialised Review Board
 
-Both councillors and the master accept an optional `prompt` field. The master prompt can be set globally (`council.master.prompt`) or per-preset (`presets.<name>.master.prompt`):
+Councillors accept an optional `prompt` field:
 
 ```jsonc
 {
   "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
     "presets": {
       "review-board": {
-        "master": {
-          "prompt": "Prioritise correctness and security over creativity. Flag any risks."
-        },
         "reviewer": {
           "model": "openai/gpt-5.4",
           "prompt": "You are a meticulous code reviewer. Focus on edge cases, error handling, and potential bugs."
@@ -342,29 +284,7 @@ Both councillors and the master accept an optional `prompt` field. The master pr
 }
 ```
 
-#### Example: Per-Preset Master Model + Councillor Prompt
-
-Override the master model for a specific preset while customising one councillor's role:
-
-```jsonc
-{
-  "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
-    "presets": {
-      "fast": {
-        "master": { "model": "openai/gpt-5.4" },
-        "alpha": { "model": "openai/gpt-5.4-mini" },
-        "beta": {
-          "model": "google/gemini-3-pro",
-          "prompt": "Respond as a devil's advocate. Challenge assumptions and find weaknesses."
-        }
-      }
-    }
-  }
-}
-```
-
-Without a `prompt`, the councillor or master uses its default behaviour — no changes to the prompt.
+Without a `prompt`, the councillor uses its default behaviour — no changes to the prompt.
 
 ---
 
@@ -378,7 +298,7 @@ Talk to the council agent like any other agent:
 @council Should we use event sourcing or CRUD for the order service?
 ```
 
-The council agent delegates to `council_session` internally and returns the synthesised result.
+The council agent runs councillors in parallel and synthesises the result directly.
 
 ### Orchestrator Delegation
 
@@ -420,19 +340,16 @@ If some councillors failed:
 
 | Timeout | Default | Scope |
 |---------|---------|-------|
-| `councillors_timeout` | 180000 ms (3 min) | Per-councillor — each councillor gets this much time |
-| `master_timeout` | 300000 ms (5 min) | Master synthesis — one timeout for the whole synthesis phase |
+| `timeout` | 180000 ms (3 min) | Per-councillor — each councillor gets this much time |
 
-Councillors that don't respond in time are marked `timed_out`. The master proceeds with whatever results came back.
+Councillors that don't respond in time are marked `timed_out`. The council agent proceeds with whatever results came back.
 
 ### Graceful Degradation
 
 | Scenario | Behaviour |
 |----------|-----------|
-| Some councillors fail | Master synthesises from the survivors |
-| All councillors fail | Returns error immediately — master is never invoked |
-| Master primary model fails | Tries `master_fallback` models in order before degrading |
-| All master models fail | Returns best single councillor response prefixed with `(Degraded — master failed, using <name>'s response)` |
+| Some councillors fail | Council agent synthesises from the available results |
+| All councillors fail | Returns error immediately |
 | Councillor gets empty response | Retries up to `councillor_retries` times with fresh sessions |
 
 ### Empty Response Detection
@@ -440,7 +357,7 @@ Councillors that don't respond in time are marked `timed_out`. The master procee
 Providers sometimes silently drop requests — returning zero tokens with no error. This is detected automatically:
 
 - **Background tasks** (`@explorer`, `@fixer`, etc.): Empty responses trigger the fallback chain (next model in `fallback.chains`). Controlled by `fallback.retry_on_empty` (default `true`). Set to `false` to accept empty responses without retrying.
-- **Council councillors and master**: Empty responses trigger up to `councillor_retries` fresh sessions (default `3`). Only "Empty response from provider" errors are retried — timeouts and other failures return immediately.
+- **Council councillors**: Empty responses trigger up to `councillor_retries` fresh sessions (default `3`). Only "Empty response from provider" errors are retried — timeouts and other failures return immediately.
 
 To disable empty-response retry globally:
 
@@ -450,22 +367,6 @@ To disable empty-response retry globally:
 }
 ```
 
-### Master Fallback Chain
-
-The council master can be configured with fallback models. If the primary master model fails (timeout, API error, rate limit), the system tries each fallback in order before degrading to the best councillor response. This uses the same abort-retry pattern as the foreground failover system.
-
-```jsonc
-{
-  "council": {
-    "master": { "model": "anthropic/claude-opus-4-6" },
-    "master_fallback": ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4"],
-    "presets": { /* ... */ }
-  }
-}
-```
-
-When not configured, the master uses a single model with no fallback.
-
 ---
 
 ## Troubleshooting
@@ -479,7 +380,7 @@ When not configured, the master uses a single model with no fallback.
    ```bash
    cat ~/.config/opencode/oh-my-opencode-slim.json | grep -A 5 '"council"'
    ```
-2. Ensure `master.model` and at least one preset with one councillor are defined
+2. Ensure at least one preset with one councillor is defined
 3. Restart OpenCode after config changes
 
 ### All Councillors Timing Out
@@ -488,9 +389,9 @@ When not configured, the master uses a single model with no fallback.
 
 **Solutions:**
 1. **Increase timeout:**
-   ```jsonc
-   { "council": { "councillors_timeout": 300000 } }
-   ```
+    ```jsonc
+    { "council": { "timeout": 300000 } }
+    ```
 2. **Verify model IDs** — models must be in `provider/model` format and available in your OpenCode configuration
 3. **Check provider connectivity** — ensure the model providers are reachable
 
@@ -529,9 +430,8 @@ Choose models from **different providers** for maximum perspective diversity:
 
 ### Cost Considerations
 
-- Each councillor is one agent session → N councillors = N sessions + 1 master session. Councillors may use multiple tool calls within their session (read, grep, etc.), which increases token usage but grounds responses in actual code.
-- Use smaller/faster models as councillors and a stronger model as master, unless you are willing to spend the tokens on parallel frontier models.
-- The 1-councillor preset is the most cost-effective (2 calls total)
+- Each councillor is one agent session → N councillors = N sessions. Councillors may use multiple tool calls within their session (read, grep, etc.), which increases token usage but grounds responses in actual code.
+- The 1-councillor preset is the most cost-effective (1 call total)
 
 ### Council Agent Mode
 
@@ -542,9 +442,9 @@ The council agent is registered with `mode: "all"` in the OpenCode SDK, meaning
 
 This is intentional: council is useful both as a user-facing tool for deliberate consensus-seeking and as a subagent the orchestrator can invoke for high-stakes decisions.
 
-### Customising Councillor & Master Agents
+### Customising Councillor Agents
 
-Councillor and council-master are registered agents, so you can customise them using the standard `agents` override system:
+Councillor is a registered agent, so you can customise it using the standard `agents` override system:
 
 ```jsonc
 {
@@ -553,10 +453,6 @@ Councillor and council-master are registered agents, so you can customise them u
       "model": "openai/gpt-5.4",
       "temperature": 0.3,
       "mcps": ["grep_app", "context7"]
-    },
-    "council-master": {
-      "model": "anthropic/claude-opus-4-6",
-      "variant": "high"
     }
   }
 }
@@ -566,7 +462,6 @@ Councillor and council-master are registered agents, so you can customise them u
 | Agent | Model | MCPs | Skills | Temperature |
 |-------|-------|------|--------|-------------|
 | `councillor` | `openai/gpt-5.4-mini` | none | none | 0.2 |
-| `council-master` | `openai/gpt-5.4-mini` | none | none | 0.1 |
 
 **Note:** Per-councillor model overrides in the council config (`presets.<name>.<councillor>.model`) take precedence over the agent-level default.
 
@@ -583,15 +478,12 @@ Councillor and council-master are registered agents, so you can customise them u
 │    ├── SubagentDepthTracker (recursion guard)           │
 │    │                                                     │
 │    └── Agent Sessions                                    │
-│        ├── councillor (read-only, 🔍)                   │
-│        │   └── deny all + allow: read, glob, grep,      │
-│        │       lsp, list, codesearch                     │
-│        └── council-master (zero tools, 🔒)              │
-│            └── deny all + question: deny                 │
+│        └── councillor (read-only, 🔍)                   │
+│            └── deny all + allow: read, glob, grep,      │
+│                lsp, list, codesearch                     │
 │                                                         │
 │  Agent Registration                                     │
 │    ├── council: mode "all" (user + orchestrator)        │
-│    ├── councillor: mode "subagent", hidden              │
-│    └── council-master: mode "subagent", hidden          │
+│    └── councillor: mode "subagent", hidden              │
 └─────────────────────────────────────────────────────────┘
 ```

+ 1 - 2
docs/mcps.md

@@ -24,8 +24,7 @@ Built-in Model Context Protocol (MCP) servers ship with oh-my-opencode-slim and
 | `oracle` | none |
 | `explorer` | none |
 | `fixer` | none |
-| `councillor` | none |
-| `council-master` | none |
+ | `councillor` | none |
 
 ---
 

+ 6 - 40
oh-my-opencode-slim.schema.json

@@ -352,7 +352,7 @@
       }
     },
     "disabled_agents": {
-      "description": "Agent names to disable completely. Disabled agents are not instantiated and cannot be delegated to. Orchestrator and council internal agents (councillor, council-master) cannot be disabled. By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable.",
+      "description": "Agent names to disable completely. Disabled agents are not instantiated and cannot be delegated to. Orchestrator and council internal agents (councillor) cannot be disabled. By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable.",
       "type": "array",
       "items": {
         "type": "string"
@@ -578,26 +578,6 @@
     "council": {
       "type": "object",
       "properties": {
-        "master": {
-          "type": "object",
-          "properties": {
-            "model": {
-              "type": "string",
-              "pattern": "^[^/\\s]+\\/[^\\s]+$",
-              "description": "Model ID for the council master (e.g. \"anthropic/claude-opus-4-6\")"
-            },
-            "variant": {
-              "type": "string"
-            },
-            "prompt": {
-              "description": "Optional role/guidance injected into the master synthesis prompt",
-              "type": "string"
-            }
-          },
-          "required": [
-            "model"
-          ]
-        },
         "presets": {
           "type": "object",
           "propertyNames": {
@@ -617,28 +597,16 @@
             }
           }
         },
-        "master_timeout": {
-          "default": 300000,
-          "type": "number",
-          "minimum": 0
-        },
-        "councillors_timeout": {
+        "timeout": {
           "default": 180000,
           "type": "number",
-          "minimum": 0
+          "minimum": 0,
+          "description": "Councillor timeout (ms)"
         },
         "default_preset": {
           "default": "default",
           "type": "string"
         },
-        "master_fallback": {
-          "description": "Fallback models for the council master. Tried in order if the primary model fails. Example: [\"anthropic/claude-sonnet-4-6\", \"openai/gpt-5.4\"]",
-          "type": "array",
-          "items": {
-            "type": "string",
-            "pattern": "^[^/\\s]+\\/[^\\s]+$"
-          }
-        },
         "councillor_execution_mode": {
           "default": "parallel",
           "description": "Execution mode for councillors. \"serial\" runs them one at a time (required for single-model systems). \"parallel\" runs them concurrently (default, faster for multi-model systems).",
@@ -650,14 +618,12 @@
         },
         "councillor_retries": {
           "default": 3,
-          "description": "Number of retry attempts for councillors and master that return empty responses (e.g. due to provider rate limiting). Default: 3 retries.",
+          "description": "Number of retry attempts for councillors that return empty responses (e.g. due to provider rate limiting). Default: 3 retries.",
           "type": "integer",
           "minimum": 0,
-          "maximum": 5
-        }
+        "maximum": 5
       },
       "required": [
-        "master",
         "presets"
       ]
     }

+ 0 - 84
src/agents/council-master.test.ts

@@ -1,84 +0,0 @@
-import { describe, expect, test } from 'bun:test';
-import { createCouncilMasterAgent } from './council-master';
-
-describe('createCouncilMasterAgent', () => {
-  test('creates agent with correct name', () => {
-    const agent = createCouncilMasterAgent('test-model');
-    expect(agent.name).toBe('council-master');
-  });
-
-  test('creates agent with correct description', () => {
-    const agent = createCouncilMasterAgent('test-model');
-    expect(agent.description).toContain('Council synthesis engine');
-  });
-
-  test('sets model from argument', () => {
-    const agent = createCouncilMasterAgent('custom-model');
-    expect(agent.config.model).toBe('custom-model');
-  });
-
-  test('sets temperature to 0.1', () => {
-    const agent = createCouncilMasterAgent('test-model');
-    expect(agent.config.temperature).toBe(0.1);
-  });
-
-  test('sets default prompt when no custom prompts provided', () => {
-    const agent = createCouncilMasterAgent('test-model');
-    expect(agent.config.prompt).toContain(
-      'council master responsible for synthesizing',
-    );
-  });
-
-  test('uses custom prompt when provided', () => {
-    const customPrompt = 'You are a custom synthesizer.';
-    const agent = createCouncilMasterAgent('test-model', customPrompt);
-    expect(agent.config.prompt).toBe(customPrompt);
-    expect(agent.config.prompt).not.toContain('council master');
-  });
-
-  test('appends custom append prompt', () => {
-    const customAppendPrompt = 'Additional instructions here.';
-    const agent = createCouncilMasterAgent(
-      'test-model',
-      undefined,
-      customAppendPrompt,
-    );
-    expect(agent.config.prompt).toContain('council master');
-    expect(agent.config.prompt).toContain(customAppendPrompt);
-    expect(agent.config.prompt).toContain('Additional instructions here.');
-  });
-
-  test('custom prompt takes priority over append prompt', () => {
-    const customPrompt = 'Custom prompt only.';
-    const customAppendPrompt = 'Should be ignored.';
-    const agent = createCouncilMasterAgent(
-      'test-model',
-      customPrompt,
-      customAppendPrompt,
-    );
-    expect(agent.config.prompt).toBe(customPrompt);
-    expect(agent.config.prompt).not.toContain(customAppendPrompt);
-  });
-});
-
-describe('council-master permissions', () => {
-  test('denies all with single wildcard deny', () => {
-    const agent = createCouncilMasterAgent('test-model');
-    expect(agent.config.permission).toBeDefined();
-    expect((agent.config.permission as Record<string, string>)['*']).toBe(
-      'deny',
-    );
-  });
-
-  test('denies question explicitly', () => {
-    const agent = createCouncilMasterAgent('test-model');
-    const permission = agent.config.permission as Record<string, string>;
-    expect(permission.question).toBe('deny');
-  });
-
-  test('has exactly 2 permission entries', () => {
-    const agent = createCouncilMasterAgent('test-model');
-    const permission = agent.config.permission as Record<string, string>;
-    expect(Object.keys(permission)).toHaveLength(2);
-  });
-});

+ 0 - 70
src/agents/council-master.ts

@@ -1,70 +0,0 @@
-import { type AgentDefinition, resolvePrompt } from './orchestrator';
-
-/**
- * Council Master agent — pure synthesis engine.
- *
- * The master receives all councillor responses and produces the final
- * synthesized answer. It has NO tools — synthesis is a text-in/text-out
- * operation. Councillors already did the research.
- *
- * Permission model mirrors OpenCode's built-in compaction/title/summary
- * agents: deny all.
- */
-const COUNCIL_MASTER_PROMPT = `You are the council master responsible for \
-synthesizing responses from multiple AI models.
-
-**Role**: Review all councillor responses and create the optimal final answer.
-
-**Process**:
-1. Read the original user prompt
-2. Review each councillor's response carefully
-3. Identify the best elements from each response
-4. Resolve contradictions between councillors
-5. Synthesize a final, optimal response
-
-**Behavior**:
-- Each councillor had read-only access to the codebase — their responses may \
-  reference specific files, functions, and line numbers
-- Clearly explain your reasoning for the chosen approach
-- Be transparent about trade-offs
-- Credit specific insights from individual councillors by name
-- If councillors disagree, explain your resolution
-- Don't just average responses — choose and improve
-
-**Output**:
-- Present the synthesized solution
-- Review, retain, and include relevant code examples, diagrams, and concrete \
-  details from councillor responses
-- Explain your synthesis reasoning
-- Note any remaining uncertainties
-- Acknowledge if consensus was impossible`;
-
-export function createCouncilMasterAgent(
-  model: string,
-  customPrompt?: string,
-  customAppendPrompt?: string,
-): AgentDefinition {
-  const prompt = resolvePrompt(
-    COUNCIL_MASTER_PROMPT,
-    customPrompt,
-    customAppendPrompt,
-  );
-
-  return {
-    name: 'council-master',
-    description:
-      'Council synthesis engine. Receives councillor responses and produces the final answer. No tools, pure text synthesis.',
-    config: {
-      model,
-      temperature: 0.1,
-      prompt,
-      // Deny everything — pure synthesis, no tools needed.
-      // Explicit question:deny prevents applyDefaultPermissions from
-      // re-enabling it (it only preserves an existing 'deny' value).
-      permission: {
-        '*': 'deny',
-        question: 'deny',
-      },
-    },
-  };
-}

+ 212 - 0
src/agents/council.test.ts

@@ -0,0 +1,212 @@
+import { describe, expect, test } from 'bun:test';
+import { formatCouncillorPrompt, formatCouncillorResults } from './council';
+
+describe('formatCouncillorResults', () => {
+  const originalPrompt =
+    'What is the best way to implement a REST API in TypeScript?';
+
+  test('formats completed councillor results correctly', () => {
+    const councillorResults = [
+      {
+        name: 'alpha',
+        model: 'anthropic/claude-opus-4-6',
+        status: 'completed',
+        result: 'Use Express.js with TypeScript interfaces for type safety.',
+      },
+      {
+        name: 'beta',
+        model: 'openai/gpt-5.4',
+        status: 'completed',
+        result:
+          'Consider Fastify for better performance and built-in type validation.',
+      },
+    ];
+
+    const formatted = formatCouncillorResults(
+      originalPrompt,
+      councillorResults,
+    );
+
+    expect(formatted).toContain('**Original Prompt**:');
+    expect(formatted).toContain(originalPrompt);
+    expect(formatted).toContain('**alpha** (claude-opus-4-6):');
+    expect(formatted).toContain('**beta** (gpt-5.4):');
+    expect(formatted).toContain(
+      'Use Express.js with TypeScript interfaces for type safety.',
+    );
+    expect(formatted).toContain(
+      'Consider Fastify for better performance and built-in type validation.',
+    );
+    expect(formatted).toContain('**Councillor Responses**:');
+    expect(formatted).toContain(
+      'Synthesize the optimal response based on the above.',
+    );
+    expect(formatted).not.toContain('**Failed/Timed-out Councillors**:');
+  });
+
+  test('includes failed councillors section when some fail', () => {
+    const councillorResults = [
+      {
+        name: 'alpha',
+        model: 'anthropic/claude-opus-4-6',
+        status: 'completed',
+        result: 'Use Express.js with TypeScript interfaces for type safety.',
+      },
+      {
+        name: 'beta',
+        model: 'openai/gpt-5.4',
+        status: 'timed_out',
+        error: 'Request timed out after 180000ms',
+      },
+      {
+        name: 'gamma',
+        model: 'google/gemini-pro',
+        status: 'failed',
+        error: 'Provider returned empty response',
+      },
+    ];
+
+    const formatted = formatCouncillorResults(
+      originalPrompt,
+      councillorResults,
+    );
+
+    expect(formatted).toContain('**Councillor Responses**:');
+    expect(formatted).toContain('**alpha** (claude-opus-4-6):');
+    expect(formatted).toContain(
+      'Use Express.js with TypeScript interfaces for type safety.',
+    );
+    expect(formatted).toContain('**Failed/Timed-out Councillors**:');
+    expect(formatted).toContain(
+      '**beta**: timed_out — Request timed out after 180000ms',
+    );
+    expect(formatted).toContain(
+      '**gamma**: failed — Provider returned empty response',
+    );
+    expect(formatted).not.toContain('**beta** (gpt-5.4):');
+    expect(formatted).not.toContain('**gamma** (gemini-pro):');
+  });
+
+  test('returns fallback message when all councillors fail', () => {
+    const councillorResults = [
+      {
+        name: 'alpha',
+        model: 'anthropic/claude-opus-4-6',
+        status: 'timeout',
+        error: 'Request timed out',
+      },
+      {
+        name: 'beta',
+        model: 'openai/gpt-5.4',
+        status: 'error',
+        error: 'Provider error',
+      },
+    ];
+
+    const formatted = formatCouncillorResults(
+      originalPrompt,
+      councillorResults,
+    );
+
+    expect(formatted).toContain('**Original Prompt**:');
+    expect(formatted).toContain(originalPrompt);
+    expect(formatted).toContain('**Councillor Responses**:');
+    expect(formatted).toContain('All councillors failed to produce output:');
+    expect(formatted).toContain('**alpha** (claude-opus-4-6):');
+    expect(formatted).toContain('**beta** (gpt-5.4):');
+    expect(formatted).toContain('Request timed out');
+    expect(formatted).toContain('Provider error');
+  });
+
+  test('handles councillors with result but completed status', () => {
+    const councillorResults = [
+      {
+        name: 'alpha',
+        model: 'anthropic/claude-opus-4-6',
+        status: 'completed',
+        result: 'Valid response',
+      },
+      {
+        name: 'beta',
+        model: 'openai/gpt-5.4',
+        status: 'completed',
+        result: 'Another valid response',
+      },
+    ];
+
+    const formatted = formatCouncillorResults(
+      originalPrompt,
+      councillorResults,
+    );
+
+    expect(formatted).toContain('**alpha** (claude-opus-4-6):');
+    expect(formatted).toContain('Valid response');
+    expect(formatted).toContain('**beta** (gpt-5.4):');
+    expect(formatted).toContain('Another valid response');
+    expect(formatted).toContain(
+      'Synthesize the optimal response based on the above.',
+    );
+  });
+});
+
+describe('formatCouncillorPrompt', () => {
+  const userPrompt = 'How do I implement async/await in TypeScript?';
+
+  test('returns user prompt unchanged when no councillor prompt is provided', () => {
+    const formatted = formatCouncillorPrompt(userPrompt);
+    expect(formatted).toBe(userPrompt);
+  });
+
+  test('prepends councillor prompt with separator when provided', () => {
+    const councillorPrompt =
+      'You are a TypeScript expert. Focus on practical examples.';
+    const formatted = formatCouncillorPrompt(userPrompt, councillorPrompt);
+
+    expect(formatted).toContain(councillorPrompt);
+    expect(formatted).toContain(userPrompt);
+    expect(formatted).toContain('---');
+    expect(formatted).toMatch(
+      new RegExp(
+        `^${councillorPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n\\n---\\n\\n${userPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
+      ),
+    );
+  });
+
+  test('handles multiline councillor prompt', () => {
+    const councillorPrompt =
+      'You are an expert.\nFocus on clarity.\nProvide code examples.';
+    const formatted = formatCouncillorPrompt(userPrompt, councillorPrompt);
+
+    expect(formatted).toContain(councillorPrompt);
+    expect(formatted).toContain(userPrompt);
+    expect(formatted).toContain('---');
+    expect(formatted).toMatch(
+      new RegExp(
+        `^${councillorPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n\\n---\\n\\n${userPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
+      ),
+    );
+  });
+
+  test('handles empty councillor prompt', () => {
+    const formatted = formatCouncillorPrompt(userPrompt, '');
+    expect(formatted).toBe(userPrompt);
+  });
+
+  test('handles multiline user prompt with councillor prompt', () => {
+    const councillorPrompt = 'You are an expert.';
+    const multilineUserPrompt = 'Line 1\nLine 2\nLine 3';
+    const formatted = formatCouncillorPrompt(
+      multilineUserPrompt,
+      councillorPrompt,
+    );
+
+    expect(formatted).toContain(councillorPrompt);
+    expect(formatted).toContain(multilineUserPrompt);
+    expect(formatted).toContain('---');
+    expect(formatted).toMatch(
+      new RegExp(
+        `^${councillorPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n\\n---\\n\\n${multilineUserPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
+      ),
+    );
+  });
+});

+ 38 - 22
src/agents/council.ts

@@ -1,10 +1,9 @@
 import { shortModelLabel } from '../utils/session';
 import { type AgentDefinition, resolvePrompt } from './orchestrator';
 
-// NOTE: Councillor and master system prompts live in their respective agent
-// factories (councillor.ts, council-master.ts). The format functions below
-// only structure the USER message content — the agent factory provides the
-// system prompt. This avoids duplicate system prompts (Oracle finding #1/#2).
+// NOTE: Councillor system prompts live in the councillor agent factory.
+// The format functions below only structure the USER message content — the
+// agent factory provides the system prompt.
 
 const COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM \
 orchestration system that runs consensus across multiple models.
@@ -19,14 +18,28 @@ orchestration system that runs consensus across multiple models.
 **Usage**:
 1. Call the \`council_session\` tool with the user's prompt
 2. Optionally specify a preset (default: "default")
-3. Receive the synthesized response from the council master
-4. Present the result to the user
+3. Receive the councillor responses formatted for synthesis
+4. Synthesize the optimal final answer from the councillor responses
+5. Present the synthesized result to the user
+
+**Synthesis Guidelines**:
+When you receive councillor responses, synthesize them into the optimal final answer:
+- Review all councillor responses thoroughly and create the best possible answer
+- Credit specific insights from individual councillors by name (e.g., "alpha noted that...", "beta suggested...")
+- Clearly explain your reasoning for the chosen approach
+- Be transparent about trade-offs when different approaches have valid pros/cons
+- Note any remaining uncertainties or areas where further investigation is needed
+- If councillors disagree, explain the resolution and your reasoning
+- Acknowledge if consensus was impossible and explain why
+- Don't just average responses — choose the best approach and improve upon it
+- Present the synthesized solution with relevant code examples, concrete details, and clear explanations
 
 **Behavior**:
 - Delegate requests directly to council_session
-- Don't pre-analyze or filter the prompt
-- Present the synthesized result verbatim — do not re-summarize or condense
-- Briefly explain the consensus if requested`;
+- Don't pre-analyze or filter the prompt before calling council_session
+- Synthesize the councillor results into a comprehensive, coherent answer
+- Include attribution for valuable insights from specific councillors
+- If councillors disagree, explain why you chose one approach over another`;
 
 export function createCouncilAgent(
   model: string,
@@ -76,15 +89,14 @@ export function formatCouncillorPrompt(
 }
 
 /**
- * Build the synthesis prompt for the council master.
+ * Format councillor results for the council agent to synthesize.
  *
- * Formats councillor results as structured data — the agent factory
- * (council-master.ts) provides the system prompt with synthesis instructions.
- * Returns a special prompt when all councillors failed to produce output.
- *
- * @param masterPrompt - Optional per-master guidance appended to the synthesis.
+ * Formats councillor results as structured data that the council agent
+ * (which called the tool) will receive as the tool response. The council
+ * agent's system prompt contains synthesis instructions.
+ * Returns a special message when all councillors failed to produce output.
  */
-export function formatMasterSynthesisPrompt(
+export function formatCouncillorResults(
   originalPrompt: string,
   councillorResults: Array<{
     name: string;
@@ -93,7 +105,6 @@ export function formatMasterSynthesisPrompt(
     result?: string;
     error?: string;
   }>,
-  masterPrompt?: string,
 ): string {
   const completedWithResults = councillorResults.filter(
     (cr) => cr.status === 'completed' && cr.result,
@@ -111,8 +122,17 @@ export function formatMasterSynthesisPrompt(
     .map((cr) => `**${cr.name}**: ${cr.status} — ${cr.error ?? 'Unknown'}`)
     .join('\n');
 
+  // Defensive guard: caller (runCouncil) short-circuits when all fail,
+  // but this function may be reused in other contexts.
   if (completedWithResults.length === 0) {
-    return `---\n\n**Original Prompt**:\n${originalPrompt}\n\n---\n\n**Councillor Responses**:\nAll councillors failed to produce output. Please generate a response based on the original prompt alone.`;
+    const errorDetails = councillorResults
+      .map(
+        (cr) =>
+          `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} — ${cr.error ?? 'Unknown'}`,
+      )
+      .join('\n');
+
+    return `---\n\n**Original Prompt**:\n${originalPrompt}\n\n---\n\n**Councillor Responses**:\nAll councillors failed to produce output:\n${errorDetails}\n\nPlease generate a response based on the original prompt alone.`;
   }
 
   let prompt = `---\n\n**Original Prompt**:\n${originalPrompt}\n\n---\n\n**Councillor Responses**:\n${councillorSection}`;
@@ -123,9 +143,5 @@ export function formatMasterSynthesisPrompt(
 
   prompt += '\n\n---\n\nSynthesize the optimal response based on the above.';
 
-  if (masterPrompt) {
-    prompt += `\n\n---\n\n**Master Guidance**:\n${masterPrompt}`;
-  }
-
   return prompt;
 }

+ 0 - 3
src/agents/display-name.test.ts

@@ -189,15 +189,12 @@ describe('displayName', () => {
       disabled_agents: [],
       agents: {
         councillor: { displayName: 'reviewer' },
-        'council-master': { displayName: 'arbiter' },
       },
     };
 
     const sdkConfigs = getAgentConfigs(config);
 
     expect(sdkConfigs.reviewer).toBeUndefined();
-    expect(sdkConfigs.arbiter).toBeUndefined();
     expect(sdkConfigs.councillor?.hidden).toBe(true);
-    expect(sdkConfigs['council-master']?.hidden).toBe(true);
   });
 });

+ 10 - 75
src/agents/index.test.ts

@@ -318,9 +318,9 @@ describe('createAgents', () => {
     expect(names).toContain('fixer');
   });
 
-  test('creates exactly 9 agents by default (1 orchestrator + 8 subagents, observer disabled)', () => {
+  test('creates exactly 8 agents by default (1 orchestrator + 7 subagents, observer disabled)', () => {
     const agents = createAgents();
-    expect(agents.length).toBe(9);
+    expect(agents.length).toBe(8);
   });
 });
 
@@ -342,76 +342,13 @@ describe('getAgentConfigs', () => {
 });
 
 describe('council agent model resolution', () => {
-  test('council agent uses config.council.master.model', () => {
-    const config = {
-      council: {
-        master: { model: 'anthropic/claude-sonnet-4-6' },
-        presets: {
-          default: {
-            councillors: {
-              alpha: { model: 'test/alpha-model' },
-            },
-            master: undefined,
-          },
-        },
-      },
-    } as unknown as PluginConfig;
-    const agents = createAgents(config);
-    const council = agents.find((a) => a.name === 'council');
-    expect(council?.config.model).toBe('anthropic/claude-sonnet-4-6');
-  });
-
-  test('council agent falls back to default without council config', () => {
+  test('council agent uses default model', () => {
     const agents = createAgents();
     const council = agents.find((a) => a.name === 'council');
     expect(council?.config.model).toBe(DEFAULT_MODELS.council);
   });
 
-  test('council-master agent uses config.council.master.model', () => {
-    const config = {
-      council: {
-        master: { model: 'anthropic/claude-sonnet-4-6' },
-        presets: {
-          default: {
-            councillors: {
-              alpha: { model: 'test/alpha-model' },
-            },
-            master: undefined,
-          },
-        },
-      },
-    } as unknown as PluginConfig;
-    const agents = createAgents(config);
-    const councilMaster = agents.find((a) => a.name === 'council-master');
-    expect(councilMaster?.config.model).toBe('anthropic/claude-sonnet-4-6');
-  });
-
-  test('council-master agent falls back to default without council config', () => {
-    const agents = createAgents();
-    const councilMaster = agents.find((a) => a.name === 'council-master');
-    expect(councilMaster?.config.model).toBe(DEFAULT_MODELS['council-master']);
-  });
-
-  test('councillor agent uses config.council.master.model', () => {
-    const config = {
-      council: {
-        master: { model: 'anthropic/claude-sonnet-4-6' },
-        presets: {
-          default: {
-            councillors: {
-              alpha: { model: 'test/alpha-model' },
-            },
-            master: undefined,
-          },
-        },
-      },
-    } as unknown as PluginConfig;
-    const agents = createAgents(config);
-    const councillor = agents.find((a) => a.name === 'councillor');
-    expect(councillor?.config.model).toBe('anthropic/claude-sonnet-4-6');
-  });
-
-  test('councillor agent falls back to default without council config', () => {
+  test('councillor agent uses default model', () => {
     const agents = createAgents();
     const councillor = agents.find((a) => a.name === 'councillor');
     expect(councillor?.config.model).toBe(DEFAULT_MODELS.councillor);
@@ -584,36 +521,34 @@ describe('disabled_agents', () => {
 
   test('protected agents cannot be disabled', () => {
     const config: PluginConfig = {
-      disabled_agents: ['orchestrator', 'councillor', 'council-master'],
+      disabled_agents: ['orchestrator', 'councillor'],
     };
     const agents = createAgents(config);
     const names = agents.map((a) => a.name);
     expect(names).toContain('orchestrator');
     expect(names).toContain('councillor');
-    expect(names).toContain('council-master');
   });
 
-  test('disabling council disables all council agents', () => {
+  test('disabling council disables council agent', () => {
     const config: PluginConfig = {
       disabled_agents: ['council'],
     };
     const agents = createAgents(config);
     const names = agents.map((a) => a.name);
     expect(names).not.toContain('council');
-    // councillor and council-master are protected, they stay
+    // councillor is protected, it stays
     expect(names).toContain('councillor');
-    expect(names).toContain('council-master');
   });
 
   test('agent count decreases when agents are disabled', () => {
     const agents = createAgents();
-    expect(agents.length).toBe(9); // 1 + 8 (observer disabled by default)
+    expect(agents.length).toBe(8); // 1 + 7 (observer disabled by default)
 
     const disabledConfig: PluginConfig = {
       disabled_agents: ['observer', 'designer'],
     };
     const disabledAgents = createAgents(disabledConfig);
-    expect(disabledAgents.length).toBe(8);
+    expect(disabledAgents.length).toBe(7);
   });
 
   test('getDisabledAgents respects protection rules', () => {
@@ -642,7 +577,7 @@ describe('disabled_agents', () => {
       disabled_agents: [],
     };
     const agents = createAgents(config);
-    expect(agents.length).toBe(10);
+    expect(agents.length).toBe(9);
     expect(agents.map((a) => a.name)).toContain('observer');
   });
 });

+ 5 - 19
src/agents/index.ts

@@ -14,7 +14,6 @@ import {
 import { getAgentMcpList } from '../config/agent-mcps';
 
 import { createCouncilAgent } from './council';
-import { createCouncilMasterAgent } from './council-master';
 import { createCouncillorAgent } from './councillor';
 import { createDesignerAgent } from './designer';
 import { createExplorerAgent } from './explorer';
@@ -97,7 +96,7 @@ function injectDisplayNames(
  * If configuredSkills is provided, it honors that list instead of defaults.
  *
  * Note: If the agent already explicitly sets question to 'deny', that is
- * respected (e.g. councillor and council-master should not ask questions).
+ * respected (e.g. councillor should not ask questions).
  */
 function applyDefaultPermissions(
   agent: AgentDefinition,
@@ -114,7 +113,7 @@ function applyDefaultPermissions(
     configuredSkills,
   );
 
-  // Respect explicit deny on question (councillor, council-master)
+  // Respect explicit deny on question (councillor)
   const questionPerm = existing.question === 'deny' ? 'deny' : 'allow';
 
   agent.config.permission = {
@@ -147,7 +146,6 @@ const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
   observer: createObserverAgent,
   council: createCouncilAgent,
   councillor: createCouncillorAgent,
-  'council-master': createCouncilMasterAgent,
 };
 
 // Public API
@@ -176,17 +174,6 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
       }
       return librarianModel ?? (DEFAULT_MODELS.librarian as string);
     }
-    // Council and council-master agents' model comes from
-    // config.council.master.model so the TUI validates the user's
-    // actual model, not the hardcoded default
-    if (
-      (name === 'council' ||
-        name === 'council-master' ||
-        name === 'councillor') &&
-      config?.council?.master?.model
-    ) {
-      return config.council.master.model;
-    }
     // Subagents always have a defined default model; cast is safe here
     return DEFAULT_MODELS[name] as string;
   };
@@ -293,8 +280,8 @@ export function getAgentConfigs(
       // Council is callable both as a primary agent (user-facing)
       // and as a subagent (orchestrator can delegate to it)
       sdkConfig.mode = 'all';
-    } else if (name === 'councillor' || name === 'council-master') {
-      // Internal agents — subagent mode, hidden from @ autocomplete
+    } else if (name === 'councillor') {
+      // Internal agent — subagent mode, hidden from @ autocomplete
       sdkConfig.mode = 'subagent';
       sdkConfig.hidden = true;
     } else if (isSubagent(name)) {
@@ -304,8 +291,7 @@ export function getAgentConfigs(
     }
   };
 
-  const isInternalOnly = (name: string): boolean =>
-    name === 'councillor' || name === 'council-master';
+  const isInternalOnly = (name: string): boolean => name === 'councillor';
 
   const entries: Array<[string, SDKAgentConfig]> = [];
 

+ 2 - 2
src/agents/orchestrator.ts

@@ -74,10 +74,10 @@ const AGENT_DESCRIPTIONS: Record<string, string> = {
 - Role: Multi-LLM consensus engine for high-confidence answers
 - Permissions: Read files
 - Stats: 3x slower than orchestrator, 3x or more cost of orchestrator
-- Capabilities: Runs multiple models in parallel, synthesizes their responses via a council master
+- Capabilities: Runs multiple models in parallel, synthesizes their responses into a consensus answer
 - **Delegate when:** Critical decisions needing diverse model perspectives • High-stakes architectural choices where consensus reduces risk • Ambiguous problems where multi-model disagreement is informative • Security-sensitive design reviews
 - **Don't delegate when:** Straightforward tasks you're confident about • Speed matters more than confidence • Single-model answer is sufficient • Routine implementation work
-- **Result handling:** Present the council's synthesized response verbatim. Do not re-summarize — the council master has already produced the final answer.
+- **Result handling:** Present the council's synthesized response verbatim. Do not re-summarize or condense.
 - **Rule of thumb:** Need second/third opinions from different models? → @council. One good answer enough? → yourself.`,
 
   observer: `@observer

+ 0 - 1
src/config/agent-mcps.ts

@@ -17,7 +17,6 @@ export const DEFAULT_AGENT_MCPS: Record<AgentName, string[]> = {
   observer: [],
   council: [],
   councillor: [],
-  'council-master': [],
 };
 
 /**

+ 2 - 9
src/config/constants.ts

@@ -13,7 +13,6 @@ export const SUBAGENT_NAMES = [
   'observer',
   'council',
   'councillor',
-  'council-master',
 ] as const;
 
 export const ORCHESTRATOR_NAME = 'orchestrator' as const;
@@ -30,7 +29,7 @@ export type AgentName = (typeof ALL_AGENT_NAMES)[number];
 // explorer/librarian/oracle: cannot spawn any subagents (leaf nodes)
 // Unknown agent types not listed here default to explorer-only access
 // Which agents each agent type can spawn via delegation.
-// councillor and council-master are internal — only CouncilManager spawns them.
+// councillor is internal — only CouncilManager spawns it.
 export const ORCHESTRATABLE_AGENTS = [
   'explorer',
   'librarian',
@@ -42,11 +41,7 @@ export const ORCHESTRATABLE_AGENTS = [
 ] as const;
 
 /** Agents that cannot be disabled even if listed in disabled_agents config. */
-export const PROTECTED_AGENTS = new Set([
-  'orchestrator',
-  'councillor',
-  'council-master',
-]);
+export const PROTECTED_AGENTS = new Set(['orchestrator', 'councillor']);
 
 /**
  * Get the list of orchestratable agents, excluding any disabled agents.
@@ -68,7 +63,6 @@ export const SUBAGENT_DELEGATION_RULES: Record<AgentName, readonly string[]> = {
   observer: [],
   council: [],
   councillor: [],
-  'council-master': [],
 };
 
 // Default models for each agent
@@ -83,7 +77,6 @@ export const DEFAULT_MODELS: Record<AgentName, string | undefined> = {
   observer: 'openai/gpt-5.4-mini',
   council: 'openai/gpt-5.4-mini',
   councillor: 'openai/gpt-5.4-mini',
-  'council-master': 'openai/gpt-5.4-mini',
 };
 
 // Polling configuration

+ 162 - 264
src/config/council-schema.test.ts

@@ -3,10 +3,7 @@ import {
   CouncilConfigSchema,
   type CouncillorConfig,
   CouncillorConfigSchema,
-  type CouncilMasterConfig,
-  CouncilMasterConfigSchema,
   CouncilPresetSchema,
-  PresetMasterOverrideSchema,
 } from './council-schema';
 
 describe('CouncillorConfigSchema', () => {
@@ -23,119 +20,172 @@ describe('CouncillorConfigSchema', () => {
     }
   });
 
-  test('validates config with only required model field', () => {
-    const minimalConfig: CouncillorConfig = {
-      model: 'openai/gpt-5.4-mini',
+  test('accepts deprecated master fields and reports them', () => {
+    const config = {
+      master: { model: 'anthropic/claude-opus-4-6' },
+      master_timeout: 300000,
+      master_fallback: ['openai/gpt-5.4'],
+      presets: {
+        default: {
+          alpha: { model: 'openai/gpt-5.4-mini' },
+        },
+      },
     };
 
-    const result = CouncillorConfigSchema.safeParse(minimalConfig);
+    const result = CouncilConfigSchema.safeParse(config);
     expect(result.success).toBe(true);
-  });
-
-  test('rejects missing model', () => {
-    const badConfig = {
-      variant: 'low',
-    };
 
-    const result = CouncillorConfigSchema.safeParse(badConfig);
-    expect(result.success).toBe(false);
+    if (result.success) {
+      // Deprecated fields are stripped but reported via _deprecated
+      expect(result.data._deprecated).toEqual([
+        'master',
+        'master_timeout',
+        'master_fallback',
+      ]);
+      // Core fields still work normally
+      expect(result.data.timeout).toBe(180000);
+      expect(Object.keys(result.data.presets.default)).toEqual(['alpha']);
+    }
   });
 
-  test('rejects empty model string', () => {
+  test('no _deprecated when config has no deprecated fields', () => {
     const config = {
-      model: '',
-    };
-
-    const result = CouncillorConfigSchema.safeParse(config);
-    expect(result.success).toBe(false);
-  });
-
-  test('accepts optional prompt field', () => {
-    const config: CouncillorConfig = {
-      model: 'openai/gpt-5.4-mini',
-      prompt: 'Focus on security implications and edge cases.',
+      presets: {
+        default: {
+          alpha: { model: 'openai/gpt-5.4-mini' },
+        },
+      },
     };
 
-    const result = CouncillorConfigSchema.safeParse(config);
+    const result = CouncilConfigSchema.safeParse(config);
     expect(result.success).toBe(true);
+
     if (result.success) {
-      expect(result.data.prompt).toBe(
-        'Focus on security implications and edge cases.',
-      );
+      expect(result.data._deprecated).toBeUndefined();
     }
   });
+});
 
-  test('prompt is optional and defaults to undefined', () => {
-    const config: CouncillorConfig = {
-      model: 'openai/gpt-5.4-mini',
-    };
+test('preset with only legacy "master" key results in empty councillors', () => {
+  const config = {
+    presets: {
+      'master-only': {
+        master: { model: 'anthropic/claude-opus-4-6' },
+      },
+    },
+  };
 
-    const result = CouncillorConfigSchema.safeParse(config);
-    expect(result.success).toBe(true);
-    if (result.success) {
-      expect(result.data.prompt).toBeUndefined();
-    }
-  });
+  const result = CouncilConfigSchema.safeParse(config);
+  expect(result.success).toBe(true);
+
+  if (result.success) {
+    const preset = result.data.presets['master-only'];
+    expect(Object.keys(preset)).toEqual([]);
+  }
 });
 
-describe('CouncilMasterConfigSchema', () => {
-  test('validates good config', () => {
-    const goodConfig: CouncilMasterConfig = {
-      model: 'anthropic/claude-opus-4-6',
-      variant: 'high',
-    };
+test('unwraps legacy nested "councillors" key in preset', () => {
+  const config = {
+    presets: {
+      default: {
+        councillors: {
+          alpha: { model: 'openai/gpt-5.4-mini' },
+          beta: { model: 'openai/gpt-5.3-codex' },
+        },
+      },
+    },
+  };
+
+  const result = CouncilConfigSchema.safeParse(config);
+  expect(result.success).toBe(true);
+
+  if (result.success) {
+    const preset = result.data.presets.default;
+    expect(Object.keys(preset)).toEqual(['alpha', 'beta']);
+    expect(preset.alpha.model).toBe('openai/gpt-5.4-mini');
+    expect(preset.beta.model).toBe('openai/gpt-5.3-codex');
+  }
+});
 
-    const result = CouncilMasterConfigSchema.safeParse(goodConfig);
-    expect(result.success).toBe(true);
-    if (result.success) {
-      expect(result.data).toEqual(goodConfig);
-    }
-  });
+test('mixed legacy "councillors" and flat keys in same preset', () => {
+  const config = {
+    presets: {
+      mixed: {
+        councillors: {
+          alpha: { model: 'openai/gpt-5.4-mini' },
+        },
+        beta: { model: 'google/gemini-3-pro' },
+      },
+    },
+  };
 
-  test('validates config with only required model field', () => {
-    const minimalConfig: CouncilMasterConfig = {
-      model: 'anthropic/claude-opus-4-6',
-    };
+  const result = CouncilConfigSchema.safeParse(config);
+  expect(result.success).toBe(true);
 
-    const result = CouncilMasterConfigSchema.safeParse(minimalConfig);
-    expect(result.success).toBe(true);
-  });
+  if (result.success) {
+    const preset = result.data.presets.mixed;
+    expect(Object.keys(preset).sort()).toEqual(['alpha', 'beta']);
+  }
+});
 
-  test('rejects missing model', () => {
-    const badConfig = {
-      variant: 'high',
-    };
+test('deprecated master with non-standard model ID still parses', () => {
+  const config = {
+    master: { model: 'claude-opus-4-6' }, // no provider/ prefix
+    master_timeout: 'fast', // not a number
+    master_fallback: 'all', // not an array
+    presets: {
+      default: {
+        alpha: { model: 'openai/gpt-5.4-mini' },
+      },
+    },
+  };
+
+  const result = CouncilConfigSchema.safeParse(config);
+  expect(result.success).toBe(true);
+
+  if (result.success) {
+    expect(result.data._deprecated).toEqual([
+      'master',
+      'master_timeout',
+      'master_fallback',
+    ]);
+  }
+});
 
-    const result = CouncilMasterConfigSchema.safeParse(badConfig);
-    expect(result.success).toBe(false);
-  });
+test('rejects empty model string', () => {
+  const config = {
+    model: '',
+  };
 
-  test('accepts optional prompt field', () => {
-    const config: CouncilMasterConfig = {
-      model: 'anthropic/claude-opus-4-6',
-      prompt: 'Prioritize correctness over creativity. When in doubt, flag it.',
-    };
+  const result = CouncillorConfigSchema.safeParse(config);
+  expect(result.success).toBe(false);
+});
 
-    const result = CouncilMasterConfigSchema.safeParse(config);
-    expect(result.success).toBe(true);
-    if (result.success) {
-      expect(result.data.prompt).toBe(
-        'Prioritize correctness over creativity. When in doubt, flag it.',
-      );
-    }
-  });
+test('accepts optional prompt field', () => {
+  const config: CouncillorConfig = {
+    model: 'openai/gpt-5.4-mini',
+    prompt: 'Focus on security implications and edge cases.',
+  };
+
+  const result = CouncillorConfigSchema.safeParse(config);
+  expect(result.success).toBe(true);
+  if (result.success) {
+    expect(result.data.prompt).toBe(
+      'Focus on security implications and edge cases.',
+    );
+  }
+});
 
-  test('prompt defaults to undefined when not provided', () => {
-    const config: CouncilMasterConfig = {
-      model: 'anthropic/claude-opus-4-6',
-    };
+test('prompt is optional and defaults to undefined', () => {
+  const config: CouncillorConfig = {
+    model: 'openai/gpt-5.4-mini',
+  };
 
-    const result = CouncilMasterConfigSchema.safeParse(config);
-    expect(result.success).toBe(true);
-    if (result.success) {
-      expect(result.data.prompt).toBeUndefined();
-    }
-  });
+  const result = CouncillorConfigSchema.safeParse(config);
+  expect(result.success).toBe(true);
+  if (result.success) {
+    expect(result.data.prompt).toBeUndefined();
+  }
 });
 
 describe('CouncilPresetSchema', () => {
@@ -156,11 +206,7 @@ describe('CouncilPresetSchema', () => {
     const result = CouncilPresetSchema.safeParse(raw);
     expect(result.success).toBe(true);
     if (result.success) {
-      expect(Object.keys(result.data.councillors)).toEqual([
-        'alpha',
-        'beta',
-        'gamma',
-      ]);
+      expect(Object.keys(result.data)).toEqual(['alpha', 'beta', 'gamma']);
     }
   });
 
@@ -174,7 +220,7 @@ describe('CouncilPresetSchema', () => {
     const result = CouncilPresetSchema.safeParse(raw);
     expect(result.success).toBe(true);
     if (result.success) {
-      expect(Object.keys(result.data.councillors)).toEqual(['solo']);
+      expect(Object.keys(result.data)).toEqual(['solo']);
     }
   });
 
@@ -184,58 +230,14 @@ describe('CouncilPresetSchema', () => {
     const result = CouncilPresetSchema.safeParse(raw);
     expect(result.success).toBe(true);
     if (result.success) {
-      expect(result.data.councillors).toEqual({});
-    }
-  });
-
-  test('separates master key from councillors', () => {
-    const raw = {
-      master: { model: 'openai/gpt-5.4', prompt: 'Override prompt.' },
-      alpha: { model: 'openai/gpt-5.4-mini' },
-      beta: { model: 'google/gemini-3-pro' },
-    };
-
-    const result = CouncilPresetSchema.safeParse(raw);
-    expect(result.success).toBe(true);
-    if (result.success) {
-      expect(Object.keys(result.data.councillors)).toEqual(['alpha', 'beta']);
-      expect(result.data.master).toEqual({
-        model: 'openai/gpt-5.4',
-        prompt: 'Override prompt.',
-      });
-    }
-  });
-
-  test('preset without master key has no master override', () => {
-    const raw = {
-      alpha: { model: 'openai/gpt-5.4-mini' },
-    };
-
-    const result = CouncilPresetSchema.safeParse(raw);
-    expect(result.success).toBe(true);
-    if (result.success) {
-      expect(Object.keys(result.data.councillors)).toEqual(['alpha']);
-      expect(result.data.master).toBeUndefined();
+      expect(result.data).toEqual({});
     }
   });
-
-  test('rejects invalid master override in preset', () => {
-    const raw = {
-      master: { model: 'invalid-no-slash' },
-      alpha: { model: 'openai/gpt-5.4-mini' },
-    };
-
-    const result = CouncilPresetSchema.safeParse(raw);
-    expect(result.success).toBe(false);
-  });
 });
 
 describe('CouncilConfigSchema', () => {
   test('validates complete config with defaults', () => {
     const config = {
-      master: {
-        model: 'anthropic/claude-opus-4-6',
-      },
       presets: {
         default: {
           alpha: { model: 'openai/gpt-5.4-mini' },
@@ -250,17 +252,13 @@ describe('CouncilConfigSchema', () => {
 
     if (result.success) {
       // Check defaults are filled in
-      expect(result.data.master_timeout).toBe(300000);
-      expect(result.data.councillors_timeout).toBe(180000);
+      expect(result.data.timeout).toBe(180000);
       expect(result.data.default_preset).toBe('default');
     }
   });
 
   test('fills in defaults for optional fields', () => {
     const config = {
-      master: {
-        model: 'anthropic/claude-opus-4-6',
-      },
       presets: {
         custom: {
           alpha: { model: 'openai/gpt-5.4-mini' },
@@ -273,64 +271,26 @@ describe('CouncilConfigSchema', () => {
     expect(result.success).toBe(true);
 
     if (result.success) {
-      expect(result.data.master_timeout).toBe(300000);
-      expect(result.data.councillors_timeout).toBe(180000);
+      expect(result.data.timeout).toBe(180000);
       expect(result.data.default_preset).toBe('custom');
     }
   });
 
-  test('rejects missing master config', () => {
-    const badConfig = {
-      presets: {
-        default: {
-          alpha: { model: 'openai/gpt-5.4-mini' },
-        },
-      },
-    };
-
-    const result = CouncilConfigSchema.safeParse(badConfig);
-    expect(result.success).toBe(false);
-  });
-
   test('rejects missing presets', () => {
-    const badConfig = {
-      master: {
-        model: 'anthropic/claude-opus-4-6',
-      },
-    };
-
-    const result = CouncilConfigSchema.safeParse(badConfig);
-    expect(result.success).toBe(false);
-  });
-
-  test('rejects invalid master_timeout (negative)', () => {
-    const badConfig = {
-      master: {
-        model: 'anthropic/claude-opus-4-6',
-      },
-      presets: {
-        default: {
-          alpha: { model: 'openai/gpt-5.4-mini' },
-        },
-      },
-      master_timeout: -1000,
-    };
+    const badConfig = {};
 
     const result = CouncilConfigSchema.safeParse(badConfig);
     expect(result.success).toBe(false);
   });
 
-  test('rejects invalid councillors_timeout (negative)', () => {
+  test('rejects invalid timeout (negative)', () => {
     const badConfig = {
-      master: {
-        model: 'anthropic/claude-opus-4-6',
-      },
       presets: {
         default: {
           alpha: { model: 'openai/gpt-5.4-mini' },
         },
       },
-      councillors_timeout: -1000,
+      timeout: -1000,
     };
 
     const result = CouncilConfigSchema.safeParse(badConfig);
@@ -339,32 +299,35 @@ describe('CouncilConfigSchema', () => {
 
   test('accepts zero timeout values (no timeout)', () => {
     const config = {
-      master: {
-        model: 'anthropic/claude-opus-4-6',
-      },
       presets: {
         default: {
           alpha: { model: 'openai/gpt-5.4-mini' },
         },
       },
-      master_timeout: 0,
-      councillors_timeout: 0,
+      timeout: 0,
     };
 
     const result = CouncilConfigSchema.safeParse(config);
     expect(result.success).toBe(true);
 
     if (result.success) {
-      expect(result.data.master_timeout).toBe(0);
-      expect(result.data.councillors_timeout).toBe(0);
+      expect(result.data.timeout).toBe(0);
     }
   });
 
-  test('accepts multiple presets', () => {
-    const config = {
+  test('rejects missing presets', () => {
+    const badConfig = {
       master: {
         model: 'anthropic/claude-opus-4-6',
       },
+    };
+
+    const result = CouncilConfigSchema.safeParse(badConfig);
+    expect(result.success).toBe(false);
+  });
+
+  test('accepts multiple presets', () => {
+    const config = {
       presets: {
         default: {
           alpha: { model: 'openai/gpt-5.4-mini' },
@@ -389,76 +352,11 @@ describe('CouncilConfigSchema', () => {
     if (result.success) {
       // Verify prompt is preserved (not silently stripped)
       const thoroughPreset = result.data.presets.thorough;
-      expect(thoroughPreset.councillors.detailed1.prompt).toBe(
+      expect(thoroughPreset.detailed1.prompt).toBe(
         'Provide detailed analysis with citations.',
       );
       // Verify prompt is undefined when not set
-      expect(thoroughPreset.councillors.detailed2.prompt).toBeUndefined();
+      expect(thoroughPreset.detailed2.prompt).toBeUndefined();
     }
   });
-
-  test('accepts master with prompt', () => {
-    const config = {
-      master: {
-        model: 'anthropic/claude-opus-4-6',
-        prompt: 'Prioritize correctness over creativity.',
-      },
-      presets: {
-        default: {
-          alpha: { model: 'openai/gpt-5.4-mini' },
-        },
-      },
-    };
-
-    const result = CouncilConfigSchema.safeParse(config);
-    expect(result.success).toBe(true);
-    if (result.success) {
-      expect(result.data.master.prompt).toBe(
-        'Prioritize correctness over creativity.',
-      );
-    }
-  });
-});
-
-describe('PresetMasterOverrideSchema', () => {
-  test('accepts empty override (all fields optional)', () => {
-    const result = PresetMasterOverrideSchema.safeParse({});
-    expect(result.success).toBe(true);
-  });
-
-  test('accepts full override with model, variant, and prompt', () => {
-    const override = {
-      model: 'openai/gpt-5.4',
-      variant: 'high',
-      prompt: 'Be extra thorough.',
-    };
-    const result = PresetMasterOverrideSchema.safeParse(override);
-    expect(result.success).toBe(true);
-    if (result.success) {
-      expect(result.data.model).toBe('openai/gpt-5.4');
-      expect(result.data.variant).toBe('high');
-      expect(result.data.prompt).toBe('Be extra thorough.');
-    }
-  });
-
-  test('accepts partial override with only model', () => {
-    const result = PresetMasterOverrideSchema.safeParse({
-      model: 'anthropic/claude-sonnet-4-6',
-    });
-    expect(result.success).toBe(true);
-  });
-
-  test('accepts partial override with only prompt', () => {
-    const result = PresetMasterOverrideSchema.safeParse({
-      prompt: 'Focus on security.',
-    });
-    expect(result.success).toBe(true);
-  });
-
-  test('rejects invalid model format in override', () => {
-    const result = PresetMasterOverrideSchema.safeParse({
-      model: 'invalid-no-slash',
-    });
-    expect(result.success).toBe(false);
-  });
 });

+ 89 - 110
src/config/council-schema.ts

@@ -35,92 +35,60 @@ export const CouncillorConfigSchema = z.object({
 export type CouncillorConfig = z.infer<typeof CouncillorConfigSchema>;
 
 /**
- * Per-preset master override. All fields are optional — any field
- * provided here overrides the global `council.master` for this preset.
- * Fields not provided fall back to the global master config.
- */
-export const PresetMasterOverrideSchema = z.object({
-  model: ModelIdSchema.optional().describe(
-    'Override the master model for this preset',
-  ),
-  variant: z
-    .string()
-    .optional()
-    .describe('Override the master variant for this preset'),
-  prompt: z
-    .string()
-    .optional()
-    .describe('Override the master synthesis guidance for this preset'),
-});
-
-export type PresetMasterOverride = z.infer<typeof PresetMasterOverrideSchema>;
-
-/**
- * A named preset grouping several councillors with an optional master override.
- *
- * The reserved key `"master"` provides per-preset overrides for the council
- * master (model, variant, prompt). All other keys are treated as councillor
- * names mapping to councillor configs.
+ * A named preset grouping several councillors.
  *
- * After parsing, the preset resolves to:
- * `{ councillors: Record<string, CouncillorConfig>, master?: PresetMasterOverride }`
+ * All keys are treated as councillor names mapping to councillor configs.
+ * The reserved key `"master"` is silently ignored (legacy from when
+ * council-master was a separate agent).
  */
 export const CouncilPresetSchema = z
   .record(z.string(), z.record(z.string(), z.unknown()))
   .transform((entries, ctx) => {
     const councillors: Record<string, CouncillorConfig> = {};
-    let masterOverride: PresetMasterOverride | undefined;
 
     for (const [key, raw] of Object.entries(entries)) {
-      if (key === 'master') {
-        const parsed = PresetMasterOverrideSchema.safeParse(raw);
-        if (!parsed.success) {
-          ctx.addIssue(
-            `Invalid master override in preset: ${parsed.error.issues.map((i) => i.message).join(', ')}`,
-          );
-          return z.NEVER;
-        }
-        masterOverride = parsed.data;
-      } else {
-        const parsed = CouncillorConfigSchema.safeParse(raw);
-        if (!parsed.success) {
-          ctx.addIssue(
-            `Invalid councillor "${key}": ${parsed.error.issues.map((i) => i.message).join(', ')}`,
-          );
-          return z.NEVER;
+      // Silently skip the legacy "master" key — no longer parsed as a
+      // councillor. Old configs with per-preset master overrides won't
+      // error, but the override has no effect.
+      if (key === 'master') continue;
+
+      // Legacy nested format: old configs wrapped councillors in a
+      // "councillors" key inside each preset. Unwrap them into the
+      // parent so the config still works without migration.
+      if (key === 'councillors' && typeof raw === 'object' && raw !== null) {
+        for (const [innerKey, innerRaw] of Object.entries(
+          raw as Record<string, unknown>,
+        )) {
+          const innerParsed =
+            CouncillorConfigSchema.safeParse(innerRaw);
+          if (!innerParsed.success) {
+            ctx.addIssue({
+              code: z.ZodIssueCode.custom,
+              message: `Invalid councillor "${innerKey}" (nested under legacy "councillors" key): ${innerParsed.error.issues.map((i) => i.message).join(', ')}`,
+            });
+            return z.NEVER;
+          }
+          councillors[innerKey] = innerParsed.data;
         }
-        councillors[key] = parsed.data;
+        continue;
+      }
+
+      const parsed = CouncillorConfigSchema.safeParse(raw);
+      if (!parsed.success) {
+        ctx.addIssue({
+          code: z.ZodIssueCode.custom,
+          message: `Invalid councillor "${key}": ${parsed.error.issues.map((i) => i.message).join(', ')}`,
+        });
+        return z.NEVER;
       }
+      councillors[key] = parsed.data;
     }
 
-    return { councillors, master: masterOverride };
+    return councillors;
   });
 
 export type CouncilPreset = z.infer<typeof CouncilPresetSchema>;
 
-/**
- * Council Master configuration.
- * The master receives all councillor responses and produces the final synthesis.
- *
- * Note: The master runs as a council-master agent session with zero
- * permissions (deny all). Synthesis is a text-in/text-out operation —
- * no tools or MCPs are needed.
- */
-export const CouncilMasterConfigSchema = z.object({
-  model: ModelIdSchema.describe(
-    'Model ID for the council master (e.g. "anthropic/claude-opus-4-6")',
-  ),
-  variant: z.string().optional(),
-  prompt: z
-    .string()
-    .optional()
-    .describe(
-      'Optional role/guidance injected into the master synthesis prompt',
-    ),
-});
-
-export type CouncilMasterConfig = z.infer<typeof CouncilMasterConfigSchema>;
-
 /**
  * Execution mode for councillors.
  * - parallel: Run all councillors concurrently (default, fastest for multi-model systems)
@@ -141,7 +109,6 @@ export const CouncillorExecutionModeSchema = z
  * ```jsonc
  * {
  *   "council": {
- *     "master": { "model": "anthropic/claude-opus-4-6" },
  *     "presets": {
  *       "default": {
  *         "alpha": { "model": "openai/gpt-5.4-mini" },
@@ -149,50 +116,63 @@ export const CouncillorExecutionModeSchema = z
  *         "gamma": { "model": "google/gemini-3-pro" }
  *       }
  *     },
- *     "master_timeout": 300000,
- *     "councillors_timeout": 180000,
+ *     "timeout": 180000,
  *     "councillor_execution_mode": "serial"
  *   }
  * }
  * ```
  */
-export const CouncilConfigSchema = z.object({
-  master: CouncilMasterConfigSchema,
-  presets: z.record(z.string(), CouncilPresetSchema),
-  master_timeout: z.number().min(0).default(300000),
-  councillors_timeout: z.number().min(0).default(180000),
-  default_preset: z.string().default('default'),
-  master_fallback: z
-    .array(ModelIdSchema)
-    .optional()
-    .transform((val) => {
-      if (!val) return val;
-      const unique = [...new Set(val)];
-      if (unique.length !== val.length) {
-        // Silently deduplicate — no validation error is raised for
-        // duplicate entries; duplicates are removed transparently.
-        return unique;
-      }
-      return val;
-    })
-    .describe(
-      'Fallback models for the council master. Tried in order if the primary model fails. ' +
-        'Example: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4"]',
+export const CouncilConfigSchema = z
+  .object({
+    presets: z.record(z.string(), CouncilPresetSchema),
+    timeout: z.number().min(0).default(180000),
+    default_preset: z.string().default('default'),
+    councillor_execution_mode: CouncillorExecutionModeSchema.describe(
+      'Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).',
     ),
-  councillor_execution_mode: CouncillorExecutionModeSchema.describe(
-    'Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).',
-  ),
-  councillor_retries: z
-    .number()
-    .int()
-    .min(0)
-    .max(5)
-    .default(3)
-    .describe(
-      'Number of retry attempts for councillors and master that return empty responses ' +
-        '(e.g. due to provider rate limiting). Default: 3 retries.',
-    ),
-});
+    councillor_retries: z
+      .number()
+      .int()
+      .min(0)
+      .max(5)
+      .default(3)
+      .describe(
+        'Number of retry attempts for councillors that return empty responses ' +
+          '(e.g. due to provider rate limiting). Default: 3 retries.',
+      ),
+    // Deprecated fields — accepted for backward compatibility but ignored.
+    // The council agent now synthesizes directly; no separate master session.
+    // Uses permissive schemas since the values are discarded — strict
+    // validation would break old configs with non-standard model IDs.
+    master: z
+      .unknown()
+      .optional()
+      .describe('DEPRECATED — ignored. Council agent synthesizes directly.'),
+    master_timeout: z
+      .unknown()
+      .optional()
+      .describe('DEPRECATED — ignored. Use "timeout" instead.'),
+    master_fallback: z
+      .unknown()
+      .optional()
+      .describe('DEPRECATED — ignored. No separate master session.'),
+  })
+  .transform((data) => {
+    // Detect deprecated fields and attach warning for consumers
+    const deprecated: string[] = [];
+    if (data.master !== undefined) deprecated.push('master');
+    if (data.master_timeout !== undefined) deprecated.push('master_timeout');
+    if (data.master_fallback !== undefined) deprecated.push('master_fallback');
+
+    return {
+      presets: data.presets,
+      timeout: data.timeout,
+      default_preset: data.default_preset,
+      councillor_execution_mode: data.councillor_execution_mode,
+      councillor_retries: data.councillor_retries,
+      _deprecated: deprecated.length > 0 ? deprecated : undefined,
+    };
+  });
 
 export type CouncilConfig = z.infer<typeof CouncilConfigSchema>;
 export type CouncillorExecutionMode = z.infer<
@@ -210,7 +190,6 @@ export type CouncillorExecutionMode = z.infer<
  * ```
  */
 export const DEFAULT_COUNCIL_CONFIG: z.input<typeof CouncilConfigSchema> = {
-  master: { model: 'anthropic/claude-opus-4-6' },
   presets: {
     default: {
       alpha: { model: 'openai/gpt-5.4-mini' },

+ 1 - 1
src/config/schema.ts

@@ -245,7 +245,7 @@ export const PluginConfigSchema = z.object({
     .describe(
       'Agent names to disable completely. ' +
         'Disabled agents are not instantiated and cannot be delegated to. ' +
-        'Orchestrator and council internal agents (councillor, council-master) cannot be disabled. ' +
+        'Orchestrator and council internal agents (councillor) cannot be disabled. ' +
         "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable.",
     ),
   disabled_mcps: z.array(z.string()).optional(),

+ 35 - 652
src/council/council-manager.test.ts

@@ -51,14 +51,11 @@ function createMockContext(overrides?: {
 }
 
 function createTestCouncilConfig(overrides?: {
-  master?: { model?: string; variant?: string };
   presets?: Record<string, Record<string, { model: string; variant?: string }>>;
   default_preset?: string;
-  master_timeout?: number;
-  councillors_timeout?: number;
+  timeout?: number;
 }): PluginConfig {
   const councilConfig = CouncilConfigSchema.parse({
-    master: overrides?.master ?? { model: 'anthropic/claude-opus-4-6' },
     presets: overrides?.presets ?? {
       default: {
         alpha: { model: 'openai/gpt-5.4-mini' },
@@ -66,8 +63,7 @@ function createTestCouncilConfig(overrides?: {
       },
     },
     default_preset: overrides?.default_preset,
-    master_timeout: overrides?.master_timeout,
-    councillors_timeout: overrides?.councillors_timeout,
+    timeout: overrides?.timeout,
   });
 
   return { council: councilConfig } as any;
@@ -192,13 +188,10 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                councillor1: { model: 'openai/gpt-5.4-mini' },
-                councillor2: { model: 'openai/gpt-5.3-codex' },
-              },
+              councillor1: { model: 'openai/gpt-5.4-mini' },
+              councillor2: { model: 'openai/gpt-5.3-codex' },
             },
           },
         },
@@ -239,17 +232,12 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
             },
             custom: {
-              councillors: {
-                beta: { model: 'openai/gpt-5.3-codex' },
-              },
+              beta: { model: 'openai/gpt-5.3-codex' },
             },
           },
           default_preset: 'custom',
@@ -293,13 +281,10 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                timeout: { model: 'openai/gpt-5.4-mini' },
-                success: { model: 'openai/gpt-5.3-codex' },
-              },
+              timeout: { model: 'openai/gpt-5.4-mini' },
+              success: { model: 'openai/gpt-5.3-codex' },
             },
           },
         },
@@ -327,58 +312,6 @@ describe('CouncilManager', () => {
       expect(successResult?.status).toBe('completed');
     });
 
-    test('returns degraded result when master fails but councillors succeed', async () => {
-      let createCallCount = 0;
-      const ctx = createMockContext({
-        sessionCreateResult: () => {
-          createCallCount++;
-          return { data: { id: `session-${createCallCount}` } };
-        },
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Councillor result' }],
-            },
-          ],
-        },
-        promptImpl: async (args: any) => {
-          // Master is third session (after 2 councillors), fail it
-          const sessionId = args.path?.id;
-          if (sessionId === 'session-3') {
-            throw new Error('Master synthesis failed');
-          }
-          return {};
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-                beta: { model: 'openai/gpt-5.3-codex' },
-              },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      const result = await manager.runCouncil(
-        'test prompt',
-        undefined,
-        'parent-session-id',
-      );
-
-      expect(result.success).toBe(false);
-      expect(result.error).toContain('synthesis failed');
-      expect(result.result).toBeDefined();
-      expect(result.result).toContain('Degraded');
-      expect(result.councillorResults).toHaveLength(2);
-    });
-
     test('passes variant to councillor sessions', async () => {
       const ctx = createMockContext({
         sessionMessagesResult: {
@@ -392,12 +325,9 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini', variant: 'low' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini', variant: 'low' },
             },
           },
         },
@@ -417,41 +347,6 @@ describe('CouncilManager', () => {
       expect(councillorCall?.[0].body?.variant).toBe('low');
     });
 
-    test('passes variant to master session', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: { model: 'anthropic/claude-opus-4-6', variant: 'high' },
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-session-id');
-
-      const promptCalls = ctx.client.session.prompt.mock.calls as Array<
-        [{ body?: { variant?: string } }]
-      >;
-      // Last prompt call is for master (after councillor)
-      const masterCall = promptCalls[promptCalls.length - 1];
-      expect(masterCall[0].body?.variant).toBe('high');
-    });
-
     test('always aborts councillor sessions after completion', async () => {
       const ctx = createMockContext({
         sessionMessagesResult: {
@@ -465,13 +360,10 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-                beta: { model: 'openai/gpt-5.3-codex' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
+              beta: { model: 'openai/gpt-5.3-codex' },
             },
           },
         },
@@ -480,20 +372,17 @@ describe('CouncilManager', () => {
 
       await manager.runCouncil('test prompt', undefined, 'parent-session-id');
 
-      // Should abort 2 councillors + 1 master = 3 total
-      expect(ctx.client.session.abort).toHaveBeenCalledTimes(3);
+      // Should abort 2 councillors
+      expect(ctx.client.session.abort).toHaveBeenCalledTimes(2);
     });
 
     test('handles councillor with invalid model format', async () => {
       const ctx = createMockContext();
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                badmodel: { model: 'invalid-model-no-slash' },
-              },
+              badmodel: { model: 'invalid-model-no-slash' },
             },
           },
         },
@@ -515,58 +404,6 @@ describe('CouncilManager', () => {
       );
     });
 
-    test('handles master with invalid model format', async () => {
-      let createCallCount = 0;
-      const ctx = createMockContext({
-        sessionCreateResult: () => {
-          createCallCount++;
-          return { data: { id: `session-${createCallCount}` } };
-        },
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Councillor response' }],
-            },
-          ],
-        },
-        promptImpl: async (args: any) => {
-          // Master is second session (after 1 councillor), fail it due to invalid model
-          const sessionId = args.path?.id;
-          if (sessionId === 'session-2') {
-            throw new Error(
-              'Invalid master model format: invalid-model-no-slash',
-            );
-          }
-          return {};
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: { model: 'invalid-model-no-slash' },
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      const result = await manager.runCouncil(
-        'test prompt',
-        undefined,
-        'parent-session-id',
-      );
-
-      expect(result.success).toBe(false);
-      expect(result.error).toContain('Invalid model format');
-      expect(result.error).toContain('All master models failed');
-      expect(result.result).toBeDefined(); // Degraded result
-    });
-
     test('extracts text and reasoning content from councillor responses', async () => {
       const ctx = createMockContext({
         sessionMessagesResult: {
@@ -583,12 +420,9 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
             },
           },
         },
@@ -602,7 +436,7 @@ describe('CouncilManager', () => {
       );
 
       expect(result.success).toBe(true);
-      // Councillors filter out reasoning parts to avoid bloating master synthesis
+      // Councillors filter out reasoning parts to avoid bloating the synthesis
       expect(result.councillorResults[0].result).not.toContain(
         'I am thinking...',
       );
@@ -661,7 +495,9 @@ describe('CouncilManager', () => {
       );
 
       expect(result.success).toBe(false);
-      expect(result.error).toBe('Preset "empty" has no councillors configured');
+      expect(result.error).toContain(
+        'Preset "empty" has no councillors configured',
+      );
       expect(result.councillorResults).toHaveLength(0);
     });
 
@@ -741,81 +577,6 @@ describe('CouncilManager', () => {
       expect(councillorCall).toBeDefined();
     });
 
-    test('passes agent field in master prompt body', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-      });
-      const config = createTestCouncilConfig();
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-id');
-
-      const promptCalls = ctx.client.session.prompt.mock.calls as Array<
-        [{ body?: { agent?: string } }]
-      >;
-      // Last prompt call is for master
-      const masterCall = promptCalls[promptCalls.length - 1];
-      expect(masterCall[0].body?.agent).toBe('council-master');
-    });
-
-    test('disables delegation tools in councillor prompt body', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-      });
-      const config = createTestCouncilConfig();
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-id');
-
-      const promptCalls = ctx.client.session.prompt.mock.calls as Array<
-        [{ body?: { tools?: Record<string, boolean>; agent?: string } }]
-      >;
-      // Find councillor call by agent field (notification may interleave)
-      const councillorCall = promptCalls.find(
-        (c) => c[0].body?.agent === 'councillor',
-      );
-      // Councillor tools: delegation disabled (leaf node)
-      expect(councillorCall?.[0].body?.tools).toEqual({ task: false });
-    });
-
-    test('disables delegation tools in master prompt body', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-      });
-      const config = createTestCouncilConfig();
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-id');
-
-      const promptCalls = ctx.client.session.prompt.mock.calls as Array<
-        [{ body?: { tools?: Record<string, boolean> } }]
-      >;
-      // Master tools: everything disabled
-      const masterCall = promptCalls[promptCalls.length - 1];
-      expect(masterCall[0].body?.tools).toEqual({ task: false });
-    });
-
     test('creates session with model label in title', async () => {
       const ctx = createMockContext({
         sessionMessagesResult: {
@@ -829,12 +590,9 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
             },
           },
         },
@@ -850,115 +608,6 @@ describe('CouncilManager', () => {
       expect(createCalls[0][0].body?.title).toBe(
         'Council alpha (gpt-5.4-mini)',
       );
-      // Master title: "Council Master (claude-opus-4-6)"
-      const masterCreate = createCalls[createCalls.length - 1];
-      expect(masterCreate[0].body?.title).toBe(
-        'Council Master (claude-opus-4-6)',
-      );
-    });
-
-    test('tries master_fallback models on primary failure', async () => {
-      let promptCallCount = 0;
-      const ctx = createMockContext({
-        sessionCreateResult: () => ({ data: { id: 'session-1' } }),
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-        promptImpl: async (args: any) => {
-          // Only count agent prompt calls (skip start notification)
-          if (args.body?.agent) {
-            promptCallCount++;
-            // Councillor succeeds, master primary fails, fallback succeeds
-            if (promptCallCount === 2) {
-              throw new Error('Primary model timeout');
-            }
-          }
-          return {};
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: { model: 'openai/primary-model' },
-          master_fallback: ['anthropic/fallback-model'],
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      const result = await manager.runCouncil(
-        'test prompt',
-        'default',
-        'parent-id',
-      );
-
-      expect(result.success).toBe(true);
-      // 1 councillor + 1 primary master (fail) + 1 fallback master (succeed) = 3
-      expect(promptCallCount).toBe(3);
-    });
-
-    test('returns error when all master_fallback models fail', async () => {
-      let agentPromptCount = 0;
-      const ctx = createMockContext({
-        sessionCreateResult: () => ({ data: { id: 'session-1' } }),
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Councillor response' }],
-            },
-          ],
-        },
-        promptImpl: async (args: any) => {
-          // Only count agent prompt calls (skip start notification)
-          if (args.body?.agent) {
-            agentPromptCount++;
-            // Councillor succeeds, all master attempts fail
-            if (agentPromptCount > 1) {
-              throw new Error('Model unavailable');
-            }
-          }
-          return {};
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: { model: 'openai/primary-model' },
-          master_fallback: ['anthropic/fallback-one', 'google/fallback-two'],
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      const result = await manager.runCouncil(
-        'test prompt',
-        'default',
-        'parent-id',
-      );
-
-      expect(result.success).toBe(false);
-      expect(result.error).toContain('All master models failed');
-      // Should try primary + 2 fallbacks = 3 master attempts
-      // 1 councillor + 3 master = 4 total agent prompts
-      expect(agentPromptCount).toBe(4);
-      // Degraded result from councillor
-      expect(result.result).toBeDefined();
     });
 
     test('passes councillor prompt to councillor session', async () => {
@@ -974,15 +623,11 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                alpha: {
-                  model: 'openai/gpt-5.4-mini',
-                  prompt:
-                    'You are a meticulous reviewer focused on edge cases.',
-                },
+              alpha: {
+                model: 'openai/gpt-5.4-mini',
+                prompt: 'You are a meticulous reviewer focused on edge cases.',
               },
             },
           },
@@ -1013,53 +658,6 @@ describe('CouncilManager', () => {
       );
     });
 
-    test('passes master prompt to master session', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Synthesized response' }],
-            },
-          ],
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: {
-            model: 'anthropic/claude-opus-4-6',
-            prompt: 'Prioritize correctness over creativity.',
-          },
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-id');
-
-      const promptCalls = ctx.client.session.prompt.mock.calls as Array<
-        [
-          {
-            body?: {
-              parts?: Array<{ type: string; text?: string }>;
-              agent?: string;
-            };
-          },
-        ]
-      >;
-      // Last call is master
-      const masterCall = promptCalls[promptCalls.length - 1];
-      expect(masterCall[0].body?.agent).toBe('council-master');
-      const promptText = masterCall[0]?.body?.parts?.[0]?.text;
-      expect(promptText).toContain('Prioritize correctness over creativity.');
-    });
-
     test('works without any prompt overrides (backward compatible)', async () => {
       const ctx = createMockContext({
         sessionMessagesResult: {
@@ -1073,12 +671,9 @@ describe('CouncilManager', () => {
       });
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
             },
           },
         },
@@ -1110,196 +705,9 @@ describe('CouncilManager', () => {
       expect(councillorCall?.[0]?.body?.parts?.[0]?.text).toBe('test prompt');
     });
 
-    test('per-preset master model override replaces global master model', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-              master: { model: 'google/gemini-3-pro' },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-id');
-
-      const createCalls = ctx.client.session.create.mock.calls as Array<
-        [{ body?: { title?: string } }]
-      >;
-      // Master title should use the override model, not global
-      const masterCreate = createCalls[createCalls.length - 1];
-      expect(masterCreate[0].body?.title).toBe('Council Master (gemini-3-pro)');
-    });
-
-    test('per-preset master prompt override replaces global master prompt', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: {
-            model: 'anthropic/claude-opus-4-6',
-            prompt: 'Global master prompt.',
-          },
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-              master: { prompt: 'Preset-specific master prompt.' },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-id');
-
-      const promptCalls = ctx.client.session.prompt.mock.calls as Array<
-        [
-          {
-            body?: {
-              parts?: Array<{ type: string; text?: string }>;
-              agent?: string;
-            };
-          },
-        ]
-      >;
-      const masterCall = promptCalls[promptCalls.length - 1];
-      const promptText = masterCall[0]?.body?.parts?.[0]?.text;
-      expect(promptText).toContain('Preset-specific master prompt.');
-      expect(promptText).not.toContain('Global master prompt.');
-    });
-
-    test('per-preset master variant override replaces global variant', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: {
-            model: 'anthropic/claude-opus-4-6',
-            variant: 'low',
-          },
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-              master: { variant: 'high' },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-id');
-
-      const promptCalls = ctx.client.session.prompt.mock.calls as Array<
-        [{ body?: { variant?: string } }]
-      >;
-      const masterCall = promptCalls[promptCalls.length - 1];
-      expect(masterCall[0].body?.variant).toBe('high');
-    });
-
-    test('no per-preset master override falls back to global master config', async () => {
-      const ctx = createMockContext({
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
-      });
-      const config: PluginConfig = {
-        council: {
-          master: {
-            model: 'anthropic/claude-opus-4-6',
-            variant: 'high',
-            prompt: 'Global prompt.',
-          },
-          presets: {
-            default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
-            },
-          },
-        },
-      } as any;
-      const manager = new CouncilManager(ctx, config, undefined);
-
-      await manager.runCouncil('test prompt', undefined, 'parent-id');
-
-      const promptCalls = ctx.client.session.prompt.mock.calls as Array<
-        [
-          {
-            body?: {
-              parts?: Array<{ type: string; text?: string }>;
-              variant?: string;
-              agent?: string;
-            };
-          },
-        ]
-      >;
-      const masterCall = promptCalls[promptCalls.length - 1];
-      // Uses global model (in title)
-      const createCalls = ctx.client.session.create.mock.calls as Array<
-        [{ body?: { title?: string } }]
-      >;
-      const masterCreate = createCalls[createCalls.length - 1];
-      expect(masterCreate[0].body?.title).toBe(
-        'Council Master (claude-opus-4-6)',
-      );
-      // Uses global variant
-      expect(masterCall[0].body?.variant).toBe('high');
-      // Uses global prompt
-      const promptText = masterCall[0]?.body?.parts?.[0]?.text;
-      expect(promptText).toContain('Global prompt.');
-    });
-
     test('retries councillor on empty response', async () => {
       const ctx = createMockContext({
         promptImpl: async () => ({}),
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Master synthesis' }],
-            },
-          ],
-        },
       });
 
       // Track messages call count and return empty first, then success
@@ -1308,7 +716,6 @@ describe('CouncilManager', () => {
       ctx.client.session.messages = mock(async (args) => {
         // First call (first councillor attempt): empty response
         // Second call (councillor retry): success
-        // Third call (master): master synthesis
         councillorMessagesCallCount++;
         if (councillorMessagesCallCount === 1) {
           return {
@@ -1330,19 +737,16 @@ describe('CouncilManager', () => {
             ],
           };
         }
-        // Master and any other calls: use original
+        // Any other calls: use original
         return originalMessages(args);
       });
 
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           councillor_retries: 1,
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
             },
           },
         },
@@ -1370,14 +774,6 @@ describe('CouncilManager', () => {
           // Simulate timeout error
           throw new Error('Prompt timed out after 180000ms');
         },
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Response' }],
-            },
-          ],
-        },
       });
 
       // Override messages to track calls (won't be reached due to timeout)
@@ -1395,13 +791,10 @@ describe('CouncilManager', () => {
 
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           councillor_retries: 2,
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
             },
           },
         },
@@ -1425,25 +818,14 @@ describe('CouncilManager', () => {
     test('exhausts councillor retries and returns failure', async () => {
       const ctx = createMockContext({
         promptImpl: async () => ({}),
-        sessionMessagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: '' }],
-            },
-          ],
-        },
       });
 
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           councillor_retries: 1,
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
             },
           },
         },
@@ -1482,13 +864,10 @@ describe('CouncilManager', () => {
 
       const config: PluginConfig = {
         council: {
-          master: { model: 'anthropic/claude-opus-4-6' },
           councillor_retries: 1,
           presets: {
             default: {
-              councillors: {
-                alpha: { model: 'openai/gpt-5.4-mini' },
-              },
+              alpha: { model: 'openai/gpt-5.4-mini' },
             },
           },
         },
@@ -1504,13 +883,17 @@ describe('CouncilManager', () => {
         'parent-id',
       );
 
-      // With retry_on_empty: false, empty response is accepted
+      // With retry_on_empty: false, empty response is accepted as completed
       expect(result.councillorResults).toHaveLength(1);
       expect(result.councillorResults[0].status).toBe('completed');
       expect(result.councillorResults[0].result).toBe('');
       // Council succeeds because empty is accepted as valid response
+      // The formatted result contains the message about all councillors failing
       expect(result.success).toBe(true);
-      expect(result.result).toBe('');
+      expect(result.result).toContain(
+        'All councillors failed to produce output',
+      );
+      expect(result.result).toContain('test prompt');
     });
   });
 });

+ 25 - 175
src/council/council-manager.ts

@@ -2,25 +2,20 @@
  * Council Manager
  *
  * Orchestrates multi-LLM council sessions: launches councillors in
- * parallel, collects results, then runs the council master for synthesis.
+ * parallel and collects their results for the council agent to synthesize.
  */
 
 import type { PluginInput } from '@opencode-ai/plugin';
 import {
   formatCouncillorPrompt,
-  formatMasterSynthesisPrompt,
+  formatCouncillorResults,
 } from '../agents/council';
 import type { PluginConfig } from '../config';
 import {
   COUNCILLOR_STAGGER_MS,
   TMUX_SPAWN_DELAY_MS,
 } from '../config/constants';
-import type {
-  CouncilConfig,
-  CouncillorConfig,
-  CouncilResult,
-  PresetMasterOverride,
-} from '../config/council-schema';
+import type { CouncillorConfig, CouncilResult } from '../config/council-schema';
 import { log } from '../utils/logger';
 import {
   extractSessionResult,
@@ -43,6 +38,7 @@ export class CouncilManager {
   private config?: PluginConfig;
   private depthTracker?: SubagentDepthTracker;
   private tmuxEnabled: boolean;
+  private deprecatedFields?: string[];
 
   constructor(
     ctx: PluginInput,
@@ -53,18 +49,23 @@ export class CouncilManager {
     this.client = ctx.client;
     this.directory = ctx.directory;
     this.config = config;
+    this.deprecatedFields = config?.council?._deprecated;
     this.depthTracker = depthTracker;
     this.tmuxEnabled = tmuxEnabled;
   }
 
+  /** Return deprecated config fields detected during parsing (for tool warnings). */
+  getDeprecatedFields(): string[] | undefined {
+    return this.deprecatedFields;
+  }
+
   /**
    * Run a full council session.
    *
    * 1. Look up the preset
    * 2. Launch all councillors in parallel
    * 3. Collect results (respecting timeout)
-   * 4. Run master synthesis
-   * 5. Return combined result
+   * 4. Return formatted councillor results for synthesis
    */
   async runCouncil(
     prompt: string,
@@ -112,24 +113,23 @@ export class CouncilManager {
       };
     }
 
-    if (Object.keys(preset.councillors).length === 0) {
+    if (Object.keys(preset).length === 0) {
       log(`[council-manager] Preset "${resolvedPreset}" has no councillors`);
       return {
         success: false,
-        error: `Preset "${resolvedPreset}" has no councillors configured`,
+        error: `Preset "${resolvedPreset}" has no councillors configured. Note: the reserved key "master" is ignored — use councillor names as keys`,
         councillorResults: [],
       };
     }
 
-    const councillorsTimeout = councilConfig.councillors_timeout ?? 180000;
-    const masterTimeout = councilConfig.master_timeout ?? 300000;
+    const timeout = councilConfig.timeout ?? 180000;
     const executionMode = councilConfig.councillor_execution_mode ?? 'parallel';
     const maxRetries = councilConfig.councillor_retries ?? 3;
 
-    const councillorCount = Object.keys(preset.councillors).length;
+    const councillorCount = Object.keys(preset).length;
 
     log(`[council-manager] Starting council with preset "${resolvedPreset}"`, {
-      councillors: Object.keys(preset.councillors),
+      councillors: Object.keys(preset),
     });
 
     // Notify parent session that council is starting
@@ -141,12 +141,12 @@ export class CouncilManager {
       },
     );
 
-    // Phase 1: Run councillors (parallel or serial based on config)
+    // Run councillors (parallel or serial based on config)
     const councillorResults = await this.runCouncillors(
       prompt,
-      preset.councillors,
+      preset,
       parentSessionId,
-      councillorsTimeout,
+      timeout,
       executionMode,
       maxRetries,
     );
@@ -167,40 +167,17 @@ export class CouncilManager {
       };
     }
 
-    // Phase 2: Master synthesis
-    const masterResult = await this.runMaster(
+    // Format councillor results for the council agent to synthesize
+    const formattedCouncillorResults = formatCouncillorResults(
       prompt,
       councillorResults,
-      councilConfig,
-      parentSessionId,
-      masterTimeout,
-      preset.master,
     );
 
-    if (!masterResult.success) {
-      log('[council-manager] Master failed', {
-        error: masterResult.error,
-      });
-
-      // Graceful degradation: return best single councillor result
-      const bestResult = councillorResults.find(
-        (r) => r.status === 'completed' && r.result,
-      );
-      return {
-        success: false,
-        error: masterResult.error ?? 'Council master failed',
-        result: bestResult?.result
-          ? `(Degraded — master failed, using ${bestResult.name}'s response)\n\n${bestResult.result}`
-          : undefined,
-        councillorResults,
-      };
-    }
-
     log('[council-manager] Council completed successfully');
 
     return {
       success: true,
-      result: masterResult.result,
+      result: formattedCouncillorResults,
       councillorResults,
     };
   }
@@ -232,12 +209,11 @@ export class CouncilManager {
   }
 
   // -------------------------------------------------------------------------
-  // Shared session lifecycle (councillors + master both use this)
+  // Shared session lifecycle
   // -------------------------------------------------------------------------
 
   /**
    * Run a single agent session: create → register → prompt → extract → cleanup.
-   * Both councillors and the master follow this identical lifecycle.
    */
   private async runAgentSession(options: {
     parentSessionId: string;
@@ -338,7 +314,7 @@ export class CouncilManager {
     parentSessionId: string,
     timeout: number,
     executionMode: 'parallel' | 'serial' = 'parallel',
-    maxRetries: number = 1,
+    maxRetries: number,
   ): Promise<CouncilResult['councillorResults']> {
     const entries = Object.entries(councillors);
     const results: Array<{
@@ -392,13 +368,7 @@ export class CouncilManager {
         const [name, cfg] = entries[index];
 
         if (result.status === 'fulfilled') {
-          results.push({
-            name,
-            model: cfg.model,
-            status: result.value.status,
-            result: result.value.result,
-            error: result.value.error,
-          });
+          results.push(result.value);
         } else {
           results.push({
             name,
@@ -491,124 +461,4 @@ export class CouncilManager {
       error: `Councillor "${name}": max retries exhausted`,
     };
   }
-
-  // -------------------------------------------------------------------------
-  // Phase 2: Master Synthesis
-  // -------------------------------------------------------------------------
-
-  /**
-   * Run a single master model with retry logic for empty responses.
-   * Only retries on "Empty response from provider" — timeouts and
-   * other failures throw immediately so runMaster can try the next
-   * fallback model.
-   */
-  private async runMasterModelWithRetry(
-    parentSessionId: string,
-    model: string,
-    modelLabel: string,
-    promptText: string,
-    variant: string | undefined,
-    timeout: number,
-    maxRetries: number,
-  ): Promise<string> {
-    const totalAttempts = 1 + maxRetries;
-
-    for (let attempt = 1; attempt <= totalAttempts; attempt++) {
-      if (attempt > 1) {
-        log(
-          `[council-manager] Retrying master (${modelLabel}), attempt ${attempt}/${totalAttempts}`,
-        );
-      }
-
-      try {
-        return await this.runAgentSession({
-          parentSessionId,
-          title: `Council Master (${modelLabel})`,
-          agent: 'council-master',
-          model,
-          promptText,
-          variant,
-          timeout,
-        });
-      } catch (error) {
-        const msg = error instanceof Error ? error.message : String(error);
-        const isEmptyResponse = msg.includes('Empty response from provider');
-        const canRetry = attempt < totalAttempts && isEmptyResponse;
-
-        if (!canRetry) {
-          throw error;
-        }
-      }
-    }
-
-    // Unreachable, but satisfies TypeScript
-    throw new Error(`Master model ${modelLabel}: max retries exhausted`);
-  }
-
-  private async runMaster(
-    prompt: string,
-    councillorResults: CouncilResult['councillorResults'],
-    councilConfig: CouncilConfig,
-    parentSessionId: string,
-    timeout: number,
-    presetMasterOverride?: PresetMasterOverride,
-  ): Promise<{ success: boolean; result?: string; error?: string }> {
-    const masterConfig = councilConfig.master;
-    const fallbackModels = councilConfig.master_fallback ?? [];
-
-    // Merge per-preset master override with global config
-    const effectiveModel = presetMasterOverride?.model ?? masterConfig.model;
-    const effectiveVariant =
-      presetMasterOverride?.variant ?? masterConfig.variant;
-    const effectivePrompt = presetMasterOverride?.prompt ?? masterConfig.prompt;
-
-    // Build ordered list of models to try (primary first, then fallbacks)
-    const attemptModels = [effectiveModel, ...fallbackModels];
-
-    // Build synthesis prompt (data only — agent factory provides system prompt)
-    const synthesisPrompt = formatMasterSynthesisPrompt(
-      prompt,
-      councillorResults,
-      effectivePrompt,
-    );
-
-    const maxRetries = councilConfig.councillor_retries ?? 3;
-    const errors: string[] = [];
-
-    for (let i = 0; i < attemptModels.length; i++) {
-      const model = attemptModels[i];
-      const currentLabel = shortModelLabel(model);
-
-      try {
-        if (i > 0) {
-          log(
-            `[council-manager] master fallback ${i}/${attemptModels.length - 1}: ${currentLabel}`,
-          );
-        }
-
-        const result = await this.runMasterModelWithRetry(
-          parentSessionId,
-          model,
-          currentLabel,
-          synthesisPrompt,
-          effectiveVariant,
-          timeout,
-          maxRetries,
-        );
-
-        return { success: true, result };
-      } catch (error) {
-        const msg = error instanceof Error ? error.message : String(error);
-        errors.push(`${currentLabel}: ${msg}`);
-
-        log(`[council-manager] master model failed: ${currentLabel} — ${msg}`);
-      }
-    }
-
-    // All models failed
-    return {
-      success: false,
-      error: `All master models failed. ${errors.join(' | ')}`,
-    };
-  }
 }

+ 31 - 23
src/tools/council.test.ts

@@ -53,6 +53,7 @@ function createMockCouncilManager(
         councillorResults,
       };
     }),
+    getDeprecatedFields: mock(() => undefined),
   } as unknown as CouncilManager;
 
   return mockManager;
@@ -233,29 +234,6 @@ describe('council_session tool', () => {
       expect(result).toContain('All councillors failed');
     });
 
-    test('handles council master failure with degraded result', async () => {
-      const ctx = createMockPluginContext();
-      const councilManager = createMockCouncilManager({
-        success: false,
-        error: 'Master synthesis failed',
-        result:
-          "(Degraded — master failed, using alpha's response)\n\nBest answer",
-        councillorResults: [
-          { name: 'alpha', status: 'completed', result: 'Best answer' },
-        ],
-      });
-      const tools = createCouncilTool(ctx, councilManager);
-
-      const result = await tools.council_session.execute({ prompt: 'Test' }, {
-        sessionID: 'test',
-      } as any);
-
-      expect(result).toContain('Degraded');
-      expect(result).toContain('Best answer');
-      expect(result).toContain('1/1 councillors responded');
-      expect(result).toContain('degraded');
-    });
-
     test('handles case when result is undefined', async () => {
       const ctx = createMockPluginContext();
       const councilManager = createMockCouncilManager({
@@ -348,6 +326,7 @@ describe('council_session tool', () => {
         runCouncil: mock(async () => {
           throw new Error('Council manager crashed');
         }),
+        getDeprecatedFields: mock(() => undefined),
       } as unknown as CouncilManager;
       const tools = createCouncilTool(ctx, councilManager);
 
@@ -515,5 +494,34 @@ describe('council_session tool', () => {
 
       expect(result).toContain('Council: 10/10 councillors responded');
     });
+
+    test('includes deprecation warning when deprecated config fields detected', async () => {
+      const ctx = createMockPluginContext();
+      const councilManager = {
+        runCouncil: mock(async () => ({
+          success: true,
+          result: 'Synthesized response',
+          councillorResults: [
+            {
+              name: 'alpha',
+              model: 'test/model',
+              status: 'completed',
+              result: 'Response',
+            },
+          ],
+        })),
+        getDeprecatedFields: mock(() => ['master', 'master_timeout']),
+      } as unknown as CouncilManager;
+      const tools = createCouncilTool(ctx, councilManager);
+
+      const result = await tools.council_session.execute({ prompt: 'Test' }, {
+        sessionID: 'test',
+      } as any);
+
+      expect(result).toContain('Config warning');
+      expect(result).toContain('`council.master`');
+      expect(result).toContain('`council.master_timeout`');
+      expect(result).toContain('deprecated and ignored');
+    });
   });
 });

+ 11 - 14
src/tools/council.ts

@@ -17,7 +17,7 @@ function formatModelComposition(
 ): string {
   return councillorResults
     .map((cr) => {
-      const shortModel = shortModelLabel(cr.model ?? '');
+      const shortModel = shortModelLabel(cr.model);
       return `${cr.name}: ${shortModel}`;
     })
     .join(', ');
@@ -27,7 +27,8 @@ function formatModelComposition(
  * Creates the council_session tool for multi-LLM orchestration.
  *
  * This tool triggers a full council session: parallel councillors →
- * master synthesis. Available to the council agent.
+ * formatted results returned to the council agent for synthesis.
+ * Available to the council agent.
  */
 export function createCouncilTool(
   _ctx: PluginInput,
@@ -36,9 +37,9 @@ export function createCouncilTool(
   const council_session = tool({
     description: `Launch a multi-LLM council session for consensus-based analysis.
 
-Sends the prompt to multiple models (councillors) in parallel, then a council master synthesizes the best response.
+Sends the prompt to multiple models (councillors) in parallel and returns their formatted responses for you to synthesize.
 
-Returns the synthesized result with councillor summary.`,
+Returns the councillor responses with a summary footer.`,
     args: {
       prompt: z.string().describe('The prompt to send to all councillors'),
       preset: z
@@ -78,16 +79,6 @@ Returns the synthesized result with councillor summary.`,
       );
 
       if (!result.success) {
-        if (result.result) {
-          // Graceful degradation — master failed, return best councillor
-          const completed = result.councillorResults.filter(
-            (cr) => cr.status === 'completed',
-          ).length;
-          const total = result.councillorResults.length;
-          const composition = formatModelComposition(result.councillorResults);
-
-          return `${result.result}\n\n---\n*Council: ${completed}/${total} councillors responded (${composition}) — degraded*`;
-        }
         return `Council session failed: ${result.error}`;
       }
 
@@ -102,6 +93,12 @@ Returns the synthesized result with councillor summary.`,
 
       output += `\n\n---\n*Council: ${completed}/${total} councillors responded (${composition})*`;
 
+      // Warn about deprecated config fields if detected
+      const deprecated = councilManager.getDeprecatedFields();
+      if (deprecated && deprecated.length > 0) {
+        output += `\n⚠ Config warning: ${deprecated.map((f) => `\`council.${f}\``).join(', ')} ${deprecated.length === 1 ? 'is' : 'are'} deprecated and ignored. The council agent synthesizes directly — remove ${deprecated.length === 1 ? 'it' : 'them'} from your config.`;
+      }
+
       return output;
     },
   });