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

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,7 +18,8 @@ export declare class TeamOrchestrator {
18
18
  private readonly teamStore;
19
19
  private readonly transcriptStore;
20
20
  private readonly engineerSessionPrompt;
21
- constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string);
21
+ private readonly architectSystemPrompt;
22
+ constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, architectSystemPrompt: string);
22
23
  getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
23
24
  listTeams(cwd: string): Promise<TeamRecord[]>;
24
25
  recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
@@ -44,6 +45,9 @@ export declare class TeamOrchestrator {
44
45
  challengerEngineer: EngineerName;
45
46
  model?: string;
46
47
  abortSignal?: AbortSignal;
48
+ onLeadEvent?: ClaudeSessionEventHandler;
49
+ onChallengerEvent?: ClaudeSessionEventHandler;
50
+ onSynthesisEvent?: ClaudeSessionEventHandler;
47
51
  }): Promise<SynthesizedPlanResult>;
48
52
  private updateEngineer;
49
53
  private reserveEngineer;
@@ -6,11 +6,13 @@ export class TeamOrchestrator {
6
6
  teamStore;
7
7
  transcriptStore;
8
8
  engineerSessionPrompt;
9
- constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt) {
9
+ architectSystemPrompt;
10
+ constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, architectSystemPrompt) {
10
11
  this.sessions = sessions;
11
12
  this.teamStore = teamStore;
12
13
  this.transcriptStore = transcriptStore;
13
14
  this.engineerSessionPrompt = engineerSessionPrompt;
15
+ this.architectSystemPrompt = architectSystemPrompt;
14
16
  }
15
17
  async getOrCreateTeam(cwd, teamId) {
16
18
  const existing = await this.teamStore.getTeam(cwd, teamId);
@@ -203,6 +205,7 @@ export class TeamOrchestrator {
203
205
  message: buildPlanDraftRequest('lead', input.request),
204
206
  model: input.model,
205
207
  abortSignal: input.abortSignal,
208
+ onEvent: input.onLeadEvent,
206
209
  }),
207
210
  this.dispatchEngineer({
208
211
  teamId: input.teamId,
@@ -212,6 +215,7 @@ export class TeamOrchestrator {
212
215
  message: buildPlanDraftRequest('challenger', input.request),
213
216
  model: input.model,
214
217
  abortSignal: input.abortSignal,
218
+ onEvent: input.onChallengerEvent,
215
219
  }),
216
220
  ]);
