@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.
- package/dist/index.d.ts +1 -1
- package/dist/manager/team-orchestrator.js +1 -1
- package/dist/plugin/agents/common.d.ts +2 -2
- package/dist/plugin/agents/common.js +5 -0
- package/dist/plugin/claude-manager.plugin.js +104 -0
- package/dist/plugin/inbox-ops.d.ts +50 -0
- package/dist/plugin/inbox-ops.js +166 -0
- package/dist/types/contracts.d.ts +18 -0
- package/package.json +1 -1
- package/dist/claude/session-live-tailer.d.ts +0 -51
- package/dist/claude/session-live-tailer.js +0 -269
- package/dist/manager/session-controller.d.ts +0 -41
- package/dist/manager/session-controller.js +0 -97
- package/dist/metadata/claude-metadata.service.d.ts +0 -12
- package/dist/metadata/claude-metadata.service.js +0 -38
- package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
- package/dist/metadata/repo-claude-config-reader.js +0 -154
- package/dist/plugin/orchestrator.plugin.d.ts +0 -2
- package/dist/plugin/orchestrator.plugin.js +0 -116
- package/dist/providers/claude-code-wrapper.d.ts +0 -13
- package/dist/providers/claude-code-wrapper.js +0 -13
- package/dist/safety/bash-safety.d.ts +0 -21
- package/dist/safety/bash-safety.js +0 -62
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +0 -28
- package/dist/src/claude/claude-agent-sdk-adapter.js +0 -559
- package/dist/src/claude/claude-session.service.d.ts +0 -9
- package/dist/src/claude/claude-session.service.js +0 -15
- package/dist/src/claude/session-live-tailer.d.ts +0 -51
- package/dist/src/claude/session-live-tailer.js +0 -269
- package/dist/src/claude/tool-approval-manager.d.ts +0 -30
- package/dist/src/claude/tool-approval-manager.js +0 -279
- package/dist/src/index.d.ts +0 -5
- package/dist/src/index.js +0 -3
- package/dist/src/manager/context-tracker.d.ts +0 -32
- package/dist/src/manager/context-tracker.js +0 -103
- package/dist/src/manager/git-operations.d.ts +0 -18
- package/dist/src/manager/git-operations.js +0 -86
- package/dist/src/manager/persistent-manager.d.ts +0 -39
- package/dist/src/manager/persistent-manager.js +0 -44
- package/dist/src/manager/session-controller.d.ts +0 -41
- package/dist/src/manager/session-controller.js +0 -97
- package/dist/src/manager/team-orchestrator.d.ts +0 -81
- package/dist/src/manager/team-orchestrator.js +0 -612
- package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/src/plugin/agent-hierarchy.js +0 -2
- package/dist/src/plugin/agents/browser-qa.d.ts +0 -14
- package/dist/src/plugin/agents/browser-qa.js +0 -31
- package/dist/src/plugin/agents/common.d.ts +0 -36
- package/dist/src/plugin/agents/common.js +0 -59
- package/dist/src/plugin/agents/cto.d.ts +0 -9
- package/dist/src/plugin/agents/cto.js +0 -39
- package/dist/src/plugin/agents/engineers.d.ts +0 -9
- package/dist/src/plugin/agents/engineers.js +0 -11
- package/dist/src/plugin/agents/index.d.ts +0 -5
- package/dist/src/plugin/agents/index.js +0 -5
- package/dist/src/plugin/agents/team-planner.d.ts +0 -10
- package/dist/src/plugin/agents/team-planner.js +0 -23
- package/dist/src/plugin/claude-manager.plugin.d.ts +0 -10
- package/dist/src/plugin/claude-manager.plugin.js +0 -950
- package/dist/src/plugin/service-factory.d.ts +0 -38
- package/dist/src/plugin/service-factory.js +0 -101
- package/dist/src/prompts/registry.d.ts +0 -2
- package/dist/src/prompts/registry.js +0 -210
- package/dist/src/state/file-run-state-store.d.ts +0 -14
- package/dist/src/state/file-run-state-store.js +0 -85
- package/dist/src/state/team-state-store.d.ts +0 -14
- package/dist/src/state/team-state-store.js +0 -88
- package/dist/src/state/transcript-store.d.ts +0 -15
- package/dist/src/state/transcript-store.js +0 -44
- package/dist/src/team/roster.d.ts +0 -5
- package/dist/src/team/roster.js +0 -40
- package/dist/src/types/contracts.d.ts +0 -261
- package/dist/src/types/contracts.js +0 -2
- package/dist/src/util/fs-helpers.d.ts +0 -8
- package/dist/src/util/fs-helpers.js +0 -21
- package/dist/src/util/project-context.d.ts +0 -10
- package/dist/src/util/project-context.js +0 -105
- package/dist/src/util/transcript-append.d.ts +0 -7
- package/dist/src/util/transcript-append.js +0 -29
- package/dist/state/file-run-state-store.d.ts +0 -14
- package/dist/state/file-run-state-store.js +0 -85
- package/dist/test/claude-agent-sdk-adapter.test.d.ts +0 -1
- package/dist/test/claude-agent-sdk-adapter.test.js +0 -707
- package/dist/test/claude-manager.plugin.test.d.ts +0 -1
- package/dist/test/claude-manager.plugin.test.js +0 -316
- package/dist/test/context-tracker.test.d.ts +0 -1
- package/dist/test/context-tracker.test.js +0 -130
- package/dist/test/cto-active-team.test.d.ts +0 -1
- package/dist/test/cto-active-team.test.js +0 -199
- package/dist/test/file-run-state-store.test.d.ts +0 -1
- package/dist/test/file-run-state-store.test.js +0 -82
- package/dist/test/fs-helpers.test.d.ts +0 -1
- package/dist/test/fs-helpers.test.js +0 -56
- package/dist/test/git-operations.test.d.ts +0 -1
- package/dist/test/git-operations.test.js +0 -133
- package/dist/test/persistent-manager.test.d.ts +0 -1
- package/dist/test/persistent-manager.test.js +0 -48
- package/dist/test/project-context.test.d.ts +0 -1
- package/dist/test/project-context.test.js +0 -92
- package/dist/test/prompt-registry.test.d.ts +0 -1
- package/dist/test/prompt-registry.test.js +0 -117
- package/dist/test/report-claude-event.test.d.ts +0 -1
- package/dist/test/report-claude-event.test.js +0 -304
- package/dist/test/session-controller.test.d.ts +0 -1
- package/dist/test/session-controller.test.js +0 -149
- package/dist/test/session-live-tailer.test.d.ts +0 -1
- package/dist/test/session-live-tailer.test.js +0 -313
- package/dist/test/team-orchestrator.test.d.ts +0 -1
- package/dist/test/team-orchestrator.test.js +0 -583
- package/dist/test/team-state-store.test.d.ts +0 -1
- package/dist/test/team-state-store.test.js +0 -54
- package/dist/test/tool-approval-manager.test.d.ts +0 -1
- package/dist/test/tool-approval-manager.test.js +0 -260
- package/dist/test/transcript-append.test.d.ts +0 -1
- package/dist/test/transcript-append.test.js +0 -37
- package/dist/test/transcript-store.test.d.ts +0 -1
- package/dist/test/transcript-store.test.js +0 -50
- package/dist/test/undo-propagation.test.d.ts +0 -1
- package/dist/test/undo-propagation.test.js +0 -837
- package/dist/util/project-context.d.ts +0 -10
- package/dist/util/project-context.js +0 -105
- package/dist/vitest.config.d.ts +0 -2
- 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,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
|
-
}
|