Prechádzať zdrojové kódy

refactor(council): remove council-master agent, let council synthesize directly (#356)

Remove the council-master agent and have the council agent synthesize
councillor results directly, eliminating a separate agent session.

- Delete council-master.ts and council-master.test.ts
- Merge synthesis instructions into council agent system prompt
- Remove runMaster()/runMasterModelWithRetry() from council-manager
- Accept deprecated master/master_timeout/master_fallback for backward compat
- Use permissive z.unknown() for deprecated fields (data is discarded)
- Unwrap legacy nested "councillors" key in presets for config migration
- Rename councillors_timeout to timeout
- Remove CouncilMasterConfigSchema and PresetMasterOverrideSchema
ReqX 3 mesiacov pred
rodič
commit
e3cf0589ec

+ 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;
     },
   });