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

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.
Files changed (53) hide show
  1. package/dist/manager/team-orchestrator.d.ts +13 -5
  2. package/dist/manager/team-orchestrator.js +134 -15
  3. package/dist/plugin/agent-hierarchy.d.ts +1 -54
  4. package/dist/plugin/agent-hierarchy.js +2 -123
  5. package/dist/plugin/agents/browser-qa.d.ts +14 -0
  6. package/dist/plugin/agents/browser-qa.js +27 -0
  7. package/dist/plugin/agents/common.d.ts +37 -0
  8. package/dist/plugin/agents/common.js +59 -0
  9. package/dist/plugin/agents/cto.d.ts +9 -0
  10. package/dist/plugin/agents/cto.js +39 -0
  11. package/dist/plugin/agents/engineers.d.ts +9 -0
  12. package/dist/plugin/agents/engineers.js +11 -0
  13. package/dist/plugin/agents/index.d.ts +6 -0
  14. package/dist/plugin/agents/index.js +5 -0
  15. package/dist/plugin/agents/team-planner.d.ts +10 -0
  16. package/dist/plugin/agents/team-planner.js +23 -0
  17. package/dist/plugin/claude-manager.plugin.js +68 -32
  18. package/dist/plugin/service-factory.d.ts +4 -3
  19. package/dist/plugin/service-factory.js +4 -1
  20. package/dist/prompts/registry.js +142 -57
  21. package/dist/src/manager/team-orchestrator.d.ts +13 -5
  22. package/dist/src/manager/team-orchestrator.js +134 -15
  23. package/dist/src/plugin/agent-hierarchy.d.ts +1 -54
  24. package/dist/src/plugin/agent-hierarchy.js +2 -123
  25. package/dist/src/plugin/agents/browser-qa.d.ts +14 -0
  26. package/dist/src/plugin/agents/browser-qa.js +27 -0
  27. package/dist/src/plugin/agents/common.d.ts +37 -0
  28. package/dist/src/plugin/agents/common.js +59 -0
  29. package/dist/src/plugin/agents/cto.d.ts +9 -0
  30. package/dist/src/plugin/agents/cto.js +39 -0
  31. package/dist/src/plugin/agents/engineers.d.ts +9 -0
  32. package/dist/src/plugin/agents/engineers.js +11 -0
  33. package/dist/src/plugin/agents/index.d.ts +6 -0
  34. package/dist/src/plugin/agents/index.js +5 -0
  35. package/dist/src/plugin/agents/team-planner.d.ts +10 -0
  36. package/dist/src/plugin/agents/team-planner.js +23 -0
  37. package/dist/src/plugin/claude-manager.plugin.js +68 -32
  38. package/dist/src/plugin/service-factory.d.ts +4 -3
  39. package/dist/src/plugin/service-factory.js +4 -1
  40. package/dist/src/prompts/registry.js +142 -57
  41. package/dist/src/team/roster.d.ts +3 -2
  42. package/dist/src/team/roster.js +2 -1
  43. package/dist/src/types/contracts.d.ts +26 -2
  44. package/dist/src/types/contracts.js +2 -1
  45. package/dist/team/roster.d.ts +3 -2
  46. package/dist/team/roster.js +2 -1
  47. package/dist/test/claude-manager.plugin.test.js +70 -0
  48. package/dist/test/prompt-registry.test.js +31 -6
  49. package/dist/test/report-claude-event.test.js +57 -3
  50. package/dist/test/team-orchestrator.test.js +155 -5
  51. package/dist/types/contracts.d.ts +26 -2
  52. package/dist/types/contracts.js +2 -1
  53. package/package.json +1 -1
@@ -2,7 +2,7 @@ import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapt
2
2
  import type { ClaudeSessionService } from '../claude/claude-session.service.js';
3
3
  import type { TeamStateStore } from '../state/team-state-store.js';
4
4
  import type { TranscriptStore } from '../state/transcript-store.js';