217
221
  const drafts = [
@@ -221,7 +225,7 @@ export class TeamOrchestrator {
221
225
  const synthesisResult = await this.sessions.runTask({
222
226
  cwd: input.cwd,
223
227
  prompt: buildSynthesisPrompt(input.request, drafts),
224
- systemPrompt: buildSynthesisSystemPrompt(),
228
+ systemPrompt: buildSynthesisSystemPrompt(this.architectSystemPrompt),
225
229
  persistSession: false,
226
230
  includePartialMessages: false,
227
231
  permissionMode: 'acceptEdits',
@@ -230,7 +234,7 @@ export class TeamOrchestrator {
230
234
  effort: 'high',
231
235
  settingSources: ['user', 'project', 'local'],
232
236
  abortSignal: input.abortSignal,
233
- });
237
+ }, input.onSynthesisEvent);
234
238
  const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
235
239
  return {
236
240
  teamId: input.teamId,
@@ -361,20 +365,8 @@ function buildPlanDraftRequest(perspective, request) {
361
365
  `User request: ${request}`,
362
366
  ].join('\n');
363
367
  }
364
- function buildSynthesisSystemPrompt() {
365
- return [
366
- 'You are synthesizing two independent engineering plans into one stronger plan.',
367
- 'Compare them on clarity, feasibility, risk, and fit to the user request.',
368
- 'Prefer the simplest path that fully addresses the goal.',
369
- 'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
370
- 'Use this output format exactly:',
371
- '## Synthesis',
372
- '<combined plan>',
373
- '## Recommended Question',
374
- '<question or NONE>',
375
- '## Recommended Answer',
376
- '<answer or NONE>',
377
- ].join('\n');
368
+ function buildSynthesisSystemPrompt(architectSystemPrompt) {
369
+ return architectSystemPrompt;
378
370
  }
379
371
  function buildSynthesisPrompt(request, drafts) {
380
372
  return [
@@ -1,5 +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
4
  export declare const ENGINEER_AGENT_IDS: {
4
5
  readonly Tom: "tom";
5
6
  readonly John: "john";
@@ -41,5 +42,13 @@ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry,
41
42
  permission: AgentPermission;
42
43
  prompt: string;
43
44
  };
45
+ export declare function buildArchitectAgentConfig(prompts: ManagerPromptRegistry): {
46
+ description: string;
47
+ mode: "subagent";
48
+ hidden: boolean;
49
+ color: string;
50
+ permission: AgentPermission;
51
+ prompt: string;
52
+ };
44
53
  export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
45
54
  export {};
@@ -1,5 +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
4
  export const ENGINEER_AGENT_IDS = {
4
5
  Tom: 'tom',
5
6
  John: 'john',
@@ -10,7 +11,6 @@ export const ENGINEER_AGENT_IDS = {
10
11
  export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
11
12
  const CTO_ONLY_TOOL_IDS = [
12
13
  'team_status',
13
- 'plan_with_team',
14
14
  'reset_engineer',
15
15
  'list_transcripts',
16
16
  'list_history',
@@ -38,6 +38,27 @@ const CTO_READONLY_TOOLS = {
38
38
  todoread: 'allow',
39
39
  question: 'allow',
40
40
  };
41
+ function buildArchitectPermissions() {
42
+ const denied = {};
43
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
44
+ denied[toolId] = 'deny';
45
+ }
46
+ return {
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',
59
+ ...denied,
60
+ };
61
+ }
41
62
  function buildCtoPermissions() {
42
63
  const denied = {};
43
64
  for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
@@ -51,6 +72,7 @@ function buildCtoPermissions() {
51
72
  for (const engineer of ENGINEER_AGENT_NAMES) {
52
73
  taskPermissions[ENGINEER_AGENT_IDS[engineer]] = 'allow';
53
74
  }
75
+ taskPermissions[AGENT_ARCHITECT] = 'allow';
54
76
  return {
55
77
  '*': 'deny',
56
78
  ...CTO_READONLY_TOOLS,
@@ -89,6 +111,16 @@ export function buildEngineerAgentConfig(prompts, engineer) {
89
111
  prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
90
112
  };
91
113
  }
114
+ export function buildArchitectAgentConfig(prompts) {
115
+ return {
116
+ description: 'Synthesizes two engineer plan drafts into one stronger, actionable plan.',
117
+ mode: 'subagent',
118
+ hidden: false,
119
+ color: '#D97757',
120
+ permission: buildArchitectPermissions(),
121
+ prompt: prompts.architectSystemPrompt,
122
+ };
123
+ }
92
124
  export function denyRestrictedToolsGlobally(permissions) {
93
125
  for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
94
126
  permissions[toolId] ??= 'deny';
@@ -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, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
5
+ import { AGENT_CTO, AGENT_ARCHITECT, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildArchitectAgentConfig, 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,6 +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
19
  for (const engineer of ENGINEER_AGENT_NAMES) {
19
20
  config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
20
21
  }
@@ -126,9 +127,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
126
127
  challengerEngineer: args.challengerEngineer,
127
128
  model: args.model,
128
129
  abortSignal: context.abort,
130
+ onLeadEvent: (event) => reportClaudeEvent(context, args.leadEngineer, event),
131
+ onChallengerEvent: (event) => reportClaudeEvent(context, args.challengerEngineer, event),
132
+ onSynthesisEvent: (event) => reportArchitectEvent(context, event),
129
133
  });
130
134
  context.metadata({
131
- title: '✅ Plan synthesis complete',
135
+ title: '✅ Architect finished',
132
136
  metadata: {
133
137
  teamId: result.teamId,
134
138
  lead: result.leadEngineer,
@@ -558,6 +562,78 @@ function reportClaudeEvent(context, engineer, event) {
558
562
  });
559
563
  }
560
564
  }
565
+ function reportArchitectEvent(context, event) {
566
+ if (event.type === 'error') {
567
+ context.metadata({
568
+ title: `❌ Architect hit an error`,
569
+ metadata: {
570
+ sessionId: event.sessionId,
571
+ error: event.text.slice(0, 200),
572
+ },
573
+ });
574
+ return;
575
+ }
576
+ if (event.type === 'init') {
577
+ context.metadata({
578
+ title: `⚡ Architect session ready`,
579
+ metadata: {
580
+ sessionId: event.sessionId,
581
+ },
582
+ });
583
+ return;
584
+ }
585
+ if (event.type === 'tool_call') {
586
+ let toolName;
587
+ let toolId;
588
+ let toolArgs;
589
+ try {
590
+ const parsed = JSON.parse(event.text);
591
+ toolName = parsed.name;
592
+ toolId = parsed.id;
593
+ if (typeof parsed.input === 'string') {
594
+ try {
595
+ toolArgs = JSON.parse(parsed.input);
596
+ }
597
+ catch {
598
+ toolArgs = parsed.input;
599
+ }
600
+ }
601
+ else {
602
+ toolArgs = parsed.input;
603
+ }
604
+ }
605
+ catch {
606
+ // event.text is not valid JSON — fall back to generic title
607
+ }
608
+ const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
609
+ context.metadata({
610
+ title: toolDescription
611
+ ? `⚡ Architect → ${toolDescription}`
612
+ : toolName
613
+ ? `⚡ Architect → ${toolName}`
614
+ : `⚡ Architect is using Claude Code tools`,
615
+ metadata: {
616
+ sessionId: event.sessionId,
617
+ ...(toolName !== undefined && { toolName }),
618
+ ...(toolId !== undefined && { toolId }),
619
+ ...(toolArgs !== undefined && { toolArgs }),
620
+ },
621
+ });
622
+ return;
623
+ }
624
+ if (event.type === 'assistant' || event.type === 'partial') {
625
+ const isThinking = event.text.startsWith('<thinking>');
626
+ const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
627
+ context.metadata({
628
+ title: `⚡ Architect ${stateLabel}`,
629
+ metadata: {
630
+ sessionId: event.sessionId,
631
+ preview: event.text.slice(0, 160),
632
+ isThinking,
633
+ },
634
+ });
635
+ }
636
+ }
561
637
  function annotateToolRun(context, title, metadata) {
562
638
  context.metadata({
563
639
  title,
@@ -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);
27
+ const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.architectSystemPrompt);
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, dispatch two engineers with complementary perspectives (lead plan + challenger review), then synthesize.',
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.',
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,6 +76,21 @@ 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.',
81
+ 'Compare the lead and challenger plans on clarity, feasibility, risk, and fit to the user request.',
82
+ 'Prefer the simplest path that fully addresses the goal. Surface tradeoffs honestly.',
83
+ 'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
84
+ 'Do not editorialize or over-explain. Be direct and concise.',
85
+ '',
86
+ 'Use this output format exactly:',
87
+ '## Synthesis',
88
+ '<combined plan>',
89
+ '## Recommended Question',
90
+ '<question or NONE>',
91
+ '## Recommended Answer',
92
+ '<answer or NONE>',
93
+ ].join('\n'),
79
94
  contextWarnings: {
80
95
  moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
81
96
  high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
@@ -18,7 +18,8 @@ export declare class TeamOrchestrator {
18
18
  private readonly teamStore;
19
19
  private readonly transcriptStore;
20
20
  private readonly engineerSessionPrompt;
21
- constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string);
21
+ private readonly architectSystemPrompt;
22
+ constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, architectSystemPrompt: string);
22
23
  getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
23
24
  listTeams(cwd: string): Promise<TeamRecord[]>;
24
25
  recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
@@ -44,6 +45,9 @@ export declare class TeamOrchestrator {
44
45
  challengerEngineer: EngineerName;
45
46
  model?: string;
46
47
  abortSignal?: AbortSignal;
48
+ onLeadEvent?: ClaudeSessionEventHandler;
49
+ onChallengerEvent?: ClaudeSessionEventHandler;
50
+ onSynthesisEvent?: ClaudeSessionEventHandler;
47
51
  }): Promise<SynthesizedPlanResult>;
48
52
  private updateEngineer;
49
53
  private reserveEngineer;
@@ -6,11 +6,13 @@ export class TeamOrchestrator {
6
6
  teamStore;
7
7
  transcriptStore;
8
8
  engineerSessionPrompt;
9
- constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt) {
9
+ architectSystemPrompt;
10
+ constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, architectSystemPrompt) {
10
11
  this.sessions = sessions;
11
12
  this.teamStore = teamStore;
12
13
  this.transcriptStore = transcriptStore;
13
14
  this.engineerSessionPrompt = engineerSessionPrompt;
15
+ this.architectSystemPrompt = architectSystemPrompt;
14
16
  }
15
17
  async getOrCreateTeam(cwd, teamId) {
16
18
  const existing = await this.teamStore.getTeam(cwd, teamId);
@@ -203,6 +205,7 @@ export class TeamOrchestrator {
203
205
  message: buildPlanDraftRequest('lead', input.request),
204
206
  model: input.model,
205
207
  abortSignal: input.abortSignal,
208
+ onEvent: input.onLeadEvent,
206
209
  }),
207
210
  this.dispatchEngineer({
208
211
  teamId: input.teamId,
@@ -212,6 +215,7 @@ export class TeamOrchestrator {
212
215
  message: buildPlanDraftRequest('challenger', input.request),
213
216
  model: input.model,
214
217
  abortSignal: input.abortSignal,
218
+ onEvent: input.onChallengerEvent,
215
219
  }),
216
220
  ]);
217
221
  const drafts = [
@@ -221,7 +225,7 @@ export class TeamOrchestrator {
221
225
  const synthesisResult = await this.sessions.runTask({
222
226
  cwd: input.cwd,
223
227
  prompt: buildSynthesisPrompt(input.request, drafts),
224
- systemPrompt: buildSynthesisSystemPrompt(),
228
+ systemPrompt: buildSynthesisSystemPrompt(this.architectSystemPrompt),
225
229
  persistSession: false,
226
230
  includePartialMessages: false,
227
231
  permissionMode: 'acceptEdits',
@@ -230,7 +234,7 @@ export class TeamOrchestrator {
230
234
  effort: 'high',
231
235
  settingSources: ['user', 'project', 'local'],
232
236
  abortSignal: input.abortSignal,
233
- });
237
+ }, input.onSynthesisEvent);
234
238
  const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
235
239
  return {
236
240
  teamId: input.teamId,
@@ -361,20 +365,8 @@ function buildPlanDraftRequest(perspective, request) {
361
365
  `User request: ${request}`,
362
366
  ].join('\n');
363
367
  }
364
- function buildSynthesisSystemPrompt() {
365
- return [
366
- 'You are synthesizing two independent engineering plans into one stronger plan.',
367
- 'Compare them on clarity, feasibility, risk, and fit to the user request.',
368
- 'Prefer the simplest path that fully addresses the goal.',
369
- 'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
370
- 'Use this output format exactly:',
371
- '## Synthesis',
372
- '<combined plan>',
373
- '## Recommended Question',
374
- '<question or NONE>',
375
- '## Recommended Answer',
376
- '<answer or NONE>',
377
- ].join('\n');
368
+ function buildSynthesisSystemPrompt(architectSystemPrompt) {
369
+ return architectSystemPrompt;
378
370
  }
379
371
  function buildSynthesisPrompt(request, drafts) {
380
372
  return [
@@ -1,5 +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
4
  export declare const ENGINEER_AGENT_IDS: {
4
5
  readonly Tom: "tom";
5
6
  readonly John: "john";
@@ -41,5 +42,13 @@ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry,
41
42
  permission: AgentPermission;
42
43
  prompt: string;
43
44
  };
45
+ export declare function buildArchitectAgentConfig(prompts: ManagerPromptRegistry): {
46
+ description: string;
47
+ mode: "subagent";
48
+ hidden: boolean;
49
+ color: string;
50
+ permission: AgentPermission;
51
+ prompt: string;
52
+ };
44
53
  export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
45
54
  export {};
@@ -1,5 +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
4
  export const ENGINEER_AGENT_IDS = {
4
5
  Tom: 'tom',
5
6
  John: 'john',
@@ -10,7 +11,6 @@ export const ENGINEER_AGENT_IDS = {
10
11
  export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
11
12
  const CTO_ONLY_TOOL_IDS = [
12
13
  'team_status',
13
- 'plan_with_team',
14
14
  'reset_engineer',
15
15
  'list_transcripts',
16
16
  'list_history',
@@ -38,6 +38,27 @@ const CTO_READONLY_TOOLS = {
38
38
  todoread: 'allow',
39
39
  question: 'allow',
40
40
  };
41
+ function buildArchitectPermissions() {
42
+ const denied = {};
43
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
44
+ denied[toolId] = 'deny';
45
+ }
46
+ return {
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',
59
+ ...denied,
60
+ };
61
+ }
41
62
  function buildCtoPermissions() {
42
63
  const denied = {};
43
64
  for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
@@ -51,6 +72,7 @@ function buildCtoPermissions() {
51
72
  for (const engineer of ENGINEER_AGENT_NAMES) {
52
73
  taskPermissions[ENGINEER_AGENT_IDS[engineer]] = 'allow';
53
74
  }
75
+ taskPermissions[AGENT_ARCHITECT] = 'allow';
54
76
  return {
55
77
  '*': 'deny',
56
78
  ...CTO_READONLY_TOOLS,
@@ -89,6 +111,16 @@ export function buildEngineerAgentConfig(prompts, engineer) {
89
111
  prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
90
112
  };
91
113
  }
114
+ export function buildArchitectAgentConfig(prompts) {
115
+ return {
116
+ description: 'Synthesizes two engineer plan drafts into one stronger, actionable plan.',
117
+ mode: 'subagent',
118
+ hidden: false,
119
+ color: '#D97757',
120
+ permission: buildArchitectPermissions(),
121
+ prompt: prompts.architectSystemPrompt,
122
+ };
123
+ }
92
124
  export function denyRestrictedToolsGlobally(permissions) {
93
125
  for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
94
126
  permissions[toolId] ??= 'deny';
@@ -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, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
5
+ import { AGENT_CTO, AGENT_ARCHITECT, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildArchitectAgentConfig, 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,6 +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
19
  for (const engineer of ENGINEER_AGENT_NAMES) {
19
20
  config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
20
21
  }
@@ -126,9 +127,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
126
127
  challengerEngineer: args.challengerEngineer,
127
128
  model: args.model,
128
129
  abortSignal: context.abort,
130
+ onLeadEvent: (event) => reportClaudeEvent(context, args.leadEngineer, event),
131
+ onChallengerEvent: (event) => reportClaudeEvent(context, args.challengerEngineer, event),
132
+ onSynthesisEvent: (event) => reportArchitectEvent(context, event),
129
133
  });
130
134
  context.metadata({
131
- title: '✅ Plan synthesis complete',
135
+ title: '✅ Architect finished',
132
136
  metadata: {
133
137
  teamId: result.teamId,
134
138
  lead: result.leadEngineer,
@@ -558,6 +562,78 @@ function reportClaudeEvent(context, engineer, event) {
558
562
  });
559
563
  }
560
564
  }
565
+ function reportArchitectEvent(context, event) {
566
+ if (event.type === 'error') {
567
+ context.metadata({
568
+ title: `❌ Architect hit an error`,
569
+ metadata: {
570
+ sessionId: event.sessionId,
571
+ error: event.text.slice(0, 200),
572
+ },
573
+ });
574
+ return;
575
+ }
576
+ if (event.type === 'init') {
577
+ context.metadata({
578
+ title: `⚡ Architect session ready`,
579
+ metadata: {
580
+ sessionId: event.sessionId,
581
+ },
582
+ });
583
+ return;
584
+ }
585
+ if (event.type === 'tool_call') {
586
+ let toolName;
587
+ let toolId;
588
+ let toolArgs;
589
+ try {
590
+ const parsed = JSON.parse(event.text);
591
+ toolName = parsed.name;
592
+ toolId = parsed.id;
593
+ if (typeof parsed.input === 'string') {
594
+ try {
595
+ toolArgs = JSON.parse(parsed.input);
596
+ }
597
+ catch {
598
+ toolArgs = parsed.input;
599
+ }
600
+ }
601
+ else {
602
+ toolArgs = parsed.input;
603
+ }
604
+ }
605
+ catch {
606
+ // event.text is not valid JSON — fall back to generic title
607
+ }
608
+ const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
609
+ context.metadata({
610
+ title: toolDescription
611
+ ? `⚡ Architect → ${toolDescription}`
612
+ : toolName
613
+ ? `⚡ Architect → ${toolName}`
614
+ : `⚡ Architect is using Claude Code tools`,
615
+ metadata: {
616
+ sessionId: event.sessionId,
617
+ ...(toolName !== undefined && { toolName }),
618
+ ...(toolId !== undefined && { toolId }),
619
+ ...(toolArgs !== undefined && { toolArgs }),
620
+ },
621
+ });
622
+ return;
623
+ }
624
+ if (event.type === 'assistant' || event.type === 'partial') {
625
+ const isThinking = event.text.startsWith('<thinking>');
626
+ const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
627
+ context.metadata({
628
+ title: `⚡ Architect ${stateLabel}`,
629
+ metadata: {
630
+ sessionId: event.sessionId,
631
+ preview: event.text.slice(0, 160),
632
+ isThinking,
633
+ },
634
+ });
635
+ }
636
+ }
561
637
  function annotateToolRun(context, title, metadata) {
562
638
  context.metadata({
563
639
  title,
@@ -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);
27
+ const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.architectSystemPrompt);
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, dispatch two engineers with complementary perspectives (lead plan + challenger review), then synthesize.',
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.',
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,6 +76,21 @@ 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.',
81
+ 'Compare the lead and challenger plans on clarity, feasibility, risk, and fit to the user request.',
82
+ 'Prefer the simplest path that fully addresses the goal. Surface tradeoffs honestly.',
83
+ 'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
84
+ 'Do not editorialize or over-explain. Be direct and concise.',
85
+ '',
86
+ 'Use this output format exactly:',
87
+ '## Synthesis',
88
+ '<combined plan>',
89
+ '## Recommended Question',
90
+ '<question or NONE>',
91
+ '## Recommended Answer',
92
+ '<answer or NONE>',
93
+ ].join('\n'),
79
94
  contextWarnings: {
80
95
  moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
81
96
  high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
@@ -2,6 +2,7 @@ export interface ManagerPromptRegistry {
2
2
  ctoSystemPrompt: string;
3
3
  engineerAgentPrompt: string;
4
4
  engineerSessionPrompt: string;
5
+ architectSystemPrompt: string;
5
6
  contextWarnings: {
6
7
  moderate: string;
7
8
  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, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from '../src/plugin/agent-hierarchy.js';
3
+ import { AGENT_CTO, AGENT_ARCHITECT, 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({
@@ -26,7 +26,6 @@ describe('ClaudeManagerPlugin', () => {
26
26
  todoread: 'allow',
27
27
  question: 'allow',
28
28
  team_status: 'allow',
29
- plan_with_team: 'allow',
30
29
  reset_engineer: 'allow',
31
30
  git_diff: 'allow',
32
31
  git_commit: 'allow',
@@ -42,6 +41,7 @@ describe('ClaudeManagerPlugin', () => {
42
41
  maya: 'allow',
43
42
  sara: 'allow',
44
43
  alex: 'allow',
44
+ architect: 'allow',
45
45
  });
46
46
  });
47
47
  it('configures every named engineer with only the claude bridge tool', async () => {
@@ -63,13 +63,33 @@ describe('ClaudeManagerPlugin', () => {
63
63
  claude: 'allow',
64
64
  git_diff: 'deny',
65
65
  git_commit: 'deny',
66
- plan_with_team: 'deny',
67
66
  reset_engineer: 'deny',
68
67
  });
69
68
  expect(agent.permission).not.toHaveProperty('read');
70
69
  expect(agent.permission).not.toHaveProperty('grep');
71
70
  }
72
71
  });
72
+ it('configures architect as a read-only subagent for plan synthesis', async () => {
73
+ const plugin = await ClaudeManagerPlugin({
74
+ worktree: '/tmp/project',
75
+ });
76
+ const config = {};
77
+ await plugin.config?.(config);
78
+ 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({
84
+ '*': 'deny',
85
+ read: 'allow',
86
+ grep: 'allow',
87
+ glob: 'allow',
88
+ list: 'allow',
89
+ codesearch: 'allow',
90
+ claude: 'deny',
91
+ });
92
+ });
73
93
  it('registers the named engineer bridge and team status tools', async () => {
74
94
  const plugin = await ClaudeManagerPlugin({
75
95
  worktree: '/tmp/project',
@@ -4,7 +4,8 @@ describe('managerPromptRegistry', () => {
4
4
  it('gives the CTO explicit orchestration guidance', () => {
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
- expect(managerPromptRegistry.ctoSystemPrompt).toContain('dispatch two engineers with complementary perspectives');
7
+ expect(managerPromptRegistry.ctoSystemPrompt).toContain('dual-engineer');
8
+ expect(managerPromptRegistry.ctoSystemPrompt).toContain('architect');
8
9
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('question');
9
10
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('Tom, John, Maya, Sara, and Alex');
10
11
  expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('clear_session');
@@ -28,4 +29,12 @@ describe('managerPromptRegistry', () => {
28
29
  expect(managerPromptRegistry.contextWarnings.high).toContain('{turns}');
29
30
  expect(managerPromptRegistry.contextWarnings.critical).toContain('near capacity');
30
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');
39
+ });
31
40
  });
@@ -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');
183
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Architect 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');
209
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Architect prompt');
210
210
  await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
211
211
  await store.updateTeam(tempRoot, 'cto-1', (team) => ({
212
212
  ...team,
@@ -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');
38
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
39
39
  const first = await orchestrator.dispatchEngineer({
40
40
  teamId: 'team-1',
41
41
  cwd: tempRoot,
@@ -74,7 +74,7 @@ describe('TeamOrchestrator', () => {
74
74
  it('rejects work when the same engineer is already busy', async () => {
75
75
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
76
76
  const store = new TeamStateStore('.state');
77
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
77
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
78
78
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
79
79
  await store.saveTeam({
80
80
  ...team,
@@ -112,7 +112,7 @@ describe('TeamOrchestrator', () => {
112
112
  events: [],
113
113
  finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
114
114
  });
115
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
115
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
116
116
  const result = await orchestrator.planWithTeam({
117
117
  teamId: 'team-1',
118
118
  cwd: tempRoot,
@@ -126,9 +126,58 @@ describe('TeamOrchestrator', () => {
126
126
  expect(result.recommendedAnswer).toBe('No, defer it.');
127
127
  expect(runTask).toHaveBeenCalledTimes(3);
128
128
  });
129
+ it('invokes lead, challenger, and synthesis event callbacks', async () => {
130
+ tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
131
+ const runTask = vi.fn(async (input, onEvent) => {
132
+ // Simulate event callbacks being invoked by the session
133
+ if (onEvent) {
134
+ await Promise.resolve(onEvent({ type: 'init', text: 'initialized' }));
135
+ }
136
+ // Return appropriate result based on call count
137
+ const calls = runTask.mock.calls.length;
138
+ if (calls === 1) {
139
+ return {
140
+ sessionId: 'ses_tom',
141
+ events: [{ type: 'init', text: 'initialized', sessionId: 'ses_tom' }],
142
+ finalText: 'Lead plan',
143
+ };
144
+ }
145
+ else if (calls === 2) {
146
+ return {
147
+ sessionId: 'ses_maya',
148
+ events: [{ type: 'init', text: 'initialized', sessionId: 'ses_maya' }],
149
+ finalText: 'Challenger plan',
150
+ };
151
+ }
152
+ else {
153
+ return {
154
+ sessionId: undefined,
155
+ events: [{ type: 'init', text: 'initialized', sessionId: undefined }],
156
+ finalText: '## Synthesis\nSynthesis\n## Recommended Question\nNONE\n## Recommended Answer\nNONE',
157
+ };
158
+ }
159
+ });
160
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
161
+ const onLeadEvent = vi.fn();
162
+ const onChallengerEvent = vi.fn();
163
+ const onSynthesisEvent = vi.fn();
164
+ await orchestrator.planWithTeam({
165
+ teamId: 'team-1',
166
+ cwd: tempRoot,
167
+ request: 'Plan the refactor',
168
+ leadEngineer: 'Tom',
169
+ challengerEngineer: 'Maya',
170
+ onLeadEvent,
171
+ onChallengerEvent,
172
+ onSynthesisEvent,
173
+ });
174
+ expect(onLeadEvent).toHaveBeenCalled();
175
+ expect(onChallengerEvent).toHaveBeenCalled();
176
+ expect(onSynthesisEvent).toHaveBeenCalled();
177
+ });
129
178
  it('persists wrapper session memory for an engineer', async () => {
130
179
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
131
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
180
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
132
181
  await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
133
182
  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.');
134
183
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
@@ -2,6 +2,7 @@ export interface ManagerPromptRegistry {
2
2
  ctoSystemPrompt: string;
3
3
  engineerAgentPrompt: string;
4
4
  engineerSessionPrompt: string;
5
+ architectSystemPrompt: string;
5
6
  contextWarnings: {
6
7
  moderate: string;
7
8
  high: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",