@doingdev/opencode-claude-manager-plugin 0.1.55 → 0.1.57

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>;
@@ -33,7 +33,7 @@ export declare class TeamOrchestrator {
33
33
  clearSession?: boolean;
34
34
  clearHistory?: boolean;
35
35
  }): Promise<void>;
36
- dispatchEngineer(input: DispatchEngineerInput): Promise<EngineerTaskResult>;
36
+ dispatchEngineer(input: DispatchEngineerInput, retryCount?: number): Promise<EngineerTaskResult>;
37
37
  static classifyError(error: unknown): EngineerFailureResult & {
38
38
  cause: unknown;
39
39
  };
@@ -41,8 +41,8 @@ export declare class TeamOrchestrator {
41
41
  teamId: string;
42
42
  cwd: string;
43
43
  request: string;
44
- leadEngineer: EngineerName;
45
- challengerEngineer: EngineerName;
44
+ leadEngineer?: EngineerName;
45
+ challengerEngineer?: EngineerName;
46
46
  model?: string;
47
47
  abortSignal?: AbortSignal;
48
48
  onLeadEvent?: ClaudeSessionEventHandler;
@@ -53,7 +53,14 @@ export declare class TeamOrchestrator {
53
53
  private reserveEngineer;
54
54
  private getEngineerState;
55
55
  private normalizeTeamRecord;
56
+ getAvailableEngineers(team: TeamRecord): EngineerName[];
57
+ selectPlanEngineers(cwd: string, teamId: string, preferredLead?: EngineerName, preferredChallenger?: EngineerName): Promise<{
58
+ lead: EngineerName;
59
+ challenger: EngineerName;
60
+ }>;
56
61
  private buildSessionSystemPrompt;
57
62
  private buildEngineerPrompt;
58
63
  }
64
+ export declare function getFailureGuidanceText(failureKind: string): string;
65
+ export declare function createActionableError(failure: EngineerFailureResult, originalError: unknown): Error;
59
66
  export {};
@@ -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);
@@ -88,7 +88,7 @@ export class TeamOrchestrator {
88
88
  context: options?.clearSession ? createEmptyEngineerRecord(engineer).context : entry.context,
89
89
  }));
90
90
  }
91
- async dispatchEngineer(input) {
91
+ async dispatchEngineer(input, retryCount = 0) {
92
92
  const team = await this.getOrCreateTeam(input.cwd, input.teamId);
93
93
  const engineerState = this.getEngineerState(team, input.engineer);
94
94
  await this.reserveEngineer(input.cwd, input.teamId, input.engineer);
@@ -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,
@@ -162,6 +161,39 @@ export class TeamOrchestrator {
162
161
  busy: false,
163
162
  busySince: null,
164
163
  }));
164
+ // Handle context exhaustion with automatic retry (max 1 retry)
165
+ const classified = TeamOrchestrator.classifyError(error);
166
+ if (classified.failureKind === 'contextExhausted' && retryCount === 0) {
167
+ // Reset the engineer's session and retry once with fresh session
168
+ await this.resetEngineer(input.cwd, input.teamId, input.engineer, {
169
+ clearSession: true,
170
+ clearHistory: false,
171
+ });
172
+ // Emit status event before retry
173
+ await input.onEvent?.({
174
+ type: 'status',
175
+ text: 'Context exhausted; resetting session and retrying once with a fresh session.',
176
+ });
177
+ try {
178
+ // Retry dispatch with fresh session (retryCount=1 prevents infinite loop)
179
+ // Use the exact same assignment message without modification
180
+ return await this.dispatchEngineer(input, 1);
181
+ }
182
+ catch (retryError) {
183
+ // If retry also fails with a different error, preserve retry failure info
184
+ const retryClassified = TeamOrchestrator.classifyError(retryError);
185
+ if (retryClassified.failureKind !== classified.failureKind) {
186
+ // Create an error that shows both failures
187
+ const combinedMessage = `Initial: ${classified.failureKind} (${classified.message})\n` +
188
+ `After retry: ${retryClassified.failureKind} (${retryClassified.message})`;
189
+ const combinedError = new Error(combinedMessage);
190
+ Object.assign(combinedError, { cause: retryError });
191
+ throw combinedError;
192
+ }
193
+ // Same error type on retry, throw the retry error (more recent state)
194
+ throw retryError;
195
+ }
196
+ }
165
197
  throw error;