5
- import type { EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord } from '../types/contracts.js';
5
+ import type { EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord, WorkerCapabilities } from '../types/contracts.js';
6
6
  interface DispatchEngineerInput {
7
7
  teamId: string;
8
8
  cwd: string;
@@ -19,7 +19,8 @@ export declare class TeamOrchestrator {
19
19
  private readonly transcriptStore;
20
20
  private readonly engineerSessionPrompt;
21
21
  private readonly planSynthesisPrompt;
22
- constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, planSynthesisPrompt: string);
22
+ private readonly workerCapabilities;
23
+ constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, planSynthesisPrompt: string, workerCapabilities: Partial<Record<EngineerName, WorkerCapabilities>>);
23
24
  getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
24
25
  listTeams(cwd: string): Promise<TeamRecord[]>;
25
26
  recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
@@ -33,7 +34,7 @@ export declare class TeamOrchestrator {
33
34
  clearSession?: boolean;
34
35
  clearHistory?: boolean;
35
36
  }): Promise<void>;
36
- dispatchEngineer(input: DispatchEngineerInput): Promise<EngineerTaskResult>;
37
+ dispatchEngineer(input: DispatchEngineerInput, retryCount?: number): Promise<EngineerTaskResult>;
37
38
  static classifyError(error: unknown): EngineerFailureResult & {
38
39
  cause: unknown;
39
40
  };
@@ -41,8 +42,8 @@ export declare class TeamOrchestrator {
41
42
  teamId: string;
42
43
  cwd: string;
43
44
  request: string;
44
- leadEngineer: EngineerName;
45
- challengerEngineer: EngineerName;
45
+ leadEngineer?: EngineerName;
46
+ challengerEngineer?: EngineerName;
46
47
  model?: string;
47
48
  abortSignal?: AbortSignal;
48
49
  onLeadEvent?: ClaudeSessionEventHandler;
@@ -53,7 +54,14 @@ export declare class TeamOrchestrator {
53
54
  private reserveEngineer;
54
55
  private getEngineerState;
55
56
  private normalizeTeamRecord;
57
+ getAvailableEngineers(team: TeamRecord): EngineerName[];
58
+ selectPlanEngineers(cwd: string, teamId: string, preferredLead?: EngineerName, preferredChallenger?: EngineerName): Promise<{
59
+ lead: EngineerName;
60
+ challenger: EngineerName;
61
+ }>;
56
62
  private buildSessionSystemPrompt;
57
63
  private buildEngineerPrompt;
58
64
  }
65
+ export declare function getFailureGuidanceText(failureKind: string): string;
66
+ export declare function createActionableError(failure: EngineerFailureResult, originalError: unknown): Error;
59
67
  export {};
@@ -7,12 +7,14 @@ export class TeamOrchestrator {
7
7
  transcriptStore;
8
8
  engineerSessionPrompt;
9
9
  planSynthesisPrompt;
10
- constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, planSynthesisPrompt) {
10
+ workerCapabilities;
11
+ constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, planSynthesisPrompt, workerCapabilities) {
11
12
  this.sessions = sessions;
12
13
  this.teamStore = teamStore;
13
14
  this.transcriptStore = transcriptStore;
14
15
  this.engineerSessionPrompt = engineerSessionPrompt;
15
16
  this.planSynthesisPrompt = planSynthesisPrompt;
17
+ this.workerCapabilities = workerCapabilities;
16
18
  }
17
19
  async getOrCreateTeam(cwd, teamId) {
18
20
  const existing = await this.teamStore.getTeam(cwd, teamId);
@@ -88,7 +90,14 @@ export class TeamOrchestrator {
88
90
  context: options?.clearSession ? createEmptyEngineerRecord(engineer).context : entry.context,
89
91
  }));
90
92
  }
