@doingdev/opencode-claude-manager-plugin 0.1.54 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,8 +18,8 @@ export declare class TeamOrchestrator {
18
18
  private readonly teamStore;
19
19
  private readonly transcriptStore;
20
20
  private readonly engineerSessionPrompt;
21
- private readonly architectSystemPrompt;
22
- constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, architectSystemPrompt: string);
21
+ private readonly planSynthesisPrompt;
22
+ constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, planSynthesisPrompt: string);
23
23
  getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
24
24
  listTeams(cwd: string): Promise<TeamRecord[]>;
25
25
  recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
@@ -6,13 +6,13 @@ export class TeamOrchestrator {
6
6
  teamStore;
7
7
  transcriptStore;
8
8
  engineerSessionPrompt;
9
- architectSystemPrompt;
10
- constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, architectSystemPrompt) {
9
+ planSynthesisPrompt;
10
+ constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, planSynthesisPrompt) {
11
11
  this.sessions = sessions;
12
12
  this.teamStore = teamStore;
13
13
  this.transcriptStore = transcriptStore;
14
14
  this.engineerSessionPrompt = engineerSessionPrompt;
15
- this.architectSystemPrompt = architectSystemPrompt;
15
+ this.planSynthesisPrompt = planSynthesisPrompt;
16
16
  }
