@doingdev/opencode-claude-manager-plugin 0.1.65 → 0.1.66

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 (123) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/manager/team-orchestrator.js +1 -1
  3. package/dist/plugin/agents/common.d.ts +2 -2
  4. package/dist/plugin/agents/common.js +5 -0
  5. package/dist/plugin/claude-manager.plugin.js +104 -0
  6. package/dist/plugin/inbox-ops.d.ts +50 -0
  7. package/dist/plugin/inbox-ops.js +166 -0
  8. package/dist/types/contracts.d.ts +18 -0
  9. package/package.json +1 -1
  10. package/dist/claude/session-live-tailer.d.ts +0 -51
  11. package/dist/claude/session-live-tailer.js +0 -269
  12. package/dist/manager/session-controller.d.ts +0 -41
  13. package/dist/manager/session-controller.js +0 -97
  14. package/dist/metadata/claude-metadata.service.d.ts +0 -12
  15. package/dist/metadata/claude-metadata.service.js +0 -38
  16. package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
  17. package/dist/metadata/repo-claude-config-reader.js +0 -154
  18. package/dist/plugin/orchestrator.plugin.d.ts +0 -2
  19. package/dist/plugin/orchestrator.plugin.js +0 -116
  20. package/dist/providers/claude-code-wrapper.d.ts +0 -13
  21. package/dist/providers/claude-code-wrapper.js +0 -13
  22. package/dist/safety/bash-safety.d.ts +0 -21
  23. package/dist/safety/bash-safety.js +0 -62
  24. package/dist/src/claude/claude-agent-sdk-adapter.d.ts +0 -28
  25. package/dist/src/claude/claude-agent-sdk-adapter.js +0 -559
  26. package/dist/src/claude/claude-session.service.d.ts +0 -9
  27. package/dist/src/claude/claude-session.service.js +0 -15
  28. package/dist/src/claude/session-live-tailer.d.ts +0 -51
  29. package/dist/src/claude/session-live-tailer.js +0 -269
  30. package/dist/src/claude/tool-approval-manager.d.ts +0 -30
  31. package/dist/src/claude/tool-approval-manager.js +0 -279
  32. package/dist/src/index.d.ts +0 -5
  33. package/dist/src/index.js +0 -3
  34. package/dist/src/manager/context-tracker.d.ts +0 -32
  35. package/dist/src/manager/context-tracker.js +0 -103
  36. package/dist/src/manager/git-operations.d.ts +0 -18
  37. package/dist/src/manager/git-operations.js +0 -86
  38. package/dist/src/manager/persistent-manager.d.ts +0 -39
  39. package/dist/src/manager/persistent-manager.js +0 -44
  40. package/dist/src/manager/session-controller.d.ts +0 -41
  41. package/dist/src/manager/session-controller.js +0 -97
  42. package/dist/src/manager/team-orchestrator.d.ts +0 -81
  43. package/dist/src/manager/team-orchestrator.js +0 -612
  44. package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
  45. package/dist/src/plugin/agent-hierarchy.js +0 -2
  46. package/dist/src/plugin/agents/browser-qa.d.ts +0 -14
  47. package/dist/src/plugin/agents/browser-qa.js +0 -31
  48. package/dist/src/plugin/agents/common.d.ts +0 -36
  49. package/dist/src/plugin/agents/common.js +0 -59
  50. package/dist/src/plugin/agents/cto.d.ts +0 -9
  51. package/dist/src/plugin/agents/cto.js +0 -39
  52. package/dist/src/plugin/agents/engineers.d.ts +0 -9
  53. package/dist/src/plugin/agents/engineers.js +0 -11
  54. package/dist/src/plugin/agents/index.d.ts +0 -5
  55. package/dist/src/plugin/agents/index.js +0 -5
  56. package/dist/src/plugin/agents/team-planner.d.ts +0 -10
  57. package/dist/src/plugin/agents/team-planner.js +0 -23
  58. package/dist/src/plugin/claude-manager.plugin.d.ts +0 -10
  59. package/dist/src/plugin/claude-manager.plugin.js +0 -950
  60. package/dist/src/plugin/service-factory.d.ts +0 -38
  61. package/dist/src/plugin/service-factory.js +0 -101
  62. package/dist/src/prompts/registry.d.ts +0 -2
  63. package/dist/src/prompts/registry.js +0 -210
  64. package/dist/src/state/file-run-state-store.d.ts +0 -14
  65. package/dist/src/state/file-run-state-store.js +0 -85
  66. package/dist/src/state/team-state-store.d.ts +0 -14
  67. package/dist/src/state/team-state-store.js +0 -88
  68. package/dist/src/state/transcript-store.d.ts +0 -15
  69. package/dist/src/state/transcript-store.js +0 -44
  70. package/dist/src/team/roster.d.ts +0 -5
  71. package/dist/src/team/roster.js +0 -40
  72. package/dist/src/types/contracts.d.ts +0 -261
  73. package/dist/src/types/contracts.js +0 -2
  74. package/dist/src/util/fs-helpers.d.ts +0 -8
  75. package/dist/src/util/fs-helpers.js +0 -21
  76. package/dist/src/util/project-context.d.ts +0 -10
  77. package/dist/src/util/project-context.js +0 -105
  78. package/dist/src/util/transcript-append.d.ts +0 -7
  79. package/dist/src/util/transcript-append.js +0 -29
  80. package/dist/state/file-run-state-store.d.ts +0 -14
  81. package/dist/state/file-run-state-store.js +0 -85
  82. package/dist/test/claude-agent-sdk-adapter.test.d.ts +0 -1
  83. package/dist/test/claude-agent-sdk-adapter.test.js +0 -707
  84. package/dist/test/claude-manager.plugin.test.d.ts +0 -1
  85. package/dist/test/claude-manager.plugin.test.js +0 -316
  86. package/dist/test/context-tracker.test.d.ts +0 -1
  87. package/dist/test/context-tracker.test.js +0 -130
  88. package/dist/test/cto-active-team.test.d.ts +0 -1
  89. package/dist/test/cto-active-team.test.js +0 -199
  90. package/dist/test/file-run-state-store.test.d.ts +0 -1
  91. package/dist/test/file-run-state-store.test.js +0 -82
  92. package/dist/test/fs-helpers.test.d.ts +0 -1
  93. package/dist/test/fs-helpers.test.js +0 -56
  94. package/dist/test/git-operations.test.d.ts +0 -1
  95. package/dist/test/git-operations.test.js +0 -133
  96. package/dist/test/persistent-manager.test.d.ts +0 -1
  97. package/dist/test/persistent-manager.test.js +0 -48
  98. package/dist/test/project-context.test.d.ts +0 -1
  99. package/dist/test/project-context.test.js +0 -92
  100. package/dist/test/prompt-registry.test.d.ts +0 -1
  101. package/dist/test/prompt-registry.test.js +0 -117
  102. package/dist/test/report-claude-event.test.d.ts +0 -1
  103. package/dist/test/report-claude-event.test.js +0 -304
  104. package/dist/test/session-controller.test.d.ts +0 -1
  105. package/dist/test/session-controller.test.js +0 -149
  106. package/dist/test/session-live-tailer.test.d.ts +0 -1
  107. package/dist/test/session-live-tailer.test.js +0 -313
  108. package/dist/test/team-orchestrator.test.d.ts +0 -1
  109. package/dist/test/team-orchestrator.test.js +0 -583
  110. package/dist/test/team-state-store.test.d.ts +0 -1
  111. package/dist/test/team-state-store.test.js +0 -54
  112. package/dist/test/tool-approval-manager.test.d.ts +0 -1
  113. package/dist/test/tool-approval-manager.test.js +0 -260
  114. package/dist/test/transcript-append.test.d.ts +0 -1
  115. package/dist/test/transcript-append.test.js +0 -37
  116. package/dist/test/transcript-store.test.d.ts +0 -1
  117. package/dist/test/transcript-store.test.js +0 -50
  118. package/dist/test/undo-propagation.test.d.ts +0 -1
  119. package/dist/test/undo-propagation.test.js +0 -837
  120. package/dist/util/project-context.d.ts +0 -10
  121. package/dist/util/project-context.js +0 -105
  122. package/dist/vitest.config.d.ts +0 -2
  123. package/dist/vitest.config.js +0 -11
