@doingdev/opencode-claude-manager-plugin 0.1.55 → 0.1.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/manager/team-orchestrator.d.ts +10 -3
- package/dist/manager/team-orchestrator.js +108 -17
- package/dist/plugin/agent-hierarchy.js +7 -3
- package/dist/plugin/claude-manager.plugin.d.ts +8 -0
- package/dist/plugin/claude-manager.plugin.js +38 -15
- package/dist/prompts/registry.js +107 -57
- package/dist/src/manager/team-orchestrator.d.ts +12 -5
- package/dist/src/manager/team-orchestrator.js +111 -20
- package/dist/src/plugin/agent-hierarchy.d.ts +2 -2
- package/dist/src/plugin/agent-hierarchy.js +15 -20
- package/dist/src/plugin/claude-manager.plugin.d.ts +8 -0
- package/dist/src/plugin/claude-manager.plugin.js +51 -27
- package/dist/src/plugin/service-factory.js +1 -1
- package/dist/src/prompts/registry.js +115 -57
- package/dist/src/types/contracts.d.ts +4 -1
- package/dist/test/claude-manager.plugin.test.js +94 -13
- package/dist/test/prompt-registry.test.js +26 -12
- package/dist/test/report-claude-event.test.js +16 -3
- package/dist/test/team-orchestrator.test.js +127 -7
- package/dist/types/contracts.d.ts +1 -1
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@ describe('TeamOrchestrator', () => {
|
|
|
35
35
|
outputTokens: 300,
|
|
36
36
|
contextWindowSize: 200_000,
|
|
37
37
|
});
|
|
38
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
38
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
39
39
|
const first = await orchestrator.dispatchEngineer({
|
|
40
40
|
teamId: 'team-1',
|
|
41
41
|
cwd: tempRoot,
|
|
@@ -53,17 +53,22 @@ describe('TeamOrchestrator', () => {
|
|
|
53
53
|
expect(first.sessionId).toBe('ses_tom');
|
|
54
54
|
expect(second.sessionId).toBe('ses_tom');
|
|
55
55
|
expect(runTask.mock.calls[0]?.[0]).toMatchObject({
|
|
56
|
-
systemPrompt: expect.stringContaining('Assigned engineer: Tom.'),
|
|
57
56
|
resumeSessionId: undefined,
|
|
58
57
|
permissionMode: 'acceptEdits',
|
|
59
58
|
restrictWriteTools: true,
|
|
60
59
|
});
|
|
60
|
+
expect(runTask.mock.calls[0]?.[0].systemPrompt).toBeUndefined();
|
|
61
|
+
expect(runTask.mock.calls[0]?.[0].prompt).toContain('Base engineer prompt');
|
|
62
|
+
expect(runTask.mock.calls[0]?.[0].prompt).toContain('Assigned engineer: Tom.');
|
|
63
|
+
expect(runTask.mock.calls[0]?.[0].prompt).toContain('Investigate the auth flow');
|
|
61
64
|
expect(runTask.mock.calls[1]?.[0]).toMatchObject({
|
|
62
|
-
systemPrompt: undefined,
|
|
63
65
|
resumeSessionId: 'ses_tom',
|
|
64
66
|
permissionMode: 'acceptEdits',
|
|
65
67
|
restrictWriteTools: false,
|
|
66
68
|
});
|
|
69
|
+
expect(runTask.mock.calls[1]?.[0].systemPrompt).toBeUndefined();
|
|
70
|
+
expect(runTask.mock.calls[1]?.[0].prompt).not.toContain('Assigned engineer: Tom.');
|
|
71
|
+
expect(runTask.mock.calls[1]?.[0].prompt).toContain('Implement the chosen fix');
|
|
67
72
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
68
73
|
expect(team.engineers.find((engineer) => engineer.name === 'Tom')).toMatchObject({
|
|
69
74
|
claudeSessionId: 'ses_tom',
|
|
@@ -74,7 +79,7 @@ describe('TeamOrchestrator', () => {
|
|
|
74
79
|
it('rejects work when the same engineer is already busy', async () => {
|
|
75
80
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
76
81
|
const store = new TeamStateStore('.state');
|
|
77
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
82
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
78
83
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
79
84
|
await store.saveTeam({
|
|
80
85
|
...team,
|
|
@@ -112,7 +117,7 @@ describe('TeamOrchestrator', () => {
|
|
|
112
117
|
events: [],
|
|
113
118
|
finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
|
|
114
119
|
});
|
|
115
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
120
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
116
121
|
const result = await orchestrator.planWithTeam({
|
|
117
122
|
teamId: 'team-1',
|
|
118
123
|
cwd: tempRoot,
|
|
@@ -125,6 +130,10 @@ describe('TeamOrchestrator', () => {
|
|
|
125
130
|
expect(result.recommendedQuestion).toBe('Should we migrate now?');
|
|
126
131
|
expect(result.recommendedAnswer).toBe('No, defer it.');
|
|
127
132
|
expect(runTask).toHaveBeenCalledTimes(3);
|
|
133
|
+
const synthesisCall = runTask.mock.calls[2]?.[0];
|
|
134
|
+
expect(synthesisCall.systemPrompt).toBeUndefined();
|
|
135
|
+
expect(synthesisCall.prompt).toContain('Synthesis prompt');
|
|
136
|
+
expect(synthesisCall.prompt).toContain('Plan the billing refactor');
|
|
128
137
|
});
|
|
129
138
|
it('invokes lead, challenger, and synthesis event callbacks', async () => {
|
|
130
139
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
@@ -157,7 +166,7 @@ describe('TeamOrchestrator', () => {
|
|
|
157
166
|
};
|
|
158
167
|
}
|
|
159
168
|
});
|
|
160
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
169
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
161
170
|
const onLeadEvent = vi.fn();
|
|
162
171
|
const onChallengerEvent = vi.fn();
|
|
163
172
|
const onSynthesisEvent = vi.fn();
|
|
@@ -177,7 +186,7 @@ describe('TeamOrchestrator', () => {
|
|
|
177
186
|
});
|
|
178
187
|
it('persists wrapper session memory for an engineer', async () => {
|
|
179
188
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
180
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
189
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
181
190
|
await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
|
|
182
191
|
await orchestrator.recordWrapperExchange(tempRoot, 'team-1', 'Tom', 'wrapper-tom', 'explore', 'Investigate the auth flow and compare approaches', 'The auth flow uses one shared validator and the cookie refresh path is the main risk.');
|
|
183
192
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
@@ -194,4 +203,115 @@ describe('TeamOrchestrator', () => {
|
|
|
194
203
|
engineer: 'Tom',
|
|
195
204
|
});
|
|
196
205
|
});
|
|
206
|
+
it('planWithTeam auto-selects two distinct engineers when names are omitted', async () => {
|
|
207
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
208
|
+
const runTask = vi.fn(async (_input, _onEvent) => {
|
|
209
|
+
// Return different results for lead vs challenger
|
|
210
|
+
const calls = runTask.mock.calls.length;
|
|
211
|
+
if (calls === 1) {
|
|
212
|
+
return {
|
|
213
|
+
sessionId: 'ses_lead',
|
|
214
|
+
events: [],
|
|
215
|
+
finalText: 'Lead plan',
|
|
216
|
+
turns: 1,
|
|
217
|
+
totalCostUsd: 0.01,
|
|
218
|
+
inputTokens: 100,
|
|
219
|
+
outputTokens: 50,
|
|
220
|
+
contextWindowSize: 200_000,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
else if (calls === 2) {
|
|
224
|
+
return {
|
|
225
|
+
sessionId: 'ses_challenger',
|
|
226
|
+
events: [],
|
|
227
|
+
finalText: 'Challenger plan',
|
|
228
|
+
turns: 1,
|
|
229
|
+
totalCostUsd: 0.01,
|
|
230
|
+
inputTokens: 100,
|
|
231
|
+
outputTokens: 50,
|
|
232
|
+
contextWindowSize: 200_000,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
return {
|
|
237
|
+
sessionId: undefined,
|
|
238
|
+
events: [],
|
|
239
|
+
finalText: '## Synthesis\nBest plan\n## Recommended Question\nNONE\n## Recommended Answer\nNONE',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
244
|
+
const result = await orchestrator.planWithTeam({
|
|
245
|
+
teamId: 'team-1',
|
|
246
|
+
cwd: tempRoot,
|
|
247
|
+
request: 'Plan the refactor',
|
|
248
|
+
// NOTE: both leadEngineer and challengerEngineer are omitted
|
|
249
|
+
});
|
|
250
|
+
expect(result.leadEngineer).toBeDefined();
|
|
251
|
+
expect(result.challengerEngineer).toBeDefined();
|
|
252
|
+
expect(result.leadEngineer).not.toEqual(result.challengerEngineer);
|
|
253
|
+
});
|
|
254
|
+
it('throws error when fewer than 2 viable engineers exist for planning', async () => {
|
|
255
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
256
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
257
|
+
// Mark all engineers as busy
|
|
258
|
+
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
259
|
+
for (const engineer of team.engineers) {
|
|
260
|
+
await orchestrator['updateEngineer'](tempRoot, 'team-1', engineer.name, (e) => ({
|
|
261
|
+
...e,
|
|
262
|
+
busy: true,
|
|
263
|
+
busySince: new Date().toISOString(),
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
await expect(orchestrator.planWithTeam({
|
|
267
|
+
teamId: 'team-1',
|
|
268
|
+
cwd: tempRoot,
|
|
269
|
+
request: 'Plan something',
|
|
270
|
+
})).rejects.toThrow('Not enough available engineers for dual planning');
|
|
271
|
+
});
|
|
272
|
+
it('context exhaustion retries exactly once with same assignment message and fresh session', async () => {
|
|
273
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
274
|
+
let callCount = 0;
|
|
275
|
+
let lastInputMessage = '';
|
|
276
|
+
const runTask = vi.fn(async (input) => {
|
|
277
|
+
callCount++;
|
|
278
|
+
lastInputMessage = input.prompt ?? '';
|
|
279
|
+
// First call throws context exhaustion, second succeeds
|
|
280
|
+
if (callCount === 1) {
|
|
281
|
+
const error = new Error('Token limit exceeded: context exhausted');
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
sessionId: 'ses_retry',
|
|
286
|
+
events: [],
|
|
287
|
+
finalText: 'Success after retry',
|
|
288
|
+
turns: 1,
|
|
289
|
+
totalCostUsd: 0.02,
|
|
290
|
+
inputTokens: 100,
|
|
291
|
+
outputTokens: 50,
|
|
292
|
+
contextWindowSize: 200_000,
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
296
|
+
const allEvents = [];
|
|
297
|
+
const result = await orchestrator.dispatchEngineer({
|
|
298
|
+
teamId: 'team-1',
|
|
299
|
+
cwd: tempRoot,
|
|
300
|
+
engineer: 'Tom',
|
|
301
|
+
mode: 'implement',
|
|
302
|
+
message: 'Fix the bug',
|
|
303
|
+
onEvent: (event) => {
|
|
304
|
+
allEvents.push({ type: event.type, text: event.text });
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
// Verify retry happened exactly once (2 runTask calls total)
|
|
308
|
+
expect(callCount).toBe(2);
|
|
309
|
+
// Verify status event was emitted for context exhaustion
|
|
310
|
+
const statusEvent = allEvents.find((e) => e.type === 'status');
|
|
311
|
+
expect(statusEvent?.text).toContain('Context exhausted');
|
|
312
|
+
// Verify the retry message is the same (contains the original task)
|
|
313
|
+
expect(lastInputMessage).toContain('Fix the bug');
|
|
314
|
+
// Verify success result
|
|
315
|
+
expect(result.finalText).toBe('Success after retry');
|
|
316
|
+
});
|
|
197
317
|
});
|
|
@@ -2,7 +2,7 @@ export interface ManagerPromptRegistry {
|
|
|
2
2
|
ctoSystemPrompt: string;
|
|
3
3
|
engineerAgentPrompt: string;
|
|
4
4
|
engineerSessionPrompt: string;
|
|
5
|
-
/** Prompt
|
|
5
|
+
/** Prompt prepended to the user prompt of the synthesis runTask call inside plan_with_team. */
|
|
6
6
|
planSynthesisPrompt: string;
|
|
7
7
|
/** Visible subagent prompt for teamPlanner — thin bridge that calls plan_with_team. */
|
|
8
8
|
teamPlannerPrompt: string;
|