91
- async dispatchEngineer(input) {
93
+ async dispatchEngineer(input, retryCount = 0) {
94
+ const workerCaps = this.workerCapabilities[input.engineer];
95
+ // Reject write-restricted workers in implement mode
96
+ if (workerCaps?.restrictWriteTools && input.mode === 'implement') {
97
+ throw new Error(`${input.engineer} is a browser QA specialist and does not support implement mode. ` +
98
+ 'It can only verify and explore (test browser interactions via Playwright). ' +
99
+ 'For code changes, use a general engineer (Tom, John, Maya, Sara, Alex).');
100
+ }
92
101
  const team = await this.getOrCreateTeam(input.cwd, input.teamId);
93
102
  const engineerState = this.getEngineerState(team, input.engineer);
94
103
  await this.reserveEngineer(input.cwd, input.teamId, input.engineer);
@@ -107,15 +116,19 @@ export class TeamOrchestrator {
107
116
  const result = await this.sessions.runTask({
108
117
  cwd: input.cwd,
109
118
  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)}`,
119
+ ? this.buildEngineerPrompt(input.mode, input.message, input.engineer)
120
+ : `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message, input.engineer)}`,
112
121
  resumeSessionId: engineerState.claudeSessionId ?? undefined,
113
122
  persistSession: true,
114
123
  includePartialMessages: true,
115
124
  permissionMode: 'acceptEdits',
116
- restrictWriteTools: input.mode === 'explore',
125
+ restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
117
126
  model: input.model,
118
- effort: input.mode === 'implement' ? 'high' : 'medium',
127
+ effort: (workerCaps?.restrictWriteTools ?? false)
128
+ ? 'medium'
129
+ : input.mode === 'implement'
130
+ ? 'high'
131
+ : 'medium',
119
132
  settingSources: ['user', 'project', 'local'],
120
133
  abortSignal: input.abortSignal,
121
134
  }, input.onEvent);
@@ -161,6 +174,39 @@ export class TeamOrchestrator {
161
174
  busy: false,
162
175
  busySince: null,
163
176
  }));
177
+ // Handle context exhaustion with automatic retry (max 1 retry)
178
+ const classified = TeamOrchestrator.classifyError(error);
179
+ if (classified.failureKind === 'contextExhausted' && retryCount === 0) {
180
+ // Reset the engineer's session and retry once with fresh session
181
+ await this.resetEngineer(input.cwd, input.teamId, input.engineer, {
182
+ clearSession: true,
183
+ clearHistory: false,
184
+ });
185
+ // Emit status event before retry
186
+ await input.onEvent?.({
187
+ type: 'status',
188
+ text: 'Context exhausted; resetting session and retrying once with a fresh session.',
189
+ });
190
+ try {
191
+ // Retry dispatch with fresh session (retryCount=1 prevents infinite loop)
192
+ // Use the exact same assignment message without modification
193
+ return await this.dispatchEngineer(input, 1);
194
+ }
195
+ catch (retryError) {
196
+ // If retry also fails with a different error, preserve retry failure info
197
+ const retryClassified = TeamOrchestrator.classifyError(retryError);
198
+ if (retryClassified.failureKind !== classified.failureKind) {
199
+ // Create an error that shows both failures
200
+ const combinedMessage = `Initial: ${classified.failureKind} (${classified.message})\n` +
201
+ `After retry: ${retryClassified.failureKind} (${retryClassified.message})`;
202
+ const combinedError = new Error(combinedMessage);
203
+ Object.assign(combinedError, { cause: retryError });
204
+ throw combinedError;
205
+ }
206
+ // Same error type on retry, throw the retry error (more recent state)
207
+ throw retryError;
208
+ }
209
+ }
164
210
  throw error;
165
211
  }
166
212
  }
@@ -192,14 +238,13 @@ export class TeamOrchestrator {
192
238
  };
193
239
  }
194
240
  async planWithTeam(input) {
195
- if (input.leadEngineer === input.challengerEngineer) {
196
- throw new Error('Choose two different engineers for plan synthesis.');
197
- }
241
+ // Auto-select engineers if not provided
242
+ const { lead: leadEngineer, challenger: challengerEngineer } = await this.selectPlanEngineers(input.cwd, input.teamId, input.leadEngineer, input.challengerEngineer);
198
243
  const [leadDraft, challengerDraft] = await Promise.all([
199
244
  this.dispatchEngineer({
200
245
  teamId: input.teamId,
201
246
  cwd: input.cwd,
202
- engineer: input.leadEngineer,
247
+ engineer: leadEngineer,
203
248
  mode: 'explore',
204
249
  message: buildPlanDraftRequest('lead', input.request),
205
250
  model: input.model,
@@ -209,7 +254,7 @@ export class TeamOrchestrator {
209
254
  this.dispatchEngineer({
210
255
  teamId: input.teamId,
211
256
  cwd: input.cwd,
212
- engineer: input.challengerEngineer,
257
+ engineer: challengerEngineer,
213
258
  mode: 'explore',
214
259
  message: buildPlanDraftRequest('challenger', input.request),
215
260
  model: input.model,
@@ -237,8 +282,8 @@ export class TeamOrchestrator {
237
282
  return {
238
283
  teamId: input.teamId,
239
284
  request: input.request,
240
- leadEngineer: input.leadEngineer,
241
- challengerEngineer: input.challengerEngineer,
285
+ leadEngineer,
286
+ challengerEngineer,
242
287
  drafts,
243
288
  synthesis: parsedSynthesis.synthesis,
244
289
  recommendedQuestion: parsedSynthesis.recommendedQuestion,
@@ -289,12 +334,60 @@ export class TeamOrchestrator {
289
334
  }
290
335
  normalizeTeamRecord(team) {
291
336
  const engineerMap = new Map(team.engineers.map((engineer) => [engineer.name, engineer]));
337
+ const emptyTeam = createEmptyTeamRecord(team.id, team.cwd);
292
338
  return {
293
339
  ...team,
294
- engineers: createEmptyTeamRecord(team.id, team.cwd).engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
340
+ engineers: emptyTeam.engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
295
341
  };
296
342
  }
343
+ getAvailableEngineers(team) {
344
+ const now = Date.now();
345
+ return team.engineers
346
+ .filter((engineer) => {
347
+ if (!engineer.busy)
348
+ return true;
349
+ // If an engineer has been marked busy but the lease expired, they're available
350
+ if (engineer.busySince) {
351
+ const leaseExpired = now - new Date(engineer.busySince).getTime() > BUSY_LEASE_MS;
352
+ return leaseExpired;
353
+ }
354
+ return false;
355
+ })
356
+ .sort((a, b) => {
357
+ // Prefer engineers with lower context pressure and less-recently-used
358
+ const aContext = a.context.estimatedContextPercent ?? 0;
359
+ const bContext = b.context.estimatedContextPercent ?? 0;
360
+ if (aContext !== bContext) {
361
+ return aContext - bContext; // Lower context first
362
+ }
363
+ // If context is equal, prefer less-recently-used
364
+ const aTime = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0;
365
+ const bTime = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0;
366
+ return aTime - bTime; // Earlier usage time first
367
+ })
368
+ .map((engineer) => engineer.name);
369
+ }
370
+ async selectPlanEngineers(cwd, teamId, preferredLead, preferredChallenger) {
371
+ const team = await this.getOrCreateTeam(cwd, teamId);
372
+ const allAvailable = this.getAvailableEngineers(team);
373
+ // Filter to only planner-eligible engineers (specialists with plannerEligible: false are excluded)
374
+ const available = allAvailable.filter((e) => this.workerCapabilities[e]?.plannerEligible !== false);
375
+ if (available.length < 2) {
376
+ throw new Error(`Not enough available engineers for dual planning. Need 2 general engineers (specialists excluded), found ${available.length}.`);
377
+ }
378
+ const lead = preferredLead ?? available[0];
379
+ const foundChallenger = preferredChallenger ?? available.find((e) => e !== lead);
380
+ const challenger = foundChallenger ?? available[1];
381
+ if (lead === challenger) {
382
+ throw new Error('Cannot use the same engineer for both lead and challenger.');
383
+ }
384
+ return { lead, challenger };
385
+ }
297
386
  buildSessionSystemPrompt(engineer, mode) {
387
+ const specialistPrompt = this.workerCapabilities[engineer]?.sessionPrompt;
388
+ if (specialistPrompt) {
389
+ return specialistPrompt;
390
+ }
298
391
  return [
299
392
  this.engineerSessionPrompt,
300
393
  '',
@@ -304,7 +397,10 @@ export class TeamOrchestrator {
304
397
  .join('\n')
305
398
  .trim();
306
399
  }
307
- buildEngineerPrompt(mode, message) {
400
+ buildEngineerPrompt(mode, message, engineer) {
401
+ if (this.workerCapabilities[engineer]?.skipModeInstructions) {
402
+ return message;
403
+ }
308
404
  return `${buildModeInstruction(mode)}\n\n${message}`;
309
405
  }
310
406
  }
@@ -398,3 +494,26 @@ function normalizeOptionalSection(value) {
398
494
  function escapeRegExp(value) {
399
495
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
400
496
  }
497
+ export function getFailureGuidanceText(failureKind) {
498
+ switch (failureKind) {
499
+ case 'contextExhausted':
500
+ 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.';
501
+ case 'engineerBusy':
502
+ return 'This engineer is currently working on another assignment. Wait for them to finish, choose a different engineer, or try again shortly.';
503
+ case 'toolDenied':
504
+ return 'A tool permission was denied during the assignment. Check the approval policy and tool permissions, then retry.';
505
+ case 'aborted':
506
+ return 'The assignment was cancelled by the user or an abort signal was triggered. Review the request and try again.';
507
+ case 'sdkError':
508
+ return 'An SDK error occurred during the assignment. Check logs for details, ensure the Claude session is healthy, and retry.';
509
+ default:
510
+ return 'An unknown error occurred during the assignment. Check logs and retry.';
511
+ }
512
+ }
513
+ export function createActionableError(failure, originalError) {
514
+ const guidance = getFailureGuidanceText(failure.failureKind);
515
+ const errorMessage = `[${failure.failureKind}] ${failure.message}\n\n` + `Next steps: ${guidance}`;
516
+ const error = new Error(errorMessage);
517
+ Object.assign(error, { cause: originalError });
518
+ return error;
519
+ }
@@ -1,54 +1 @@
1
- import type { EngineerName, ManagerPromptRegistry } from '../types/contracts.js';
2
- export declare const AGENT_CTO = "cto";
3
- export declare const AGENT_TEAM_PLANNER = "team-planner";
4
- export declare const ENGINEER_AGENT_IDS: {
5
- readonly Tom: "tom";
6
- readonly John: "john";
7
- readonly Maya: "maya";
8
- readonly Sara: "sara";
9
- readonly Alex: "alex";
10
- };
11
- export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
12
- type ToolPermission = 'allow' | 'ask' | 'deny';
13
- type AgentPermission = {
14
- '*'?: ToolPermission;
15
- read?: ToolPermission;
16
- grep?: ToolPermission;
17
- glob?: ToolPermission;
18
- list?: ToolPermission;
19
- codesearch?: ToolPermission;
20
- webfetch?: ToolPermission;
21
- websearch?: ToolPermission;
22
- lsp?: ToolPermission;
23
- todowrite?: ToolPermission;
24
- todoread?: ToolPermission;
25
- question?: ToolPermission;
26
- task?: ToolPermission | Record<string, ToolPermission>;
27
- bash?: ToolPermission | Record<string, ToolPermission>;
28
- [tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
29
- };
30
- export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
31
- description: string;
32
- mode: "primary";
33
- color: string;
34
- permission: AgentPermission;
35
- prompt: string;
36
- };
37
- export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry, engineer: EngineerName): {
38
- description: string;
39
- mode: "subagent";
40
- hidden: boolean;
41
- color: string;
42
- permission: AgentPermission;
43
- prompt: string;
44
- };
45
- export declare function buildTeamPlannerAgentConfig(prompts: ManagerPromptRegistry): {
46
- description: string;
47
- mode: "subagent";
48
- hidden: boolean;
49
- color: string;
50
- permission: AgentPermission;
51
- prompt: string;
52
- };
53
- export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
54
- export {};
1
+ export * from './agents/index.js';
@@ -1,123 +1,2 @@
1
- import { TEAM_ENGINEERS } from '../team/roster.js';
2
- export const AGENT_CTO = 'cto';
3
- export const AGENT_TEAM_PLANNER = 'team-planner';
4
- export const ENGINEER_AGENT_IDS = {
5
- Tom: 'tom',
6
- John: 'john',
7
- Maya: 'maya',
8
- Sara: 'sara',
9
- Alex: 'alex',
10
- };
11
- export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
12
- const CTO_ONLY_TOOL_IDS = [
13
- 'team_status',
14
- 'reset_engineer',
15
- 'list_transcripts',
16
- 'list_history',
17
- 'git_diff',
18
- 'git_commit',
19
- 'git_reset',
20
- 'git_status',
21
- 'git_log',
22
- 'approval_policy',
23
- 'approval_decisions',
24
- 'approval_update',
25
- ];
26
- const ENGINEER_TOOL_IDS = ['claude'];
27
- const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
28
- const CTO_READONLY_TOOLS = {
29
- read: 'allow',
30
- grep: 'allow',
31
- glob: 'allow',
32
- list: 'allow',
33
- codesearch: 'allow',
34
- webfetch: 'allow',
35
- websearch: 'allow',
36
- lsp: 'allow',
37
- todowrite: 'allow',
38
- todoread: 'allow',
39
- question: 'allow',
40
- };
41
- function buildTeamPlannerPermissions() {
42
- const denied = {};
43
- for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
44
- denied[toolId] = 'deny';
45
- }
46
- return {
47
- '*': 'deny',
48
- plan_with_team: 'allow',
49
- question: 'allow',
50
- ...denied,
51
- };
52
- }
53
- function buildCtoPermissions() {
54
- const denied = {};
55
- for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
56
- denied[toolId] = 'deny';
57
- }
58
- const allowed = {};
59
- for (const toolId of CTO_ONLY_TOOL_IDS) {
60
- allowed[toolId] = 'allow';
61
- }
62
- const taskPermissions = { '*': 'deny' };
63
- for (const engineer of ENGINEER_AGENT_NAMES) {
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.
69
- }
70
- taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
71
- return {
72
- '*': 'deny',
73
- ...CTO_READONLY_TOOLS,
74
- ...denied,
75
- ...allowed,
76
- task: taskPermissions,
77
- };
78
- }
79
- function buildEngineerPermissions() {
80
- const denied = {};
81
- for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
82
- denied[toolId] = 'deny';
83
- }
84
- return {
85
- '*': 'deny',
86
- ...denied,
87
- claude: 'allow',
88
- };
89
- }
90
- export function buildCtoAgentConfig(prompts) {
91
- return {
92
- description: 'Principal engineer who orchestrates AI-powered engineers. Decomposes work, asks clarifying questions, delegates precisely, reviews diffs, and owns the outcome.',
93
- mode: 'primary',
94
- color: '#D97757',
95
- permission: buildCtoPermissions(),
96
- prompt: prompts.ctoSystemPrompt,
97
- };
98
- }
99
- export function buildEngineerAgentConfig(prompts, engineer) {
100
- return {
101
- description: `${engineer} is a persistent engineer who works through one Claude Code session and remembers prior turns.`,
102
- mode: 'subagent',
103
- hidden: false,
104
- color: '#D97757',
105
- permission: buildEngineerPermissions(),
106
- prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
107
- };
108
- }
109
- export function buildTeamPlannerAgentConfig(prompts) {
110
- return {
111
- description: 'Runs dual-engineer planning by calling plan_with_team. Asks for engineer names if not provided.',
112
- mode: 'subagent',
113
- hidden: false,
114
- color: '#D97757',
115
- permission: buildTeamPlannerPermissions(),
116
- prompt: prompts.teamPlannerPrompt,
117
- };
118
- }
119
- export function denyRestrictedToolsGlobally(permissions) {
120
- for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
121
- permissions[toolId] ??= 'deny';
122
- }
123
- }
1
+ // Re-export barrel all symbols now live in src/plugin/agents/.
2
+ export * from './agents/index.js';
@@ -0,0 +1,14 @@
1
+ import type { EngineerName, ManagerPromptRegistry, WorkerCapabilities } from '../../types/contracts.js';
2
+ /**
3
+ * Build the worker capabilities map for all specialist workers.
4
+ * Called once at service-factory construction time to avoid re-building on each tool call.
5
+ */
6
+ export declare function buildWorkerCapabilities(prompts: ManagerPromptRegistry): Partial<Record<EngineerName, WorkerCapabilities>>;
7
+ export declare function buildBrowserQaAgentConfig(prompts: ManagerPromptRegistry): {
8
+ description: string;
9
+ mode: "subagent";
10
+ hidden: boolean;
11
+ color: string;
12
+ permission: import("./common.js").AgentPermission;
13
+ prompt: string;
14
+ };
@@ -0,0 +1,27 @@
1
+ import { buildEngineerPermissions } from './common.js';
2
+ /**
3
+ * Build the worker capabilities map for all specialist workers.
4
+ * Called once at service-factory construction time to avoid re-building on each tool call.
5
+ */
6
+ export function buildWorkerCapabilities(prompts) {
7
+ return {
8
+ BrowserQA: {
9
+ sessionPrompt: prompts.browserQaSessionPrompt,
10
+ restrictWriteTools: true,
11
+ skipModeInstructions: true,
12
+ plannerEligible: false,
13
+ isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
14
+ runtimeUnavailableTitle: '❌ Playwright unavailable',
15
+ },
16
+ };
17
+ }
18
+ export function buildBrowserQaAgentConfig(prompts) {
19
+ return {
20
+ description: 'Browser QA specialist who uses the Playwright skill/command to test web features and user flows. Maintains a persistent Claude Code session that remembers prior verification runs.',
21
+ mode: 'subagent',
22
+ hidden: false,
23
+ color: '#D97757',
24
+ permission: buildEngineerPermissions(), // Same permissions as engineers (claude tool only)
25
+ prompt: prompts.browserQaAgentPrompt,
26
+ };
27
+ }
@@ -0,0 +1,37 @@
1
+ export declare const AGENT_CTO = "cto";
2
+ export declare const AGENT_TEAM_PLANNER = "team-planner";
3
+ export declare const AGENT_BROWSER_QA = "browser-qa";
4
+ export declare const ENGINEER_AGENT_IDS: {
5
+ readonly Tom: "tom";
6
+ readonly John: "john";
7
+ readonly Maya: "maya";
8
+ readonly Sara: "sara";
9
+ readonly Alex: "alex";
10
+ readonly BrowserQA: "browser-qa";
11
+ };
12
+ /** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
13
+ export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
14
+ export declare const CTO_ONLY_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
15
+ export declare const ENGINEER_TOOL_IDS: readonly ["claude"];
16
+ export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
17
+ export type ToolPermission = 'allow' | 'ask' | 'deny';
18
+ export type AgentPermission = {
19
+ '*'?: ToolPermission;
20
+ read?: ToolPermission;
21
+ grep?: ToolPermission;
22
+ glob?: ToolPermission;
23
+ list?: ToolPermission;
24
+ codesearch?: ToolPermission;
25
+ webfetch?: ToolPermission;
26
+ websearch?: ToolPermission;
27
+ lsp?: ToolPermission;
28
+ todowrite?: ToolPermission;
29
+ todoread?: ToolPermission;
30
+ question?: ToolPermission;
31
+ task?: ToolPermission | Record<string, ToolPermission>;
32
+ bash?: ToolPermission | Record<string, ToolPermission>;
33
+ [tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
34
+ };
35
+ export declare const CTO_READONLY_TOOLS: Record<string, ToolPermission>;
36
+ export declare function buildEngineerPermissions(): AgentPermission;
37
+ export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
@@ -0,0 +1,59 @@
1
+ import { PLANNER_ELIGIBLE_ENGINEERS } from '../../team/roster.js';
2
+ export const AGENT_CTO = 'cto';
3
+ export const AGENT_TEAM_PLANNER = 'team-planner';
4
+ export const AGENT_BROWSER_QA = 'browser-qa';
5
+ export const ENGINEER_AGENT_IDS = {
6
+ Tom: 'tom',
7
+ John: 'john',
8
+ Maya: 'maya',
9
+ Sara: 'sara',
10
+ Alex: 'alex',
11
+ BrowserQA: 'browser-qa',
12
+ };
13
+ /** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
14
+ export const ENGINEER_AGENT_NAMES = PLANNER_ELIGIBLE_ENGINEERS;
15
+ export const CTO_ONLY_TOOL_IDS = [
16
+ 'team_status',
17
+ 'reset_engineer',
18
+ 'list_transcripts',
19
+ 'list_history',
20
+ 'git_diff',
21
+ 'git_commit',
22
+ 'git_reset',
23
+ 'git_status',
24
+ 'git_log',
25
+ 'approval_policy',
26
+ 'approval_decisions',
27
+ 'approval_update',
28
+ ];
29
+ export const ENGINEER_TOOL_IDS = ['claude'];
30
+ export const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
31
+ export const CTO_READONLY_TOOLS = {
32
+ read: 'allow',
33
+ grep: 'allow',
34
+ glob: 'allow',
35
+ list: 'allow',
36
+ codesearch: 'allow',
37
+ webfetch: 'allow',
38
+ websearch: 'allow',
39
+ lsp: 'allow',
40
+ todowrite: 'allow',
41
+ todoread: 'allow',
42
+ question: 'allow',
43
+ };
44
+ export function buildEngineerPermissions() {
45
+ const denied = {};
46
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
47
+ denied[toolId] = 'deny';
48
+ }
49
+ return {
50
+ '*': 'deny',
51
+ ...denied,
52
+ claude: 'allow',
53
+ };
54
+ }
55
+ export function denyRestrictedToolsGlobally(permissions) {
56
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
57
+ permissions[toolId] ??= 'deny';
58
+ }
59
+ }
@@ -0,0 +1,9 @@
1
+ import type { ManagerPromptRegistry } from '../../types/contracts.js';
2
+ import type { AgentPermission } from './common.js';
3
+ export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
4
+ description: string;
5
+ mode: "primary";
6
+ color: string;
7
+ permission: AgentPermission;
8
+ prompt: string;
9
+ };
@@ -0,0 +1,39 @@
1
+ import { AGENT_BROWSER_QA, AGENT_TEAM_PLANNER, ALL_RESTRICTED_TOOL_IDS, CTO_ONLY_TOOL_IDS, CTO_READONLY_TOOLS, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './common.js';
2
+ function buildCtoPermissions() {
3
+ const denied = {};
4
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
5
+ denied[toolId] = 'deny';
6
+ }
7
+ const allowed = {};
8
+ for (const toolId of CTO_ONLY_TOOL_IDS) {
9
+ allowed[toolId] = 'allow';
10
+ }
11
+ const taskPermissions = { '*': 'deny' };
12
+ for (const engineer of ENGINEER_AGENT_NAMES) {
13
+ const agentId = ENGINEER_AGENT_IDS[engineer];
14
+ // Support both uppercase (user-friendly) and lowercase (canonical) agent IDs.
15
+ // This ensures both task({ subagent_type: 'Tom' }) and task({ subagent_type: 'tom' }) work.
16
+ taskPermissions[engineer] = 'allow'; // 'Tom', 'John', etc.
17
+ taskPermissions[agentId] = 'allow'; // 'tom', 'john', etc.
18
+ }
19
+ // BrowserQA is a specialist registered separately from ENGINEER_AGENT_NAMES; add both forms.
20
+ taskPermissions['BrowserQA'] = 'allow'; // uppercase (user-friendly)
21
+ taskPermissions[AGENT_BROWSER_QA] = 'allow'; // 'browser-qa' canonical
22
+ taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
23
+ return {
24
+ '*': 'deny',
25
+ ...CTO_READONLY_TOOLS,
26
+ ...denied,
27
+ ...allowed,
28
+ task: taskPermissions,
29
+ };
30
+ }
31
+ export function buildCtoAgentConfig(prompts) {
32
+ return {
33
+ description: 'Principal engineer who orchestrates AI-powered engineers. Decomposes work, asks clarifying questions, delegates precisely, reviews diffs, and owns the outcome.',
34
+ mode: 'primary',
35
+ color: '#D97757',
36
+ permission: buildCtoPermissions(),
37
+ prompt: prompts.ctoSystemPrompt,
38
+ };
39
+ }
@@ -0,0 +1,9 @@
1
+ import type { EngineerName, ManagerPromptRegistry } from '../../types/contracts.js';
2
+ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry, engineer: EngineerName): {
3
+ description: string;
4
+ mode: "subagent";
5
+ hidden: boolean;
6
+ color: string;
7
+ permission: import("./common.js").AgentPermission;
8
+ prompt: string;
9
+ };