@@ -1,612 +0,0 @@
1
- import { createEmptyEngineerRecord, createEmptyTeamRecord } from '../team/roster.js';
2
- import { ContextTracker } from './context-tracker.js';
3
- const BUSY_LEASE_MS = 15 * 60 * 1000;
4
- export class TeamOrchestrator {
5
- sessions;
6
- teamStore;
7
- transcriptStore;
8
- engineerSessionPrompt;
9
- planSynthesisPrompt;
10
- workerCapabilities;
11
- constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, planSynthesisPrompt, workerCapabilities) {
12
- this.sessions = sessions;
13
- this.teamStore = teamStore;
14
- this.transcriptStore = transcriptStore;
15
- this.engineerSessionPrompt = engineerSessionPrompt;
16
- this.planSynthesisPrompt = planSynthesisPrompt;
17
- this.workerCapabilities = workerCapabilities;
18
- }
19
- async getOrCreateTeam(cwd, teamId) {
20
- const existing = await this.teamStore.getTeam(cwd, teamId);
21
- if (existing) {
22
- return this.normalizeTeamRecord(existing);
23
- }
24
- const created = createEmptyTeamRecord(teamId, cwd);
25
- await this.teamStore.saveTeam(created);
26
- return created;
27
- }
28
- async listTeams(cwd) {
29
- const teams = await this.teamStore.listTeams(cwd);
30
- return teams.map((team) => this.normalizeTeamRecord(team));
31
- }
32
- async recordWrapperSession(cwd, teamId, engineer, wrapperSessionId) {
33
- await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
34
- ...entry,
35
- wrapperSessionId,
36
- }));
37
- }
38
- async recordWrapperExchange(cwd, teamId, engineer, wrapperSessionId, mode, assignment, result) {
39
- const timestamp = new Date().toISOString();
40
- await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
41
- ...entry,
42
- wrapperSessionId,
43
- wrapperHistory: appendWrapperHistoryEntries(entry.wrapperHistory, [
44
- { timestamp, type: 'assignment', mode, text: summarizeText(assignment, 320) },
45
- { timestamp, type: 'result', mode, text: summarizeText(result, 320) },
46
- ]),
47
- lastMode: mode,
48
- lastTaskSummary: summarizeMessage(assignment),
49
- lastUsedAt: timestamp,
50
- }));
51
- }
52
- async getWrapperSystemContext(cwd, teamId, engineer) {
53
- const team = await this.getOrCreateTeam(cwd, teamId);
54
- const state = this.getEngineerState(team, engineer);
55
- if (state.wrapperHistory.length === 0) {
56
- return null;
57
- }
58
- const historyLines = state.wrapperHistory.map((entry) => {
59
- const modeLabel = entry.mode ? ` [${entry.mode}]` : '';
60
- return `- ${entry.type}${modeLabel}: ${entry.text}`;
61
- });
62
- return [
63
- `Persistent wrapper memory for ${engineer} in CTO team ${teamId}:`,
64
- 'Use this only to improve delegation quality and continuity.',
65
- 'Prefer the current assignment when it conflicts with older context.',
66
- ...historyLines,
67
- ].join('\n');
68
- }
69
- async findTeamByWrapperSession(cwd, wrapperSessionId) {
70
- const teams = await this.listTeams(cwd);
71
- for (const team of teams) {
72
- for (const engineer of team.engineers) {
73
- if (engineer.wrapperSessionId === wrapperSessionId) {
74
- return {
75
- teamId: team.id,
76
- engineer: engineer.name,
77
- };
78
- }
79
- }
80
- }
81
- return null;
82
- }
83
- /**
84
- * Remove wrapper history entries whose timestamp is strictly after cutoffIso.
85
- * Used during CTO undo propagation to prune stale wrapper memory.
86
- */
87
- async pruneWrapperHistoryAfter(cwd, teamId, engineer, cutoffIso) {
88
- await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
89
- ...entry,
90
- wrapperHistory: entry.wrapperHistory.filter((h) => h.timestamp <= cutoffIso),
91
- }));
92
- }
93
- async resetEngineer(cwd, teamId, engineer, options) {
94
- await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
95
- ...entry,
96
- busy: false,
97
- busySince: null,
98
- claudeSessionId: options?.clearSession ? null : entry.claudeSessionId,
99
- wrapperHistory: options?.clearHistory ? [] : entry.wrapperHistory,
100
- context: options?.clearSession ? createEmptyEngineerRecord(engineer).context : entry.context,
101
- }));
102
- }
103
- async dispatchEngineer(input, retryCount = 0) {
104
- const workerCaps = this.workerCapabilities[input.engineer];
105
- // Reject write-restricted workers in implement mode
106
- if (workerCaps?.restrictWriteTools && input.mode === 'implement') {
107
- throw new Error(`${input.engineer} is a browser QA specialist and does not support implement mode. ` +
108
- 'It can only verify and explore (test browser interactions via Playwright). ' +
109
- 'For code changes, use a general engineer (Tom, John, Maya, Sara, Alex).');
110
- }
111
- const team = await this.getOrCreateTeam(input.cwd, input.teamId);
112
- const engineerState = this.getEngineerState(team, input.engineer);
113
- await this.reserveEngineer(input.cwd, input.teamId, input.engineer);
114
- try {
115
- const tracker = new ContextTracker();
116
- if (engineerState.context.sessionId) {
117
- tracker.restore({
118
- sessionId: engineerState.context.sessionId,
119
- totalTurns: engineerState.context.totalTurns,
120
- totalCostUsd: engineerState.context.totalCostUsd,
121
- estimatedContextPercent: engineerState.context.estimatedContextPercent,
122
- contextWindowSize: engineerState.context.contextWindowSize,
123
- latestInputTokens: engineerState.context.latestInputTokens,
124
- });
125
- }
126
- const result = await this.sessions.runTask({
127
- cwd: input.cwd,
128
- prompt: engineerState.claudeSessionId
129
- ? this.buildEngineerPrompt(input.mode, input.message, input.engineer)
130
- : `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message, input.engineer)}`,
131
- resumeSessionId: engineerState.claudeSessionId ?? undefined,
132
- persistSession: true,
133
- includePartialMessages: true,
134
- permissionMode: 'acceptEdits',
135
- allowedTools: workerCaps?.sessionAllowedTools,
136
- restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
137
- model: input.model,
138
- effort: (workerCaps?.restrictWriteTools ?? false)
139
- ? 'medium'
140
- : input.mode === 'implement'
141
- ? 'high'
142
- : 'medium',
143
- settingSources: ['user', 'project', 'local'],
144
- abortSignal: input.abortSignal,
145
- }, input.onEvent);
146
- tracker.recordResult({
147
- sessionId: result.sessionId,
148
- turns: result.turns,
149
- totalCostUsd: result.totalCostUsd,
150
- inputTokens: result.inputTokens,
151
- outputTokens: result.outputTokens,
152
- contextWindowSize: result.contextWindowSize,
153
- });
154
- if (result.sessionId && result.events.length > 0) {
155
- await this.transcriptStore.appendEvents(input.cwd, result.sessionId, result.events);
156
- }
157
- const context = tracker.snapshot();
158
- await this.updateEngineer(input.cwd, input.teamId, input.engineer, (entry) => ({
159
- ...entry,
160
- claudeSessionId: result.sessionId ?? engineerState.claudeSessionId,
161
- busy: false,
162
- busySince: null,
163
- lastMode: input.mode,
164
- lastTaskSummary: summarizeMessage(input.message),
165
- lastUsedAt: new Date().toISOString(),
166
- context,
167
- }));
168
- return {
169
- teamId: input.teamId,
170
- engineer: input.engineer,
171
- mode: input.mode,
172
- sessionId: result.sessionId,
173
- finalText: result.finalText,
174
- turns: result.turns,
175
- totalCostUsd: result.totalCostUsd,
176
- inputTokens: result.inputTokens,
177
- outputTokens: result.outputTokens,
178
- contextWindowSize: result.contextWindowSize,
179
- context,
180
- };
181
- }
182
- catch (error) {
183
- await this.updateEngineer(input.cwd, input.teamId, input.engineer, (engineer) => ({
184
- ...engineer,
185
- busy: false,
186
- busySince: null,
187
- }));
188
- // Handle context exhaustion with automatic retry (max 1 retry)
189
- const classified = TeamOrchestrator.classifyError(error);
190
- if (classified.failureKind === 'contextExhausted' && retryCount === 0) {
191
- // Reset the engineer's session and retry once with fresh session
192
- await this.resetEngineer(input.cwd, input.teamId, input.engineer, {
193
- clearSession: true,
194
- clearHistory: false,
195
- });
196
- // Emit status event before retry
197
- await input.onEvent?.({
198
- type: 'status',
199
- text: 'Context exhausted; resetting session and retrying once with a fresh session.',
200
- });
201
- try {
202
- // Retry dispatch with fresh session (retryCount=1 prevents infinite loop)
203
- // Use the exact same assignment message without modification
204
- return await this.dispatchEngineer(input, 1);
205
- }
206
- catch (retryError) {
207
- // If retry also fails with a different error, preserve retry failure info
208
- const retryClassified = TeamOrchestrator.classifyError(retryError);
209
- if (retryClassified.failureKind !== classified.failureKind) {
210
- // Create an error that shows both failures
211
- const combinedMessage = `Initial: ${classified.failureKind} (${classified.message})\n` +
212
- `After retry: ${retryClassified.failureKind} (${retryClassified.message})`;
213
- const combinedError = new Error(combinedMessage);
214
- Object.assign(combinedError, { cause: retryError });
215
- throw combinedError;
216
- }
217
- // Same error type on retry, throw the retry error (more recent state)
218
- throw retryError;
219
- }
220
- }
221
- throw error;
222
- }
223
- }
224
- static classifyError(error) {
225
- const message = error instanceof Error ? error.message : String(error);
226
- let failureKind = 'unknown';
227
- if (message.includes('already working on another assignment')) {
228
- failureKind = 'engineerBusy';
229
- }
230
- else if (message.includes('context') || message.includes('token limit')) {
231
- failureKind = 'contextExhausted';
232
- }
233
- else if (message.includes('does not support implement mode')) {
234
- failureKind = 'modeNotSupported';
235
- }
236
- else if (message.includes('denied') || message.includes('not allowed')) {
237
- failureKind = 'toolDenied';
238
- }
239
- else if (message.includes('abort') || message.includes('cancel')) {
240
- failureKind = 'aborted';
241
- }
242
- else {
243
- failureKind = 'sdkError';
244
- }
245
- return {
246
- teamId: '',
247
- engineer: 'Tom',
248
- mode: 'explore',
249
- failureKind,
250
- message,
251
- cause: error,
252
- };
253
- }
254
- async planWithTeam(input) {
255
- // Auto-select engineers if not provided
256
- const { lead: leadEngineer, challenger: challengerEngineer } = await this.selectPlanEngineers(input.cwd, input.teamId, input.leadEngineer, input.challengerEngineer);
257
- const [leadDraft, challengerDraft] = await Promise.all([
258
- this.dispatchEngineer({
259
- teamId: input.teamId,
260
- cwd: input.cwd,
261
- engineer: leadEngineer,
262
- mode: 'explore',
263
- message: buildPlanDraftRequest('lead', input.request),
264
- model: input.model,
265
- abortSignal: input.abortSignal,
266
- onEvent: input.onLeadEvent,
267
- }),
268
- this.dispatchEngineer({
269
- teamId: input.teamId,
270
- cwd: input.cwd,
271
- engineer: challengerEngineer,
272
- mode: 'explore',
273
- message: buildPlanDraftRequest('challenger', input.request),
274
- model: input.model,
275
- abortSignal: input.abortSignal,
276
- onEvent: input.onChallengerEvent,
277
- }),
278
- ]);
279
- const drafts = [
280
- { ...leadDraft, request: input.request },
281
- { ...challengerDraft, request: input.request },
282
- ];
283
- const synthesisResult = await this.sessions.runTask({
284
- cwd: input.cwd,
285
- prompt: `${this.planSynthesisPrompt}\n\n${buildSynthesisPrompt(input.request, drafts)}`,
286
- persistSession: false,
287
- includePartialMessages: false,
288
- permissionMode: 'acceptEdits',
289
- restrictWriteTools: true,
290
- model: input.model,
291
- effort: 'high',
292
- settingSources: ['user', 'project', 'local'],
293
- abortSignal: input.abortSignal,
294
- }, input.onSynthesisEvent);
295
- const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
296
- return {
297
- teamId: input.teamId,
298
- request: input.request,
299
- leadEngineer,
300
- challengerEngineer,
301
- drafts,
302
- synthesis: parsedSynthesis.synthesis,
303
- recommendedQuestion: parsedSynthesis.recommendedQuestion,
304
- recommendedAnswer: parsedSynthesis.recommendedAnswer,
305
- };
306
- }
307
- async updateEngineer(cwd, teamId, engineerName, update) {
308
- await this.getOrCreateTeam(cwd, teamId);
309
- await this.teamStore.updateTeam(cwd, teamId, (team) => {
310
- const normalized = this.normalizeTeamRecord(team);
311
- const existing = this.getEngineerState(normalized, engineerName);
312
- return {
313
- ...normalized,
314
- updatedAt: new Date().toISOString(),
315
- engineers: normalized.engineers.map((engineer) => engineer.name === engineerName ? update(existing) : engineer),
316
- };
317
- });
318
- }
319
- async reserveEngineer(cwd, teamId, engineerName) {
320
- await this.getOrCreateTeam(cwd, teamId);
321
- await this.teamStore.updateTeam(cwd, teamId, (team) => {
322
- const normalized = this.normalizeTeamRecord(team);
323
- const engineer = this.getEngineerState(normalized, engineerName);
324
- if (engineer.busy) {
325
- const leaseExpired = engineer.busySince !== null &&
326
- Date.now() - new Date(engineer.busySince).getTime() > BUSY_LEASE_MS;
327
- if (!leaseExpired) {
328
- throw new Error(`${engineerName} is already working on another assignment.`);
329
- }
330
- }
331
- const now = new Date().toISOString();
332
- return {
333
- ...normalized,
334
- updatedAt: now,
335
- engineers: normalized.engineers.map((entry) => entry.name === engineerName
336
- ? {
337
- ...entry,
338
- busy: true,
339
- busySince: now,
340
- }
341
- : entry),
342
- };
343
- });
344
- }
345
- getEngineerState(team, engineerName) {
346
- return (team.engineers.find((engineer) => engineer.name === engineerName) ??
347
- createEmptyEngineerRecord(engineerName));
348
- }
349
- normalizeTeamRecord(team) {
350
- const engineerMap = new Map(team.engineers.map((engineer) => [engineer.name, engineer]));
351
- const emptyTeam = createEmptyTeamRecord(team.id, team.cwd);
352
- return {
353
- ...team,
354
- engineers: emptyTeam.engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
355
- };
356
- }
357
- getAvailableEngineers(team) {
358
- const now = Date.now();
359
- return team.engineers
360
- .filter((engineer) => {
361
- if (!engineer.busy)
362
- return true;
363
- // If an engineer has been marked busy but the lease expired, they're available
364
- if (engineer.busySince) {
365
- const leaseExpired = now - new Date(engineer.busySince).getTime() > BUSY_LEASE_MS;
366
- return leaseExpired;
367
- }
368
- return false;
369
- })
370
- .sort((a, b) => {
371
- // Prefer engineers with lower context pressure and less-recently-used
372
- const aContext = a.context.estimatedContextPercent ?? 0;
373
- const bContext = b.context.estimatedContextPercent ?? 0;
374
- if (aContext !== bContext) {
375
- return aContext - bContext; // Lower context first
376
- }
377
- // If context is equal, prefer less-recently-used
378
- const aTime = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0;
379
- const bTime = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0;
380
- return aTime - bTime; // Earlier usage time first
381
- })
382
- .map((engineer) => engineer.name);
383
- }
384
- async selectPlanEngineers(cwd, teamId, preferredLead, preferredChallenger) {
385
- const team = await this.getOrCreateTeam(cwd, teamId);
386
- const allAvailable = this.getAvailableEngineers(team);
387
- // Filter to only planner-eligible engineers (specialists with plannerEligible: false are excluded)
388
- const available = allAvailable.filter((e) => this.workerCapabilities[e]?.plannerEligible !== false);
389
- if (available.length < 2) {
390
- throw new Error(`Not enough available engineers for dual planning. Need 2 general engineers (specialists excluded), found ${available.length}.`);
391
- }
392
- const lead = preferredLead ?? available[0];
393
- const foundChallenger = preferredChallenger ?? available.find((e) => e !== lead);
394
- const challenger = foundChallenger ?? available[1];
395
- if (lead === challenger) {
396
- throw new Error('Cannot use the same engineer for both lead and challenger.');
397
- }
398
- return { lead, challenger };
399
- }
400
- async getActivePlan(cwd, teamId) {
401
- const team = await this.getOrCreateTeam(cwd, teamId);
402
- return team.activePlan ?? null;
403
- }
404
- async setActivePlan(cwd, teamId, plan) {
405
- await this.getOrCreateTeam(cwd, teamId);
406
- const now = new Date().toISOString();
407
- const slices = plan.slices.map((description, index) => ({
408
- index,
409
- description,
410
- status: 'pending',
411
- }));
412
- const activePlan = {
413
- id: `plan-${Date.now()}`,
414
- summary: plan.summary,
415
- taskSize: plan.taskSize,
416
- createdAt: now,
417
- confirmedAt: now,
418
- preAuthorized: plan.preAuthorized,
419
- slices,
420
- currentSliceIndex: slices.length > 0 ? 0 : null,
421
- };
422
- await this.teamStore.updateTeam(cwd, teamId, (team) => ({
423
- ...team,
424
- updatedAt: now,
425
- activePlan,
426
- }));
427
- return activePlan;
428
- }
429
- async clearActivePlan(cwd, teamId) {
430
- await this.getOrCreateTeam(cwd, teamId);
431
- const now = new Date().toISOString();
432
- await this.teamStore.updateTeam(cwd, teamId, (team) => ({
433
- ...team,
434
- updatedAt: now,
435
- activePlan: undefined,
436
- }));
437
- }
438
- async updateActivePlanSlice(cwd, teamId, sliceIndex, status) {
439
- await this.getOrCreateTeam(cwd, teamId);
440
- const now = new Date().toISOString();
441
- await this.teamStore.updateTeam(cwd, teamId, (team) => {
442
- if (!team.activePlan) {
443
- throw new Error(`Cannot update slice: team "${teamId}" has no active plan. Persist an active plan before updating slices.`);
444
- }
445
- const sliceExists = team.activePlan.slices.some((s) => s.index === sliceIndex);
446
- if (!sliceExists) {
447
- const sliceCount = team.activePlan.slices.length;
448
- const rangeMsg = sliceCount === 0 ? 'plan has no slices' : `valid range: 0–${sliceCount - 1}`;
449
- throw new Error(`Cannot update slice: slice index ${sliceIndex} does not exist in active plan "${team.activePlan.id}" (${rangeMsg}).`);
450
- }
451
- const slices = team.activePlan.slices.map((s) => s.index === sliceIndex
452
- ? {
453
- ...s,
454
- status,
455
- ...(status === 'done' || status === 'skipped' ? { completedAt: now } : {}),
456
- }
457
- : s);
458
- const isLastSlice = sliceIndex === team.activePlan.slices.length - 1;
459
- const nextIndex = status === 'done' || status === 'skipped'
460
- ? isLastSlice
461
- ? null
462
- : sliceIndex + 1
463
- : team.activePlan.currentSliceIndex;
464
- return {
465
- ...team,
466
- updatedAt: now,
467
- activePlan: {
468
- ...team.activePlan,
469
- slices,
470
- currentSliceIndex: nextIndex,
471
- },
472
- };
473
- });
474
- }
475
- buildSessionSystemPrompt(engineer, mode) {
476
- const specialistPrompt = this.workerCapabilities[engineer]?.sessionPrompt;
477
- if (specialistPrompt) {
478
- return specialistPrompt;
479
- }
480
- return [
481
- this.engineerSessionPrompt,
482
- '',
483
- `Assigned engineer: ${engineer}.`,
484
- `Current work mode: ${mode}.`,
485
- ]
486
- .join('\n')
487
- .trim();
488
- }
489
- buildEngineerPrompt(mode, message, engineer) {
490
- if (this.workerCapabilities[engineer]?.skipModeInstructions) {
491
- return message;
492
- }
493
- return `${buildModeInstruction(mode)}\n\n${message}`;
494
- }
495
- }
496
- function buildModeInstruction(mode) {
497
- switch (mode) {
498
- case 'explore':
499
- return [
500
- 'Exploration mode.',
501
- 'Read, search, and reason about the codebase without editing files.',
502
- 'The caller should specify the desired output for this exploration task, such as root cause, findings, affected files, options, risk review, or a concrete plan.',
503
- 'If the caller does not specify the output shape, return concise findings, relevant file paths, open questions, and the recommended next step.',
504
- 'Do not create or edit files.',
505
- ].join(' ');
506
- case 'implement':
507
- return [
508
- 'Implementation mode.',
509
- 'Before making any edits, state a brief implementation plan: which files you will change, what each change does, and why.',
510
- 'Then make the changes, run the most relevant verification (tests, lint, typecheck), and report what changed and what you verified.',
511
- 'Before reporting done, review your own diff for issues that pass tests but break in production.',
512
- ].join(' ');
513
- case 'verify':
514
- return [
515
- 'Verification mode.',
516
- 'Run targeted checks in order of relevance: tests, lint, typecheck, build.',
517
- 'Check that changed code paths have test coverage.',
518
- 'Report pass/fail with evidence.',
519
- 'Escalate failures with exact output.',
520
- ].join(' ');
521
- }
522
- }
523
- function summarizeMessage(message) {
524
- const compact = message.replace(/\s+/g, ' ').trim();
525
- return compact.length > 120 ? `${compact.slice(0, 117)}...` : compact;
526
- }
527
- function summarizeText(text, limit) {
528
- const compact = text.replace(/\s+/g, ' ').trim();
529
- return compact.length > limit ? `${compact.slice(0, limit - 3)}...` : compact;
530
- }
531
- function appendWrapperHistoryEntries(existing, nextEntries) {
532
- return [...existing, ...nextEntries].slice(-12);
533
- }
534
- function buildPlanDraftRequest(perspective, request) {
535
- const posture = perspective === 'lead'
536
- ? 'You are the lead planner. Propose the most direct workable plan with concrete file paths and clear next steps. Think about failure modes and edge cases — what can break at each boundary?'
537
- : 'You are the challenger. Stress-test assumptions, surface missing decisions, and propose a stronger alternative when the lead plan is weak. Think about failure modes and edge cases — what can break at each boundary?';
538
- return [
539
- posture,
540
- '',
541
- 'Return exactly these sections:',
542
- '1. Objective',
543
- '2. Proposed approach (include system boundaries and data flow)',
544
- '3. Files or systems likely involved',
545
- '4. Failure modes and edge cases (what happens when things go wrong?)',
546
- '5. Risks and open questions',
547
- '6. Verification (how to prove it works, including what tests to add)',
548
- '7. Step-by-step plan',
549
- '',
550
- `User request: ${request}`,
551
- ].join('\n');
552
- }
553
- function buildSynthesisPrompt(request, drafts) {
554
- return [
555
- `User request: ${request}`,
556
- '',
557
- `Lead engineer (${drafts[0].engineer}) draft:`,
558
- drafts[0].finalText,
559
- '',
560
- `Challenger engineer (${drafts[1].engineer}) draft:`,
561
- drafts[1].finalText,
562
- ].join('\n');
563
- }
564
- function parseSynthesisResult(text) {
565
- const synthesis = extractSection(text, 'Synthesis') ?? text.trim();
566
- const recommendedQuestion = normalizeOptionalSection(extractSection(text, 'Recommended Question'));
567
- const recommendedAnswer = normalizeOptionalSection(extractSection(text, 'Recommended Answer'));
568
- return {
569
- synthesis,
570
- recommendedQuestion,
571
- recommendedAnswer,
572
- };
573
- }
574
- function extractSection(text, heading) {
575
- const regex = new RegExp(`## ${escapeRegExp(heading)}\\n([\\s\\S]*?)(?=\\n## |$)`);
576
- const match = text.match(regex);
577
- return match?.[1]?.trim() ?? null;
578
- }
579
- function normalizeOptionalSection(value) {
580
- if (!value) {
581
- return null;
582
- }
583
- return value.toUpperCase() === 'NONE' ? null : value;
584
- }
585
- function escapeRegExp(value) {
586
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
587
- }
588
- export function getFailureGuidanceText(failureKind) {
589
- switch (failureKind) {
590
- case 'contextExhausted':
591
- 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.';
592
- case 'engineerBusy':
593
- return 'This engineer is currently working on another assignment. Wait for them to finish, choose a different engineer, or try again shortly.';
594
- case 'toolDenied':
595
- return 'A tool permission was denied during the assignment. Check the approval policy and tool permissions, then retry.';
596
- case 'modeNotSupported':
597
- return 'This engineer does not support the requested work mode. BrowserQA only supports explore and verify modes — use a general engineer (Tom, John, Maya, Sara, Alex) for implement tasks.';
598
- case 'aborted':
599
- return 'The assignment was cancelled by the user or an abort signal was triggered. Review the request and try again.';
600
- case 'sdkError':
601
- return 'An SDK error occurred during the assignment. Check logs for details, ensure the Claude session is healthy, and retry.';
602
- default:
603
- return 'An unknown error occurred during the assignment. Check logs and retry.';
604
- }
605
- }
606
- export function createActionableError(failure, originalError) {
607
- const guidance = getFailureGuidanceText(failure.failureKind);
608
- const errorMessage = `[${failure.failureKind}] ${failure.message}\n\n` + `Next steps: ${guidance}`;
609
- const error = new Error(errorMessage);
610
- Object.assign(error, { cause: originalError });
611
- return error;
612
- }
@@ -1 +0,0 @@
1
- export * from './agents/index.js';
@@ -1,2 +0,0 @@
1
- // Re-export barrel — all symbols now live in src/plugin/agents/.
2
- export * from './agents/index.js';
@@ -1,14 +0,0 @@
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
- };
@@ -1,31 +0,0 @@
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
- // Pre-approve the Playwriter toolchain at the SDK level so headless sessions
16
- // never stall waiting for interactive confirmation. Write tools remain blocked
17
- // by restrictWriteTools and the canUseTool write-filter.
18
- sessionAllowedTools: ['Skill', 'Bash', 'Read', 'Grep', 'Glob', 'LS', 'ListDirectory'],
19
- },
20
- };
21
- }
22
- export function buildBrowserQaAgentConfig(prompts) {
23
- return {
24
- 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.',
25
- mode: 'subagent',
26
- hidden: false,
27
- color: '#D97757',
28
- permission: buildEngineerPermissions(), // Same permissions as engineers (claude tool only)
29
- prompt: prompts.browserQaAgentPrompt,
30
- };
31
- }