17
17
  async getOrCreateTeam(cwd, teamId) {
18
18
  const existing = await this.teamStore.getTeam(cwd, teamId);
@@ -106,10 +106,9 @@ export class TeamOrchestrator {
106
106
  }
107
107
  const result = await this.sessions.runTask({
108
108
  cwd: input.cwd,
109
- prompt: this.buildEngineerPrompt(input.mode, input.message),
110
- systemPrompt: engineerState.claudeSessionId
111
- ? undefined
112
- : this.buildSessionSystemPrompt(input.engineer, input.mode),
109
+ prompt: engineerState.claudeSessionId
110
+ ? this.buildEngineerPrompt(input.mode, input.message)
111
+ : `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message)}`,
113
112
  resumeSessionId: engineerState.claudeSessionId ?? undefined,
114
113
  persistSession: true,
115
114
  includePartialMessages: true,
@@ -224,8 +223,7 @@ export class TeamOrchestrator {
224
223
  ];
225
224
  const synthesisResult = await this.sessions.runTask({
226
225
  cwd: input.cwd,
227
- prompt: buildSynthesisPrompt(input.request, drafts),
228
- systemPrompt: buildSynthesisSystemPrompt(this.architectSystemPrompt),
226
+ prompt: `${this.planSynthesisPrompt}\n\n${buildSynthesisPrompt(input.request, drafts)}`,
229
227
  persistSession: false,
230
228
  includePartialMessages: false,
231
229
  permissionMode: 'acceptEdits',
@@ -365,9 +363,6 @@ function buildPlanDraftRequest(perspective, request) {
365
363
  `User request: ${request}`,
366
364
  ].join('\n');
367
365
  }
368
- function buildSynthesisSystemPrompt(architectSystemPrompt) {
369
- return architectSystemPrompt;
370
- }
371
366
  function buildSynthesisPrompt(request, drafts) {
372
367
  return [
373
368
  `User request: ${request}`,
@@ -1,6 +1,6 @@
1
1
  import type { EngineerName, ManagerPromptRegistry } from '../types/contracts.js';
2
2
  export declare const AGENT_CTO = "cto";
3
- export declare const AGENT_ARCHITECT = "architect";
3
+ export declare const AGENT_TEAM_PLANNER = "team-planner";
4
4
  export declare const ENGINEER_AGENT_IDS: {
5
5
  readonly Tom: "tom";
6
6
  readonly John: "john";
@@ -42,7 +42,7 @@ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry,
42
42
  permission: AgentPermission;
43
43
  prompt: string;
44
44
  };
45
- export declare function buildArchitectAgentConfig(prompts: ManagerPromptRegistry): {
45
+ export declare function buildTeamPlannerAgentConfig(prompts: ManagerPromptRegistry): {
46
46
  description: string;
47
47
  mode: "subagent";
48
48
  hidden: boolean;
@@ -1,6 +1,6 @@
1
1
  import { TEAM_ENGINEERS } from '../team/roster.js';
2
2
  export const AGENT_CTO = 'cto';
3
- export const AGENT_ARCHITECT = 'architect';
3
+ export const AGENT_TEAM_PLANNER = 'team-planner';
4
4
  export const ENGINEER_AGENT_IDS = {
5
5
  Tom: 'tom',
6
6
  John: 'john',
@@ -38,24 +38,15 @@ const CTO_READONLY_TOOLS = {
38
38
  todoread: 'allow',
39
39
  question: 'allow',
40
40
  };
41
- function buildArchitectPermissions() {
41
+ function buildTeamPlannerPermissions() {
42
42
  const denied = {};
43
43
  for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
44
44
  denied[toolId] = 'deny';
45
45
  }
46
46
  return {
47
47
  '*': 'deny',
48
- read: 'allow',
49
- grep: 'allow',
50
- glob: 'allow',
51
- list: 'allow',
52
- codesearch: 'allow',
53
- webfetch: 'deny',
54
- websearch: 'deny',
55
- lsp: 'deny',
56
- todowrite: 'deny',
57
- todoread: 'deny',
58
- question: 'deny',
48
+ plan_with_team: 'allow',
49
+ question: 'allow',
59
50
  ...denied,
60
51
  };
61
52
  }
@@ -70,9 +61,13 @@ function buildCtoPermissions() {
70
61
  }
71
62
  const taskPermissions = { '*': 'deny' };
72
63
  for (const engineer of ENGINEER_AGENT_NAMES) {
73
- taskPermissions[ENGINEER_AGENT_IDS[engineer]] = 'allow';
64
+ const agentId = ENGINEER_AGENT_IDS[engineer];
65
+ // Support both uppercase (user-friendly) and lowercase (canonical) agent IDs.
66
+ // This ensures both task({ subagent_type: 'Tom' }) and task({ subagent_type: 'tom' }) work.
67
+ taskPermissions[engineer] = 'allow'; // 'Tom', 'John', etc.
68
+ taskPermissions[agentId] = 'allow'; // 'tom', 'john', etc.
74
69
  }
75
- taskPermissions[AGENT_ARCHITECT] = 'allow';
70
+ taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
76
71
  return {
77
72
  '*': 'deny',
78
73
  ...CTO_READONLY_TOOLS,
@@ -111,14 +106,14 @@ export function buildEngineerAgentConfig(prompts, engineer) {
111
106
  prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
112
107
  };
113
108
  }
114
- export function buildArchitectAgentConfig(prompts) {
109
+ export function buildTeamPlannerAgentConfig(prompts) {
115
110
  return {
116
- description: 'Synthesizes two engineer plan drafts into one stronger, actionable plan.',
111
+ description: 'Runs dual-engineer planning by calling plan_with_team. Asks for engineer names if not provided.',
117
112
  mode: 'subagent',
118
113
  hidden: false,
119
114
  color: '#D97757',
120
- permission: buildArchitectPermissions(),
121
- prompt: prompts.architectSystemPrompt,
115
+ permission: buildTeamPlannerPermissions(),
116
+ prompt: prompts.teamPlannerPrompt,
122
117
  };
123
118
  }
124
119
  export function denyRestrictedToolsGlobally(permissions) {
@@ -1,2 +1,10 @@
1
1
  import { type Plugin } from '@opencode-ai/plugin';
2
+ import type { EngineerName } from '../types/contracts.js';
2
3
  export declare const ClaudeManagerPlugin: Plugin;
4
+ /**
5
+ * Normalize an agent ID to its lowercase canonical form.
6
+ * Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
7
+ */
8
+ export declare function normalizeAgentId(agentId: string): string;
9
+ export declare function engineerFromAgent(agentId: string): EngineerName;
10
+ export declare function isEngineerAgent(agentId: string): boolean;
@@ -2,7 +2,7 @@ import { tool } from '@opencode-ai/plugin';
2
2
  import { managerPromptRegistry } from '../prompts/registry.js';
3
3
  import { isEngineerName } from '../team/roster.js';
4
4
  import { TeamOrchestrator } from '../manager/team-orchestrator.js';
5
- import { AGENT_CTO, AGENT_ARCHITECT, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildArchitectAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
5
+ import { AGENT_CTO, AGENT_TEAM_PLANNER, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
6
6
  import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
7
7
  const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
8
8
  const MODE_ENUM = ['explore', 'implement', 'verify'];
@@ -15,7 +15,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
15
15
  config.permission ??= {};
16
16
  denyRestrictedToolsGlobally(config.permission);
17
17
  config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
18
- config.agent[AGENT_ARCHITECT] ??= buildArchitectAgentConfig(managerPromptRegistry);
18
+ config.agent[AGENT_TEAM_PLANNER] ??= buildTeamPlannerAgentConfig(managerPromptRegistry);
19
19
  for (const engineer of ENGINEER_AGENT_NAMES) {
20
20
  config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
21
21
  }
@@ -129,10 +129,10 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
129
129
  abortSignal: context.abort,
130
130
  onLeadEvent: (event) => reportClaudeEvent(context, args.leadEngineer, event),
131
131
  onChallengerEvent: (event) => reportClaudeEvent(context, args.challengerEngineer, event),
132
- onSynthesisEvent: (event) => reportArchitectEvent(context, event),
132
+ onSynthesisEvent: (event) => reportPlanSynthesisEvent(context, event),
133
133
  });
134
134
  context.metadata({
135
- title: '✅ Architect finished',
135
+ title: '✅ Plan synthesis finished',
136
136
  metadata: {
137
137
  teamId: result.teamId,
138
138
  lead: result.leadEngineer,
@@ -411,16 +411,25 @@ async function runEngineerAssignment(input, context) {
411
411
  });
412
412
  return result;
413
413
  }
414
- function engineerFromAgent(agentId) {
415
- const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === agentId);
414
+ /**
415
+ * Normalize an agent ID to its lowercase canonical form.
416
+ * Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
417
+ */
418
+ export function normalizeAgentId(agentId) {
419
+ return agentId.toLowerCase();
420
+ }
421
+ export function engineerFromAgent(agentId) {
422
+ const normalized = normalizeAgentId(agentId);
423
+ const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === normalized);
416
424
  const engineer = engineerEntry?.[0];
417
425
  if (!engineer || !isEngineerName(engineer)) {
418
426
  throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
419
427
  }
420
428
  return engineer;
421
429
  }
422
- function isEngineerAgent(agentId) {
423
- return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
430
+ export function isEngineerAgent(agentId) {
431
+ const normalized = normalizeAgentId(agentId);
432
+ return Object.values(ENGINEER_AGENT_IDS).some((id) => id === normalized);
424
433
  }
425
434
  /**
426
435
  * Resolves the team ID for an engineer session.
@@ -562,10 +571,10 @@ function reportClaudeEvent(context, engineer, event) {
562
571
  });
563
572
  }
564
573
  }
565
- function reportArchitectEvent(context, event) {
574
+ function reportPlanSynthesisEvent(context, event) {
566
575
  if (event.type === 'error') {
567
576
  context.metadata({
568
- title: `❌ Architect hit an error`,
577
+ title: `❌ Plan synthesis hit an error`,
569
578
  metadata: {
570
579
  sessionId: event.sessionId,
571
580
  error: event.text.slice(0, 200),
@@ -575,7 +584,7 @@ function reportArchitectEvent(context, event) {
575
584
  }
576
585
  if (event.type === 'init') {
577
586
  context.metadata({
578
- title: `⚡ Architect session ready`,
587
+ title: `⚡ Plan synthesis ready`,
579
588
  metadata: {
580
589
  sessionId: event.sessionId,
581
590
  },
@@ -608,10 +617,10 @@ function reportArchitectEvent(context, event) {
608
617
  const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
609
618
  context.metadata({
610
619
  title: toolDescription
611
- ? `⚡ Architect → ${toolDescription}`
620
+ ? `⚡ Plan synthesis → ${toolDescription}`
612
621
  : toolName
613
- ? `⚡ Architect → ${toolName}`
614
- : `⚡ Architect is using Claude Code tools`,
622
+ ? `⚡ Plan synthesis → ${toolName}`
623
+ : `⚡ Plan synthesis is running`,
615
624
  metadata: {
616
625
  sessionId: event.sessionId,
617
626
  ...(toolName !== undefined && { toolName }),
@@ -625,7 +634,7 @@ function reportArchitectEvent(context, event) {
625
634
  const isThinking = event.text.startsWith('<thinking>');
626
635
  const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
627
636
  context.metadata({
628
- title: `⚡ Architect ${stateLabel}`,
637
+ title: `⚡ Plan synthesis ${stateLabel}`,
629
638
  metadata: {
630
639
  sessionId: event.sessionId,
631
640
  preview: event.text.slice(0, 160),
@@ -635,8 +644,9 @@ function reportArchitectEvent(context, event) {
635
644
  }
636
645
  }
637
646
  function annotateToolRun(context, title, metadata) {
647
+ const agentLabel = context.agent === AGENT_CTO ? 'CTO' : undefined;
638
648
  context.metadata({
639
- title,
649
+ title: agentLabel ? `${agentLabel} → ${title}` : title,
640
650
  metadata,
641
651
  });
642
652
  }
@@ -24,7 +24,7 @@ export function getOrCreatePluginServices(worktree) {
24
24
  const teamStore = new TeamStateStore();
25
25
  const transcriptStore = new TranscriptStore();
26
26
  const manager = new PersistentManager(gitOps, transcriptStore);
27
- const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.architectSystemPrompt);
27
+ const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.planSynthesisPrompt);
28
28
  const services = {
29
29
  manager,
30
30
  sessions: sessionService,
@@ -27,7 +27,7 @@ export const managerPromptRegistry = {
27
27
  '',
28
28
  'Plan and decompose:',
29
29
  '- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
30
- '- For medium or large tasks, delegate dual-engineer exploration to two engineers, then task the `architect` subagent to synthesize their independent plans into one stronger plan.',
30
+ "- For medium or large tasks, delegate dual-engineer exploration and synthesis to the `team-planner` subagent: use `task(subagent_type: 'team-planner', ...)`. When tasking engineer subagents directly, use lowercase subagent IDs: tom, john, maya, sara, alex.",
31
31
  '- Define clear success criteria before delegating. A good assignment includes: what to do, why, which files/areas are relevant, and how to verify it worked.',
32
32
  '',
33
33
  'Delegate through the Task tool:',
@@ -76,8 +76,8 @@ export const managerPromptRegistry = {
76
76
  'Report blockers immediately with exact error output. Do not retry silently more than once.',
77
77
  'Do not run git commit, git push, git reset, git checkout, or git stash.',
78
78
  ].join('\n'),
79
- architectSystemPrompt: [
80
- 'You are the Architect. Your role is to synthesize two independent engineering plans into one stronger, unified plan.',
79
+ planSynthesisPrompt: [
80
+ 'You are synthesizing two independent engineering plans into one stronger, unified plan.',
81
81
  'Compare the lead and challenger plans on clarity, feasibility, risk, and fit to the user request.',
82
82
  'Prefer the simplest path that fully addresses the goal. Surface tradeoffs honestly.',
83
83
  'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
@@ -91,6 +91,14 @@ export const managerPromptRegistry = {
91
91
  '## Recommended Answer',
92
92
  '<answer or NONE>',
93
93
  ].join('\n'),
94
+ teamPlannerPrompt: [
95
+ 'You are the team planner. Your only job is to invoke `plan_with_team`.',
96
+ '`plan_with_team` dispatches two engineers in parallel (lead + challenger) then synthesizes their plans.',
97
+ '',
98
+ 'If the task includes a lead engineer and a challenger engineer, call `plan_with_team` immediately.',
99
+ 'If either engineer name is missing, use `question` to ask: which engineers should lead and challenge (Tom, John, Maya, Sara, or Alex)?',
100
+ 'Do not attempt any planning or analysis yourself. Delegate entirely to `plan_with_team`.',
101
+ ].join('\n'),
94
102
  contextWarnings: {
95
103
  moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
96
104
  high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
@@ -18,8 +18,8 @@ export declare class TeamOrchestrator {
18
18
  private readonly teamStore;
19
19
  private readonly transcriptStore;
20
20
  private readonly engineerSessionPrompt;
21
- private readonly architectSystemPrompt;
22
- constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, architectSystemPrompt: string);
21
+ private readonly planSynthesisPrompt;
22
+ constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, planSynthesisPrompt: string);
23
23
  getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
24
24
  listTeams(cwd: string): Promise<TeamRecord[]>;
25
25
  recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
@@ -6,13 +6,13 @@ export class TeamOrchestrator {
6
6
  teamStore;
7
7
  transcriptStore;
8
8
  engineerSessionPrompt;
9
- architectSystemPrompt;
10
- constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, architectSystemPrompt) {
9
+ planSynthesisPrompt;
10
+ constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, planSynthesisPrompt) {
11
11
  this.sessions = sessions;
12
12
  this.teamStore = teamStore;
13
13
  this.transcriptStore = transcriptStore;
14
14
  this.engineerSessionPrompt = engineerSessionPrompt;
15
- this.architectSystemPrompt = architectSystemPrompt;
15
+ this.planSynthesisPrompt = planSynthesisPrompt;
16
16
  }
17
17
  async getOrCreateTeam(cwd, teamId) {
18
18
  const existing = await this.teamStore.getTeam(cwd, teamId);
@@ -106,10 +106,9 @@ export class TeamOrchestrator {
106
106
  }
107
107
  const result = await this.sessions.runTask({
108
108
  cwd: input.cwd,
109
- prompt: this.buildEngineerPrompt(input.mode, input.message),
110
- systemPrompt: engineerState.claudeSessionId
111
- ? undefined
112
- : this.buildSessionSystemPrompt(input.engineer, input.mode),
109
+ prompt: engineerState.claudeSessionId
110
+ ? this.buildEngineerPrompt(input.mode, input.message)
111
+ : `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message)}`,
113
112
  resumeSessionId: engineerState.claudeSessionId ?? undefined,
114
113
  persistSession: true,
115
114
  includePartialMessages: true,
@@ -224,8 +223,7 @@ export class TeamOrchestrator {
224
223
  ];
225
224
  const synthesisResult = await this.sessions.runTask({
226
225
  cwd: input.cwd,
227
- prompt: buildSynthesisPrompt(input.request, drafts),
228
- systemPrompt: buildSynthesisSystemPrompt(this.architectSystemPrompt),
226
+ prompt: `${this.planSynthesisPrompt}\n\n${buildSynthesisPrompt(input.request, drafts)}`,
229
227
  persistSession: false,
230
228
  includePartialMessages: false,
231
229
  permissionMode: 'acceptEdits',
@@ -365,9 +363,6 @@ function buildPlanDraftRequest(perspective, request) {
365
363
  `User request: ${request}`,
366
364
  ].join('\n');
367
365
  }
368
- function buildSynthesisSystemPrompt(architectSystemPrompt) {
369
- return architectSystemPrompt;
370
- }
371
366
  function buildSynthesisPrompt(request, drafts) {
372
367
  return [
373
368
  `User request: ${request}`,
@@ -1,6 +1,6 @@
1
1
  import type { EngineerName, ManagerPromptRegistry } from '../types/contracts.js';
2
2
  export declare const AGENT_CTO = "cto";
3
- export declare const AGENT_ARCHITECT = "architect";
3
+ export declare const AGENT_TEAM_PLANNER = "team-planner";
4
4
  export declare const ENGINEER_AGENT_IDS: {
5
5
  readonly Tom: "tom";
6
6
  readonly John: "john";
@@ -42,7 +42,7 @@ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry,
42
42
  permission: AgentPermission;
43
43
  prompt: string;
44
44
  };
45
- export declare function buildArchitectAgentConfig(prompts: ManagerPromptRegistry): {
45
+ export declare function buildTeamPlannerAgentConfig(prompts: ManagerPromptRegistry): {
46
46
  description: string;
47
47
  mode: "subagent";
48
48
  hidden: boolean;
@@ -1,6 +1,6 @@
1
1
  import { TEAM_ENGINEERS } from '../team/roster.js';
2
2
  export const AGENT_CTO = 'cto';
3
- export const AGENT_ARCHITECT = 'architect';
3
+ export const AGENT_TEAM_PLANNER = 'team-planner';
4
4
  export const ENGINEER_AGENT_IDS = {
5
5
  Tom: 'tom',
6
6
  John: 'john',
@@ -38,24 +38,15 @@ const CTO_READONLY_TOOLS = {
38
38
  todoread: 'allow',
39
39
  question: 'allow',
40
40
  };
41
- function buildArchitectPermissions() {
41
+ function buildTeamPlannerPermissions() {
42
42
  const denied = {};
43
43
  for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
44
44
  denied[toolId] = 'deny';
45
45
  }
46
46
  return {
47
47
  '*': 'deny',
48
- read: 'allow',
49
- grep: 'allow',
50
- glob: 'allow',
51
- list: 'allow',
52
- codesearch: 'allow',
53
- webfetch: 'deny',
54
- websearch: 'deny',
55
- lsp: 'deny',
56
- todowrite: 'deny',
57
- todoread: 'deny',
58
- question: 'deny',
48
+ plan_with_team: 'allow',
49
+ question: 'allow',
59
50
  ...denied,
60
51
  };
61
52
  }
@@ -70,9 +61,13 @@ function buildCtoPermissions() {
70
61
  }
71
62
  const taskPermissions = { '*': 'deny' };
72
63
  for (const engineer of ENGINEER_AGENT_NAMES) {
73
- taskPermissions[ENGINEER_AGENT_IDS[engineer]] = 'allow';
64
+ const agentId = ENGINEER_AGENT_IDS[engineer];
65
+ // Support both uppercase (user-friendly) and lowercase (canonical) agent IDs.
66
+ // This ensures both task({ subagent_type: 'Tom' }) and task({ subagent_type: 'tom' }) work.
67
+ taskPermissions[engineer] = 'allow'; // 'Tom', 'John', etc.
68
+ taskPermissions[agentId] = 'allow'; // 'tom', 'john', etc.
74
69
  }
75
- taskPermissions[AGENT_ARCHITECT] = 'allow';
70
+ taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
76
71
  return {
77
72
  '*': 'deny',
78
73
  ...CTO_READONLY_TOOLS,
@@ -111,14 +106,14 @@ export function buildEngineerAgentConfig(prompts, engineer) {
111
106
  prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
112
107
  };
113
108
  }
114
- export function buildArchitectAgentConfig(prompts) {
109
+ export function buildTeamPlannerAgentConfig(prompts) {
115
110
  return {
116
- description: 'Synthesizes two engineer plan drafts into one stronger, actionable plan.',
111
+ description: 'Runs dual-engineer planning by calling plan_with_team. Asks for engineer names if not provided.',
117
112
  mode: 'subagent',
118
113
  hidden: false,
119
114
  color: '#D97757',
120
- permission: buildArchitectPermissions(),
121
- prompt: prompts.architectSystemPrompt,
115
+ permission: buildTeamPlannerPermissions(),
116
+ prompt: prompts.teamPlannerPrompt,
122
117
  };
123
118
  }
124
119
  export function denyRestrictedToolsGlobally(permissions) {
@@ -1,2 +1,10 @@
1
1
  import { type Plugin } from '@opencode-ai/plugin';
2
+ import type { EngineerName } from '../types/contracts.js';
2
3
  export declare const ClaudeManagerPlugin: Plugin;
4
+ /**
5
+ * Normalize an agent ID to its lowercase canonical form.
6
+ * Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
7
+ */
8
+ export declare function normalizeAgentId(agentId: string): string;
9
+ export declare function engineerFromAgent(agentId: string): EngineerName;
10
+ export declare function isEngineerAgent(agentId: string): boolean;
@@ -2,7 +2,7 @@ import { tool } from '@opencode-ai/plugin';
2
2
  import { managerPromptRegistry } from '../prompts/registry.js';
3
3
  import { isEngineerName } from '../team/roster.js';
4
4
  import { TeamOrchestrator } from '../manager/team-orchestrator.js';
5
- import { AGENT_CTO, AGENT_ARCHITECT, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildArchitectAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
5
+ import { AGENT_CTO, AGENT_TEAM_PLANNER, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
6
6
  import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
7
7
  const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
8
8
  const MODE_ENUM = ['explore', 'implement', 'verify'];
@@ -15,7 +15,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
15
15
  config.permission ??= {};
16
16
  denyRestrictedToolsGlobally(config.permission);
17
17
  config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
18
- config.agent[AGENT_ARCHITECT] ??= buildArchitectAgentConfig(managerPromptRegistry);
18
+ config.agent[AGENT_TEAM_PLANNER] ??= buildTeamPlannerAgentConfig(managerPromptRegistry);
19
19
  for (const engineer of ENGINEER_AGENT_NAMES) {
20
20
  config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
21
21
  }
@@ -129,10 +129,10 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
129
129
  abortSignal: context.abort,
130
130
  onLeadEvent: (event) => reportClaudeEvent(context, args.leadEngineer, event),
131
131
  onChallengerEvent: (event) => reportClaudeEvent(context, args.challengerEngineer, event),
132
- onSynthesisEvent: (event) => reportArchitectEvent(context, event),
132
+ onSynthesisEvent: (event) => reportPlanSynthesisEvent(context, event),
133
133
  });
134
134
  context.metadata({
135
- title: '✅ Architect finished',
135
+ title: '✅ Plan synthesis finished',
136
136
  metadata: {
137
137
  teamId: result.teamId,
138
138
  lead: result.leadEngineer,
@@ -411,16 +411,25 @@ async function runEngineerAssignment(input, context) {
411
411
  });
412
412
  return result;
413
413
  }
414
- function engineerFromAgent(agentId) {
415
- const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === agentId);
414
+ /**
415
+ * Normalize an agent ID to its lowercase canonical form.
416
+ * Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
417
+ */
418
+ export function normalizeAgentId(agentId) {
419
+ return agentId.toLowerCase();
420
+ }
421
+ export function engineerFromAgent(agentId) {
422
+ const normalized = normalizeAgentId(agentId);
423
+ const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === normalized);
416
424
  const engineer = engineerEntry?.[0];
417
425
  if (!engineer || !isEngineerName(engineer)) {
418
426
  throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
419
427
  }
420
428
  return engineer;
421
429
  }
422
- function isEngineerAgent(agentId) {
423
- return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
430
+ export function isEngineerAgent(agentId) {
431
+ const normalized = normalizeAgentId(agentId);
432
+ return Object.values(ENGINEER_AGENT_IDS).some((id) => id === normalized);
424
433
  }
425
434
  /**
426
435
  * Resolves the team ID for an engineer session.
@@ -562,10 +571,10 @@ function reportClaudeEvent(context, engineer, event) {
562
571
  });
563
572
  }
564
573
  }
565
- function reportArchitectEvent(context, event) {
574
+ function reportPlanSynthesisEvent(context, event) {
566
575
  if (event.type === 'error') {
567
576
  context.metadata({
568
- title: `❌ Architect hit an error`,
577
+ title: `❌ Plan synthesis hit an error`,
569
578
  metadata: {
570
579
  sessionId: event.sessionId,
571
580
  error: event.text.slice(0, 200),
@@ -575,7 +584,7 @@ function reportArchitectEvent(context, event) {
575
584
  }
576
585
  if (event.type === 'init') {
577
586
  context.metadata({
578
- title: `⚡ Architect session ready`,
587
+ title: `⚡ Plan synthesis ready`,
579
588
  metadata: {
580
589
  sessionId: event.sessionId,
581
590
  },
@@ -608,10 +617,10 @@ function reportArchitectEvent(context, event) {
608
617
  const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
609
618
  context.metadata({
610
619
  title: toolDescription
611
- ? `⚡ Architect → ${toolDescription}`
620
+ ? `⚡ Plan synthesis → ${toolDescription}`
612
621
  : toolName
613
- ? `⚡ Architect → ${toolName}`
614
- : `⚡ Architect is using Claude Code tools`,
622
+ ? `⚡ Plan synthesis → ${toolName}`
623
+ : `⚡ Plan synthesis is running`,
615
624
  metadata: {
616
625
  sessionId: event.sessionId,
617
626
  ...(toolName !== undefined && { toolName }),
@@ -625,7 +634,7 @@ function reportArchitectEvent(context, event) {
625
634
  const isThinking = event.text.startsWith('<thinking>');
626
635
  const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
627
636
  context.metadata({
628
- title: `⚡ Architect ${stateLabel}`,
637
+ title: `⚡ Plan synthesis ${stateLabel}`,
629
638
  metadata: {
630
639
  sessionId: event.sessionId,
631
640
  preview: event.text.slice(0, 160),
@@ -635,8 +644,9 @@ function reportArchitectEvent(context, event) {
635
644
  }
636
645
  }
637
646
  function annotateToolRun(context, title, metadata) {
647
+ const agentLabel = context.agent === AGENT_CTO ? 'CTO' : undefined;
638
648
  context.metadata({
639
- title,
649
+ title: agentLabel ? `${agentLabel} → ${title}` : title,
640
650
  metadata,
641
651
  });
642
652
  }
@@ -24,7 +24,7 @@ export function getOrCreatePluginServices(worktree) {
24
24
  const teamStore = new TeamStateStore();
25
25
  const transcriptStore = new TranscriptStore();
26
26
  const manager = new PersistentManager(gitOps, transcriptStore);
27
- const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.architectSystemPrompt);
27
+ const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.planSynthesisPrompt);
28
28
  const services = {
29
29
  manager,
30
30
  sessions: sessionService,
@@ -27,7 +27,7 @@ export const managerPromptRegistry = {
27
27
  '',
28
28
  'Plan and decompose:',
29
29
  '- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
30
- '- For medium or large tasks, delegate dual-engineer exploration to two engineers, then task the `architect` subagent to synthesize their independent plans into one stronger plan.',
30
+ "- For medium or large tasks, delegate dual-engineer exploration and synthesis to the `team-planner` subagent: use `task(subagent_type: 'team-planner', ...)`. When tasking engineer subagents directly, use lowercase subagent IDs: tom, john, maya, sara, alex.",
31
31
  '- Define clear success criteria before delegating. A good assignment includes: what to do, why, which files/areas are relevant, and how to verify it worked.',
32
32
  '',
33
33
  'Delegate through the Task tool:',
@@ -76,8 +76,8 @@ export const managerPromptRegistry = {
76
76
  'Report blockers immediately with exact error output. Do not retry silently more than once.',
77
77
  'Do not run git commit, git push, git reset, git checkout, or git stash.',
78
78
  ].join('\n'),
79
- architectSystemPrompt: [
80
- 'You are the Architect. Your role is to synthesize two independent engineering plans into one stronger, unified plan.',
79
+ planSynthesisPrompt: [
80
+ 'You are synthesizing two independent engineering plans into one stronger, unified plan.',
81
81
  'Compare the lead and challenger plans on clarity, feasibility, risk, and fit to the user request.',
82
82
  'Prefer the simplest path that fully addresses the goal. Surface tradeoffs honestly.',
83
83
  'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
@@ -91,6 +91,14 @@ export const managerPromptRegistry = {
91
91
  '## Recommended Answer',
92
92
  '<answer or NONE>',
93
93
  ].join('\n'),
94
+ teamPlannerPrompt: [
95
+ 'You are the team planner. Your only job is to invoke `plan_with_team`.',
96
+ '`plan_with_team` dispatches two engineers in parallel (lead + challenger) then synthesizes their plans.',
97
+ '',
98
+ 'If the task includes a lead engineer and a challenger engineer, call `plan_with_team` immediately.',
99
+ 'If either engineer name is missing, use `question` to ask: which engineers should lead and challenge (Tom, John, Maya, Sara, or Alex)?',
100
+ 'Do not attempt any planning or analysis yourself. Delegate entirely to `plan_with_team`.',
101
+ ].join('\n'),
94
102
  contextWarnings: {
95
103
  moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
96
104
  high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
@@ -2,7 +2,10 @@ export interface ManagerPromptRegistry {
2
2
  ctoSystemPrompt: string;
3
3
  engineerAgentPrompt: string;
4
4
  engineerSessionPrompt: string;
5
- architectSystemPrompt: string;
5
+ /** Prompt injected as the system prompt of the non-persistent synthesis runTask call inside plan_with_team. */
6
+ planSynthesisPrompt: string;
7
+ /** Visible subagent prompt for teamPlanner — thin bridge that calls plan_with_team. */
8
+ teamPlannerPrompt: string;
6
9
  contextWarnings: {
7
10
  moderate: string;
8
11
  high: string;
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { ClaudeManagerPlugin } from '../src/plugin/claude-manager.plugin.js';
3
- import { AGENT_CTO, AGENT_ARCHITECT, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from '../src/plugin/agent-hierarchy.js';
3
+ import { AGENT_CTO, AGENT_TEAM_PLANNER, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from '../src/plugin/agent-hierarchy.js';
4
4
  describe('ClaudeManagerPlugin', () => {
5
5
  it('configures CTO with orchestration tools and question access', async () => {
6
6
  const plugin = await ClaudeManagerPlugin({
@@ -34,16 +34,38 @@ describe('ClaudeManagerPlugin', () => {
34
34
  git_log: 'allow',
35
35
  claude: 'deny',
36
36
  });
37
+ // Task permissions should include both uppercase (user-friendly) and lowercase (canonical) agent IDs.
37
38
  expect(cto.permission.task).toEqual({
38
39
  '*': 'deny',
40
+ Tom: 'allow',
39
41
  tom: 'allow',
42
+ John: 'allow',
40
43
  john: 'allow',
44
+ Maya: 'allow',
41
45
  maya: 'allow',
46
+ Sara: 'allow',
42
47
  sara: 'allow',
48
+ Alex: 'allow',
43
49
  alex: 'allow',
44
- architect: 'allow',
50
+ 'team-planner': 'allow',
45
51
  });
46
52
  });
53
+ it('allows CTO to delegate to engineers using both uppercase and lowercase agent IDs', async () => {
54
+ const plugin = await ClaudeManagerPlugin({
55
+ worktree: '/tmp/project',
56
+ });
57
+ const config = {};
58
+ await plugin.config?.(config);
59
+ const agents = (config.agent ?? {});
60
+ const cto = agents[AGENT_CTO];
61
+ const taskPerms = cto.permission.task;
62
+ // Verify both uppercase and lowercase can be used for delegation.
63
+ // This prevents delegation failures when users write task(subagent_type: 'Maya') vs task(subagent_type: 'maya').
64
+ expect(taskPerms.Tom).toBe('allow');
65
+ expect(taskPerms.tom).toBe('allow');
66
+ expect(taskPerms.Maya).toBe('allow');
67
+ expect(taskPerms.maya).toBe('allow');
68
+ });
47
69
  it('configures every named engineer with only the claude bridge tool', async () => {
48
70
  const plugin = await ClaudeManagerPlugin({
49
71
  worktree: '/tmp/project',
@@ -69,26 +91,25 @@ describe('ClaudeManagerPlugin', () => {
69
91
  expect(agent.permission).not.toHaveProperty('grep');
70
92
  }
71
93
  });
72
- it('configures architect as a read-only subagent for plan synthesis', async () => {
94
+ it('configures team-planner as a planning-bridge subagent', async () => {
73
95
  const plugin = await ClaudeManagerPlugin({
74
96
  worktree: '/tmp/project',
75
97
  });
76
98
  const config = {};
77
99
  await plugin.config?.(config);
78
100
  const agents = (config.agent ?? {});
79
- const architect = agents[AGENT_ARCHITECT];
80
- expect(architect).toBeDefined();
81
- expect(architect.mode).toBe('subagent');
82
- expect(architect.description.toLowerCase()).toContain('synthesiz');
83
- expect(architect.permission).toMatchObject({
101
+ const teamPlanner = agents[AGENT_TEAM_PLANNER];
102
+ expect(teamPlanner).toBeDefined();
103
+ expect(teamPlanner.mode).toBe('subagent');
104
+ expect(teamPlanner.description.toLowerCase()).toContain('plan');
105
+ expect(teamPlanner.permission).toMatchObject({
84
106
  '*': 'deny',
85
- read: 'allow',
86
- grep: 'allow',
87
- glob: 'allow',
88
- list: 'allow',
89
- codesearch: 'allow',
107
+ plan_with_team: 'allow',
108
+ question: 'allow',
90
109
  claude: 'deny',
91
110
  });
111
+ expect(teamPlanner.permission).not.toHaveProperty('read');
112
+ expect(teamPlanner.permission).not.toHaveProperty('grep');
92
113
  });
93
114
  it('registers the named engineer bridge and team status tools', async () => {
94
115
  const plugin = await ClaudeManagerPlugin({
@@ -129,3 +150,53 @@ describe('ClaudeManagerPlugin', () => {
129
150
  expect(plugin['experimental.chat.system.transform']).toBeTypeOf('function');
130
151
  });
131
152
  });
153
+ describe('Agent ID normalization and lookup helpers', () => {
154
+ it('normalizeAgentId converts mixed-case agent IDs to lowercase', async () => {
155
+ const { normalizeAgentId } = await import('../src/plugin/claude-manager.plugin.js');
156
+ expect(normalizeAgentId('Tom')).toBe('tom');
157
+ expect(normalizeAgentId('MAYA')).toBe('maya');
158
+ expect(normalizeAgentId('john')).toBe('john');
159
+ expect(normalizeAgentId('JoHn')).toBe('john');
160
+ });
161
+ it('engineerFromAgent resolves both uppercase and lowercase agent IDs', async () => {
162
+ const { engineerFromAgent } = await import('../src/plugin/claude-manager.plugin.js');
163
+ // Lowercase (canonical)
164
+ expect(engineerFromAgent('tom')).toBe('Tom');
165
+ expect(engineerFromAgent('maya')).toBe('Maya');
166
+ expect(engineerFromAgent('john')).toBe('John');
167
+ // Uppercase (normalized)
168
+ expect(engineerFromAgent('Tom')).toBe('Tom');
169
+ expect(engineerFromAgent('Maya')).toBe('Maya');
170
+ expect(engineerFromAgent('John')).toBe('John');
171
+ // Mixed case
172
+ expect(engineerFromAgent('JoHn')).toBe('John');
173
+ expect(engineerFromAgent('mAyA')).toBe('Maya');
174
+ });
175
+ it('engineerFromAgent throws on invalid agent IDs', async () => {
176
+ const { engineerFromAgent } = await import('../src/plugin/claude-manager.plugin.js');
177
+ expect(() => engineerFromAgent('invalid')).toThrow('The claude tool can only be used from a named engineer agent');
178
+ expect(() => engineerFromAgent('TomInvalid')).toThrow('The claude tool can only be used from a named engineer agent');
179
+ });
180
+ it('isEngineerAgent identifies both uppercase and lowercase agent IDs', async () => {
181
+ const { isEngineerAgent } = await import('../src/plugin/claude-manager.plugin.js');
182
+ // Lowercase (canonical)
183
+ expect(isEngineerAgent('tom')).toBe(true);
184
+ expect(isEngineerAgent('maya')).toBe(true);
185
+ expect(isEngineerAgent('john')).toBe(true);
186
+ expect(isEngineerAgent('sara')).toBe(true);
187
+ expect(isEngineerAgent('alex')).toBe(true);
188
+ // Uppercase (normalized)
189
+ expect(isEngineerAgent('Tom')).toBe(true);
190
+ expect(isEngineerAgent('Maya')).toBe(true);
191
+ expect(isEngineerAgent('John')).toBe(true);
192
+ expect(isEngineerAgent('Sara')).toBe(true);
193
+ expect(isEngineerAgent('Alex')).toBe(true);
194
+ // Mixed case
195
+ expect(isEngineerAgent('JoHn')).toBe(true);
196
+ expect(isEngineerAgent('mAyA')).toBe(true);
197
+ // Invalid
198
+ expect(isEngineerAgent('invalid')).toBe(false);
199
+ expect(isEngineerAgent('cto')).toBe(false);
200
+ expect(isEngineerAgent('team-planner')).toBe(false);
201
+ });
202
+ });
@@ -5,7 +5,7 @@ describe('managerPromptRegistry', () => {
5
5
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('You are a principal engineer orchestrating a team of AI-powered engineers');
6
6
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('Task tool');
7
7
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('dual-engineer');
8
- expect(managerPromptRegistry.ctoSystemPrompt).toContain('architect');
8
+ expect(managerPromptRegistry.ctoSystemPrompt).toContain('team-planner');
9
9
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('question');
10
10
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('Tom, John, Maya, Sara, and Alex');
11
11
  expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('clear_session');
@@ -29,12 +29,16 @@ describe('managerPromptRegistry', () => {
29
29
  expect(managerPromptRegistry.contextWarnings.high).toContain('{turns}');
30
30
  expect(managerPromptRegistry.contextWarnings.critical).toContain('near capacity');
31
31
  });
32
- it('gives the architect synthesis guidance with complete output format', () => {
33
- expect(managerPromptRegistry.architectSystemPrompt).toContain('Architect');
34
- expect(managerPromptRegistry.architectSystemPrompt).toContain('synthesiz');
35
- expect(managerPromptRegistry.architectSystemPrompt).toContain('two independent');
36
- expect(managerPromptRegistry.architectSystemPrompt).toContain('## Synthesis');
37
- expect(managerPromptRegistry.architectSystemPrompt).toContain('## Recommended Question');
38
- expect(managerPromptRegistry.architectSystemPrompt).toContain('## Recommended Answer');
32
+ it('planSynthesisPrompt contains synthesis guidance and complete output format', () => {
33
+ expect(managerPromptRegistry.planSynthesisPrompt).toContain('synthesiz');
34
+ expect(managerPromptRegistry.planSynthesisPrompt).toContain('two independent');
35
+ expect(managerPromptRegistry.planSynthesisPrompt).toContain('## Synthesis');
36
+ expect(managerPromptRegistry.planSynthesisPrompt).toContain('## Recommended Question');
37
+ expect(managerPromptRegistry.planSynthesisPrompt).toContain('## Recommended Answer');
38
+ });
39
+ it('teamPlannerPrompt directs the agent to call plan_with_team and ask if engineers are missing', () => {
40
+ expect(managerPromptRegistry.teamPlannerPrompt).toContain('plan_with_team');
41
+ expect(managerPromptRegistry.teamPlannerPrompt).toContain('question');
42
+ expect(managerPromptRegistry.teamPlannerPrompt).toContain('engineer');
39
43
  });
40
44
  });
@@ -180,7 +180,7 @@ describe('second invocation continuity', () => {
180
180
  // ── Phase 1: first task via orchestrator (no real SDK needed) ──────────
181
181
  const store = new TeamStateStore();
182
182
  await store.setActiveTeam(tempRoot, 'cto-1');
183
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Architect prompt');
183
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt');
184
184
  await orchestrator.recordWrapperSession(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1');
185
185
  await orchestrator.recordWrapperExchange(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1', 'explore', 'Investigate the auth flow', 'Found two race conditions in the token refresh path.');
186
186
  // ── Phase 2: process restart ───────────────────────────────────────────
@@ -206,7 +206,7 @@ describe('second invocation continuity', () => {
206
206
  // ── Phase 1: pre-seed Tom with a claudeSessionId ───────────────────────
207
207
  const store = new TeamStateStore();
208
208
  await store.setActiveTeam(tempRoot, 'cto-1');
209
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Architect prompt');
209
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt');
210
210
  await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
211
211
  await store.updateTeam(tempRoot, 'cto-1', (team) => ({
212
212
  ...team,
@@ -240,7 +240,7 @@ describe('second invocation continuity', () => {
240
240
  expect(runTask).toHaveBeenCalledOnce();
241
241
  expect(runTask.mock.calls[0]?.[0]).toMatchObject({
242
242
  resumeSessionId: 'ses-tom-persisted',
243
- systemPrompt: undefined, // no new system prompt when resuming
244
243
  });
244
+ expect(runTask.mock.calls[0]?.[0].systemPrompt).toBeUndefined(); // no system prompt when resuming
245
245
  });
246
246
  });
@@ -35,7 +35,7 @@ describe('TeamOrchestrator', () => {
35
35
  outputTokens: 300,
36
36
  contextWindowSize: 200_000,
37
37
  });
38
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
38
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
39
39
  const first = await orchestrator.dispatchEngineer({
40
40
  teamId: 'team-1',
41
41
  cwd: tempRoot,
@@ -53,17 +53,22 @@ describe('TeamOrchestrator', () => {
53
53
  expect(first.sessionId).toBe('ses_tom');
54
54
  expect(second.sessionId).toBe('ses_tom');
55
55
  expect(runTask.mock.calls[0]?.[0]).toMatchObject({
56
- systemPrompt: expect.stringContaining('Assigned engineer: Tom.'),
57
56
  resumeSessionId: undefined,
58
57
  permissionMode: 'acceptEdits',
59
58
  restrictWriteTools: true,
60
59
  });
60
+ expect(runTask.mock.calls[0]?.[0].systemPrompt).toBeUndefined();
61
+ expect(runTask.mock.calls[0]?.[0].prompt).toContain('Base engineer prompt');
62
+ expect(runTask.mock.calls[0]?.[0].prompt).toContain('Assigned engineer: Tom.');
63
+ expect(runTask.mock.calls[0]?.[0].prompt).toContain('Investigate the auth flow');
61
64
  expect(runTask.mock.calls[1]?.[0]).toMatchObject({
62
- systemPrompt: undefined,
63
65
  resumeSessionId: 'ses_tom',
64
66
  permissionMode: 'acceptEdits',
65
67
  restrictWriteTools: false,
66
68
  });
69
+ expect(runTask.mock.calls[1]?.[0].systemPrompt).toBeUndefined();
70
+ expect(runTask.mock.calls[1]?.[0].prompt).not.toContain('Assigned engineer: Tom.');
71
+ expect(runTask.mock.calls[1]?.[0].prompt).toContain('Implement the chosen fix');
67
72
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
68
73
  expect(team.engineers.find((engineer) => engineer.name === 'Tom')).toMatchObject({
69
74
  claudeSessionId: 'ses_tom',
@@ -74,7 +79,7 @@ describe('TeamOrchestrator', () => {
74
79
  it('rejects work when the same engineer is already busy', async () => {
75
80
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
76
81
  const store = new TeamStateStore('.state');
77
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
82
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
78
83
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
79
84
  await store.saveTeam({
80
85
  ...team,
@@ -112,7 +117,7 @@ describe('TeamOrchestrator', () => {
112
117
  events: [],
113
118
  finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
114
119
  });
115
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
120
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
116
121
  const result = await orchestrator.planWithTeam({
117
122
  teamId: 'team-1',
118
123
  cwd: tempRoot,
@@ -125,6 +130,10 @@ describe('TeamOrchestrator', () => {
125
130
  expect(result.recommendedQuestion).toBe('Should we migrate now?');
126
131
  expect(result.recommendedAnswer).toBe('No, defer it.');
127
132
  expect(runTask).toHaveBeenCalledTimes(3);
133
+ const synthesisCall = runTask.mock.calls[2]?.[0];
134
+ expect(synthesisCall.systemPrompt).toBeUndefined();
135
+ expect(synthesisCall.prompt).toContain('Synthesis prompt');
136
+ expect(synthesisCall.prompt).toContain('Plan the billing refactor');
128
137
  });
129
138
  it('invokes lead, challenger, and synthesis event callbacks', async () => {
130
139
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
@@ -157,7 +166,7 @@ describe('TeamOrchestrator', () => {
157
166
  };
158
167
  }
159
168
  });
160
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
169
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
161
170
  const onLeadEvent = vi.fn();
162
171
  const onChallengerEvent = vi.fn();
163
172
  const onSynthesisEvent = vi.fn();
@@ -177,7 +186,7 @@ describe('TeamOrchestrator', () => {
177
186
  });
178
187
  it('persists wrapper session memory for an engineer', async () => {
179
188
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
180
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
189
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
181
190
  await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
182
191
  await orchestrator.recordWrapperExchange(tempRoot, 'team-1', 'Tom', 'wrapper-tom', 'explore', 'Investigate the auth flow and compare approaches', 'The auth flow uses one shared validator and the cookie refresh path is the main risk.');
183
192
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
@@ -2,7 +2,10 @@ export interface ManagerPromptRegistry {
2
2
  ctoSystemPrompt: string;
3
3
  engineerAgentPrompt: string;
4
4
  engineerSessionPrompt: string;
5
- architectSystemPrompt: string;
5
+ /** Prompt injected as the system prompt of the non-persistent synthesis runTask call inside plan_with_team. */
6
+ planSynthesisPrompt: string;
7
+ /** Visible subagent prompt for teamPlanner — thin bridge that calls plan_with_team. */
8
+ teamPlannerPrompt: string;
6
9
  contextWarnings: {
7
10
  moderate: string;
8
11
  high: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",