166
198
  }
167
199
  }
@@ -193,14 +225,13 @@ export class TeamOrchestrator {
193
225
  };
194
226
  }
195
227
  async planWithTeam(input) {
196
- if (input.leadEngineer === input.challengerEngineer) {
197
- throw new Error('Choose two different engineers for plan synthesis.');
198
- }
228
+ // Auto-select engineers if not provided
229
+ const { lead: leadEngineer, challenger: challengerEngineer } = await this.selectPlanEngineers(input.cwd, input.teamId, input.leadEngineer, input.challengerEngineer);
199
230
  const [leadDraft, challengerDraft] = await Promise.all([
200
231
  this.dispatchEngineer({
201
232
  teamId: input.teamId,
202
233
  cwd: input.cwd,
203
- engineer: input.leadEngineer,
234
+ engineer: leadEngineer,
204
235
  mode: 'explore',
205
236
  message: buildPlanDraftRequest('lead', input.request),
206
237
  model: input.model,
@@ -210,7 +241,7 @@ export class TeamOrchestrator {
210
241
  this.dispatchEngineer({
211
242
  teamId: input.teamId,
212
243
  cwd: input.cwd,
213
- engineer: input.challengerEngineer,
244
+ engineer: challengerEngineer,
214
245
  mode: 'explore',
215
246
  message: buildPlanDraftRequest('challenger', input.request),
216
247
  model: input.model,
@@ -224,8 +255,7 @@ export class TeamOrchestrator {
224
255
  ];
225
256
  const synthesisResult = await this.sessions.runTask({
226
257
  cwd: input.cwd,
227
- prompt: buildSynthesisPrompt(input.request, drafts),
228
- systemPrompt: buildSynthesisSystemPrompt(this.architectSystemPrompt),
258
+ prompt: `${this.planSynthesisPrompt}\n\n${buildSynthesisPrompt(input.request, drafts)}`,
229
259
  persistSession: false,
230
260
  includePartialMessages: false,
231
261
  permissionMode: 'acceptEdits',
@@ -239,8 +269,8 @@ export class TeamOrchestrator {
239
269
  return {
240
270
  teamId: input.teamId,
241
271
  request: input.request,
242
- leadEngineer: input.leadEngineer,
243
- challengerEngineer: input.challengerEngineer,
272
+ leadEngineer,
273
+ challengerEngineer,
244
274
  drafts,
245
275
  synthesis: parsedSynthesis.synthesis,
246
276
  recommendedQuestion: parsedSynthesis.recommendedQuestion,
@@ -296,6 +326,47 @@ export class TeamOrchestrator {
296
326
  engineers: createEmptyTeamRecord(team.id, team.cwd).engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
297
327
  };
298
328
  }
329
+ getAvailableEngineers(team) {
330
+ const now = Date.now();
331
+ return team.engineers
332
+ .filter((engineer) => {
333
+ if (!engineer.busy)
334
+ return true;
335
+ // If an engineer has been marked busy but the lease expired, they're available
336
+ if (engineer.busySince) {
337
+ const leaseExpired = now - new Date(engineer.busySince).getTime() > BUSY_LEASE_MS;
338
+ return leaseExpired;
339
+ }
340
+ return false;
341
+ })
342
+ .sort((a, b) => {
343
+ // Prefer engineers with lower context pressure and less-recently-used
344
+ const aContext = a.context.estimatedContextPercent ?? 0;
345
+ const bContext = b.context.estimatedContextPercent ?? 0;
346
+ if (aContext !== bContext) {
347
+ return aContext - bContext; // Lower context first
348
+ }
349
+ // If context is equal, prefer less-recently-used
350
+ const aTime = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0;
351
+ const bTime = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0;
352
+ return aTime - bTime; // Earlier usage time first
353
+ })
354
+ .map((engineer) => engineer.name);
355
+ }
356
+ async selectPlanEngineers(cwd, teamId, preferredLead, preferredChallenger) {
357
+ const team = await this.getOrCreateTeam(cwd, teamId);
358
+ const available = this.getAvailableEngineers(team);
359
+ if (available.length < 2) {
360
+ throw new Error(`Not enough available engineers for dual planning. Need 2, found ${available.length}.`);
361
+ }
362
+ const lead = preferredLead ?? available[0];
363
+ const foundChallenger = preferredChallenger ?? available.find((e) => e !== lead);
364
+ const challenger = foundChallenger ?? available[1];
365
+ if (lead === challenger) {
366
+ throw new Error('Cannot use the same engineer for both lead and challenger.');
367
+ }
368
+ return { lead, challenger };
369
+ }
299
370
  buildSessionSystemPrompt(engineer, mode) {
300
371
  return [
301
372
  this.engineerSessionPrompt,
@@ -365,9 +436,6 @@ function buildPlanDraftRequest(perspective, request) {
365
436
  `User request: ${request}`,
366
437
  ].join('\n');
367
438
  }
368
- function buildSynthesisSystemPrompt(architectSystemPrompt) {
369
- return architectSystemPrompt;
370
- }
371
439
  function buildSynthesisPrompt(request, drafts) {
372
440
  return [
373
441
  `User request: ${request}`,
@@ -403,3 +471,26 @@ function normalizeOptionalSection(value) {
403
471
  function escapeRegExp(value) {
404
472
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
405
473
  }
474
+ export function getFailureGuidanceText(failureKind) {
475
+ switch (failureKind) {
476
+ case 'contextExhausted':
477
+ return 'Context exhausted after using all available tokens. The engineer was reset and the assignment retried once. If it still fails, the task may be too large; consider breaking it into smaller steps.';
478
+ case 'engineerBusy':
479
+ return 'This engineer is currently working on another assignment. Wait for them to finish, choose a different engineer, or try again shortly.';
480
+ case 'toolDenied':
481
+ return 'A tool permission was denied during the assignment. Check the approval policy and tool permissions, then retry.';
482
+ case 'aborted':
483
+ return 'The assignment was cancelled by the user or an abort signal was triggered. Review the request and try again.';
484
+ case 'sdkError':
485
+ return 'An SDK error occurred during the assignment. Check logs for details, ensure the Claude session is healthy, and retry.';
486
+ default:
487
+ return 'An unknown error occurred during the assignment. Check logs and retry.';
488
+ }
489
+ }
490
+ export function createActionableError(failure, originalError) {
491
+ const guidance = getFailureGuidanceText(failure.failureKind);
492
+ const errorMessage = `[${failure.failureKind}] ${failure.message}\n\n` + `Next steps: ${guidance}`;
493
+ const error = new Error(errorMessage);
494
+ Object.assign(error, { cause: originalError });
495
+ return error;
496
+ }
@@ -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,
@@ -103,7 +98,7 @@ export function buildCtoAgentConfig(prompts) {
103
98
  }
104
99
  export function buildEngineerAgentConfig(prompts, engineer) {
105
100
  return {
106
- description: `${engineer} is a persistent engineer who works through one Claude Code session and remembers prior turns.`,
101
+ description: `${engineer} is a persistent engineer who works through one Claude Code session and remembers prior turns. Receives structured assignments (goal, mode, context, acceptance criteria, relevant paths, constraints, verification).`,
107
102
  mode: 'subagent',
108
103
  hidden: false,
109
104
  color: '#D97757',
@@ -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. Automatically selects two non-overlapping available engineers if engineer names are 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;
@@ -1,8 +1,8 @@
1
1
  import { tool } from '@opencode-ai/plugin';
2
2
  import { managerPromptRegistry } from '../prompts/registry.js';
3
3
  import { isEngineerName } from '../team/roster.js';
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';
4
+ import { TeamOrchestrator, createActionableError, getFailureGuidanceText, } from '../manager/team-orchestrator.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
  }
@@ -105,34 +105,36 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
105
105
  },
106
106
  }),
107
107
  plan_with_team: tool({
108
- description: 'Run dual-engineer plan synthesis. Two engineers explore in parallel (lead + challenger), then their plans are synthesized into one stronger plan.',
108
+ description: 'Run dual-engineer plan synthesis. Two engineers explore in parallel (lead + challenger), then their plans are synthesized into one stronger plan. Automatically selects distinct available engineers if names are not provided.',
109
109
  args: {
110
110
  request: tool.schema.string().min(1),
111
- leadEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
112
- challengerEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
111
+ leadEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']).optional(),
112
+ challengerEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']).optional(),
113
113
  model: tool.schema.enum(MODEL_ENUM).optional(),
114
114
  },
115
115
  async execute(args, context) {
116
116
  const teamId = getActiveTeamSession(context.worktree) ?? context.sessionID;
117
+ // Pre-determine engineers for event labeling (using orchestrator selection logic)
118
+ const { lead, challenger } = await services.orchestrator.selectPlanEngineers(context.worktree, teamId, args.leadEngineer, args.challengerEngineer);
117
119
  annotateToolRun(context, 'Running dual-engineer plan synthesis', {
118
120
  teamId,
119
- lead: args.leadEngineer,
120
- challenger: args.challengerEngineer,
121
+ lead,
122
+ challenger,
121
123
  });
122
124
  const result = await services.orchestrator.planWithTeam({
123
125
  teamId,
124
126
  cwd: context.worktree,
125
127
  request: args.request,
126
- leadEngineer: args.leadEngineer,
127
- challengerEngineer: args.challengerEngineer,
128
+ leadEngineer: lead,
129
+ challengerEngineer: challenger,
128
130
  model: args.model,
129
131
  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),
132
+ onLeadEvent: (event) => reportClaudeEvent(context, lead, event),
133
+ onChallengerEvent: (event) => reportClaudeEvent(context, challenger, event),
134
+ onSynthesisEvent: (event) => reportPlanSynthesisEvent(context, event),
133
135
  });
134
136
  context.metadata({
135
- title: '✅ Architect finished',
137
+ title: '✅ Plan synthesis finished',
136
138
  metadata: {
137
139
  teamId: result.teamId,
138
140
  lead: result.leadEngineer,
@@ -386,6 +388,7 @@ async function runEngineerAssignment(input, context) {
386
388
  failure.teamId = input.teamId;
387
389
  failure.engineer = input.engineer;
388
390
  failure.mode = input.mode;
391
+ const guidance = getFailureGuidanceText(failure.failureKind);
389
392
  context.metadata({
390
393
  title: `❌ ${input.engineer} failed (${failure.failureKind})`,
391
394
  metadata: {
@@ -393,9 +396,10 @@ async function runEngineerAssignment(input, context) {
393
396
  engineer: failure.engineer,
394
397
  failureKind: failure.failureKind,
395
398
  message: failure.message.slice(0, 200),
399
+ guidance,
396
400
  },
397
401
  });
398
- throw error;
402
+ throw createActionableError(failure, error);
399
403
  }
400
404
  await services.orchestrator.recordWrapperExchange(context.worktree, input.teamId, input.engineer, context.sessionID, input.mode, input.message, result.finalText);
401
405
  context.metadata({
@@ -411,16 +415,25 @@ async function runEngineerAssignment(input, context) {
411
415
  });
412
416
  return result;
413
417
  }
414
- function engineerFromAgent(agentId) {
415
- const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === agentId);
418
+ /**
419
+ * Normalize an agent ID to its lowercase canonical form.
420
+ * Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
421
+ */
422
+ export function normalizeAgentId(agentId) {
423
+ return agentId.toLowerCase();
424
+ }
425
+ export function engineerFromAgent(agentId) {
426
+ const normalized = normalizeAgentId(agentId);
427
+ const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === normalized);
416
428
  const engineer = engineerEntry?.[0];
417
429
  if (!engineer || !isEngineerName(engineer)) {
418
430
  throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
419
431
  }
420
432
  return engineer;
421
433
  }
422
- function isEngineerAgent(agentId) {
423
- return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
434
+ export function isEngineerAgent(agentId) {
435
+ const normalized = normalizeAgentId(agentId);
436
+ return Object.values(ENGINEER_AGENT_IDS).some((id) => id === normalized);
424
437
  }
425
438
  /**
426
439
  * Resolves the team ID for an engineer session.
@@ -496,6 +509,16 @@ function reportClaudeEvent(context, engineer, event) {
496
509
  });
497
510
  return;
498
511
  }
512
+ if (event.type === 'status') {
513
+ context.metadata({
514
+ title: `ℹ️ ${engineer}: ${event.text}`,
515
+ metadata: {
516
+ engineer,
517
+ status: event.text,
518
+ },
519
+ });
520
+ return;
521
+ }
499
522
  if (event.type === 'init') {
500
523
  context.metadata({
501
524
  title: `⚡ ${engineer} session ready`,
@@ -562,10 +585,10 @@ function reportClaudeEvent(context, engineer, event) {
562
585
  });
563
586
  }
564
587
  }
565
- function reportArchitectEvent(context, event) {
588
+ function reportPlanSynthesisEvent(context, event) {
566
589
  if (event.type === 'error') {
567
590
  context.metadata({
568
- title: `❌ Architect hit an error`,
591
+ title: `❌ Plan synthesis hit an error`,
569
592
  metadata: {
570
593
  sessionId: event.sessionId,
571
594
  error: event.text.slice(0, 200),
@@ -575,7 +598,7 @@ function reportArchitectEvent(context, event) {
575
598
  }
576
599
  if (event.type === 'init') {
577
600
  context.metadata({
578
- title: `⚡ Architect session ready`,
601
+ title: `⚡ Plan synthesis ready`,
579
602
  metadata: {
580
603
  sessionId: event.sessionId,
581
604
  },
@@ -608,10 +631,10 @@ function reportArchitectEvent(context, event) {
608
631
  const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
609
632
  context.metadata({
610
633
  title: toolDescription
611
- ? `⚡ Architect → ${toolDescription}`
634
+ ? `⚡ Plan synthesis → ${toolDescription}`
612
635
  : toolName
613
- ? `⚡ Architect → ${toolName}`
614
- : `⚡ Architect is using Claude Code tools`,
636
+ ? `⚡ Plan synthesis → ${toolName}`
637
+ : `⚡ Plan synthesis is running`,
615
638
  metadata: {
616
639
  sessionId: event.sessionId,
617
640
  ...(toolName !== undefined && { toolName }),
@@ -625,7 +648,7 @@ function reportArchitectEvent(context, event) {
625
648
  const isThinking = event.text.startsWith('<thinking>');
626
649
  const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
627
650
  context.metadata({
628
- title: `⚡ Architect ${stateLabel}`,
651
+ title: `⚡ Plan synthesis ${stateLabel}`,
629
652
  metadata: {
630
653
  sessionId: event.sessionId,
631
654
  preview: event.text.slice(0, 160),
@@ -635,8 +658,9 @@ function reportArchitectEvent(context, event) {
635
658
  }
636
659
  }
637
660
  function annotateToolRun(context, title, metadata) {
661
+ const agentLabel = context.agent === AGENT_CTO ? 'CTO' : undefined;
638
662
  context.metadata({
639
- title,
663
+ title: agentLabel ? `${agentLabel} → ${title}` : title,
640
664
  metadata,
641
665
  });
642
666
  }
@@ -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,