@doingdev/opencode-claude-manager-plugin 0.1.58 → 0.1.60
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 -1
- package/dist/manager/team-orchestrator.js +77 -1
- package/dist/plugin/agents/common.d.ts +2 -2
- package/dist/plugin/agents/common.js +2 -0
- package/dist/plugin/claude-manager.plugin.js +95 -24
- package/dist/plugin/service-factory.d.ts +4 -4
- package/dist/plugin/service-factory.js +14 -18
- package/dist/prompts/registry.js +22 -4
- package/dist/src/manager/team-orchestrator.d.ts +10 -1
- package/dist/src/manager/team-orchestrator.js +77 -1
- package/dist/src/plugin/agents/common.d.ts +2 -2
- package/dist/src/plugin/agents/common.js +2 -0
- package/dist/src/plugin/claude-manager.plugin.js +95 -24
- package/dist/src/plugin/service-factory.d.ts +4 -4
- package/dist/src/plugin/service-factory.js +14 -18
- package/dist/src/prompts/registry.js +22 -4
- package/dist/src/state/team-state-store.d.ts +0 -3
- package/dist/src/state/team-state-store.js +0 -22
- package/dist/src/types/contracts.d.ts +19 -0
- package/dist/state/team-state-store.d.ts +0 -3
- package/dist/state/team-state-store.js +0 -22
- package/dist/test/claude-manager.plugin.test.js +172 -1
- package/dist/test/cto-active-team.test.js +176 -29
- package/dist/test/prompt-registry.test.js +52 -0
- package/dist/test/report-claude-event.test.js +16 -12
- package/dist/test/team-orchestrator.test.js +158 -2
- package/dist/test/team-state-store.test.js +0 -18
- package/dist/types/contracts.d.ts +19 -0
- package/package.json +1 -1
|
@@ -44,7 +44,8 @@ describe('TeamOrchestrator', () => {
|
|
|
44
44
|
outputTokens: 300,
|
|
45
45
|
contextWindowSize: 200_000,
|
|
46
46
|
});
|
|
47
|
-
const
|
|
47
|
+
const store = new TeamStateStore('.state');
|
|
48
|
+
const orchestrator = new TeamOrchestrator({ runTask }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
48
49
|
const first = await orchestrator.dispatchEngineer({
|
|
49
50
|
teamId: 'team-1',
|
|
50
51
|
cwd: tempRoot,
|
|
@@ -78,6 +79,8 @@ describe('TeamOrchestrator', () => {
|
|
|
78
79
|
expect(runTask.mock.calls[1]?.[0].systemPrompt).toBeUndefined();
|
|
79
80
|
expect(runTask.mock.calls[1]?.[0].prompt).not.toContain('Assigned engineer: Tom.');
|
|
80
81
|
expect(runTask.mock.calls[1]?.[0].prompt).toContain('Implement the chosen fix');
|
|
82
|
+
// Hybrid workflow: implement mode must include a pre-implementation plan step
|
|
83
|
+
expect(runTask.mock.calls[1]?.[0].prompt).toContain('state a brief implementation plan');
|
|
81
84
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
82
85
|
expect(team.engineers.find((engineer) => engineer.name === 'Tom')).toMatchObject({
|
|
83
86
|
claudeSessionId: 'ses_tom',
|
|
@@ -301,7 +304,8 @@ describe('TeamOrchestrator', () => {
|
|
|
301
304
|
contextWindowSize: 200_000,
|
|
302
305
|
};
|
|
303
306
|
});
|
|
304
|
-
const
|
|
307
|
+
const store = new TeamStateStore('.state');
|
|
308
|
+
const orchestrator = new TeamOrchestrator({ runTask }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
305
309
|
const allEvents = [];
|
|
306
310
|
const result = await orchestrator.dispatchEngineer({
|
|
307
311
|
teamId: 'team-1',
|
|
@@ -339,6 +343,158 @@ describe('TeamOrchestrator', () => {
|
|
|
339
343
|
expect(error.message).toContain('BrowserQA is a browser QA specialist');
|
|
340
344
|
expect(error.message).toContain('does not support implement mode');
|
|
341
345
|
});
|
|
346
|
+
it('setActivePlan persists plan with slices on TeamRecord', async () => {
|
|
347
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
348
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
349
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
350
|
+
const activePlan = await orchestrator.setActivePlan(tempRoot, 'team-1', {
|
|
351
|
+
summary: 'Implement billing refactor in three slices',
|
|
352
|
+
taskSize: 'large',
|
|
353
|
+
slices: ['types + contracts', 'core logic', 'tests'],
|
|
354
|
+
preAuthorized: false,
|
|
355
|
+
});
|
|
356
|
+
expect(activePlan.id).toMatch(/^plan-/);
|
|
357
|
+
expect(activePlan.summary).toBe('Implement billing refactor in three slices');
|
|
358
|
+
expect(activePlan.taskSize).toBe('large');
|
|
359
|
+
expect(activePlan.preAuthorized).toBe(false);
|
|
360
|
+
expect(activePlan.slices).toHaveLength(3);
|
|
361
|
+
expect(activePlan.slices[0]).toMatchObject({
|
|
362
|
+
index: 0,
|
|
363
|
+
description: 'types + contracts',
|
|
364
|
+
status: 'pending',
|
|
365
|
+
});
|
|
366
|
+
expect(activePlan.slices[1]).toMatchObject({
|
|
367
|
+
index: 1,
|
|
368
|
+
description: 'core logic',
|
|
369
|
+
status: 'pending',
|
|
370
|
+
});
|
|
371
|
+
expect(activePlan.slices[2]).toMatchObject({
|
|
372
|
+
index: 2,
|
|
373
|
+
description: 'tests',
|
|
374
|
+
status: 'pending',
|
|
375
|
+
});
|
|
376
|
+
expect(activePlan.currentSliceIndex).toBe(0);
|
|
377
|
+
expect(activePlan.confirmedAt).not.toBeNull();
|
|
378
|
+
const retrieved = await orchestrator.getActivePlan(tempRoot, 'team-1');
|
|
379
|
+
expect(retrieved).toMatchObject({ id: activePlan.id, taskSize: 'large' });
|
|
380
|
+
});
|
|
381
|
+
it('clearActivePlan removes activePlan from TeamRecord', async () => {
|
|
382
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
383
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
384
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
385
|
+
await orchestrator.setActivePlan(tempRoot, 'team-1', {
|
|
386
|
+
summary: 'Small task',
|
|
387
|
+
taskSize: 'simple',
|
|
388
|
+
slices: [],
|
|
389
|
+
preAuthorized: false,
|
|
390
|
+
});
|
|
391
|
+
const beforeClear = await orchestrator.getActivePlan(tempRoot, 'team-1');
|
|
392
|
+
expect(beforeClear).not.toBeNull();
|
|
393
|
+
await orchestrator.clearActivePlan(tempRoot, 'team-1');
|
|
394
|
+
const afterClear = await orchestrator.getActivePlan(tempRoot, 'team-1');
|
|
395
|
+
expect(afterClear).toBeNull();
|
|
396
|
+
});
|
|
397
|
+
it('updateActivePlanSlice marks a slice done and advances currentSliceIndex', async () => {
|
|
398
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
399
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
400
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
401
|
+
await orchestrator.setActivePlan(tempRoot, 'team-1', {
|
|
402
|
+
summary: 'Two-slice task',
|
|
403
|
+
taskSize: 'large',
|
|
404
|
+
slices: ['slice A', 'slice B'],
|
|
405
|
+
preAuthorized: true,
|
|
406
|
+
});
|
|
407
|
+
await orchestrator.updateActivePlanSlice(tempRoot, 'team-1', 0, 'done');
|
|
408
|
+
const plan = await orchestrator.getActivePlan(tempRoot, 'team-1');
|
|
409
|
+
expect(plan).not.toBeNull();
|
|
410
|
+
expect(plan.slices[0]).toMatchObject({ index: 0, status: 'done' });
|
|
411
|
+
expect(plan.slices[0].completedAt).toBeDefined();
|
|
412
|
+
expect(plan.slices[1]).toMatchObject({ index: 1, status: 'pending' });
|
|
413
|
+
expect(plan.currentSliceIndex).toBe(1);
|
|
414
|
+
});
|
|
415
|
+
it('updateActivePlanSlice sets currentSliceIndex to null when the final slice is completed', async () => {
|
|
416
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
417
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
418
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
419
|
+
await orchestrator.setActivePlan(tempRoot, 'team-1', {
|
|
420
|
+
summary: 'Single-slice task',
|
|
421
|
+
taskSize: 'large',
|
|
422
|
+
slices: ['ship the feature'],
|
|
423
|
+
preAuthorized: true,
|
|
424
|
+
});
|
|
425
|
+
// Complete the only slice (index 0, which is also the last)
|
|
426
|
+
await orchestrator.updateActivePlanSlice(tempRoot, 'team-1', 0, 'done');
|
|
427
|
+
const plan = await orchestrator.getActivePlan(tempRoot, 'team-1');
|
|
428
|
+
expect(plan).not.toBeNull();
|
|
429
|
+
expect(plan.slices[0]).toMatchObject({ index: 0, status: 'done' });
|
|
430
|
+
expect(plan.currentSliceIndex).toBeNull();
|
|
431
|
+
});
|
|
432
|
+
it('setActivePlan sets currentSliceIndex to null when no slices are provided', async () => {
|
|
433
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
434
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
435
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
436
|
+
const plan = await orchestrator.setActivePlan(tempRoot, 'team-1', {
|
|
437
|
+
summary: 'Simple no-slice task',
|
|
438
|
+
taskSize: 'simple',
|
|
439
|
+
slices: [],
|
|
440
|
+
preAuthorized: false,
|
|
441
|
+
});
|
|
442
|
+
expect(plan.currentSliceIndex).toBeNull();
|
|
443
|
+
expect(plan.slices).toHaveLength(0);
|
|
444
|
+
});
|
|
445
|
+
it('updateActivePlanSlice throws when team has no active plan', async () => {
|
|
446
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
447
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
448
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
449
|
+
await expect(orchestrator.updateActivePlanSlice(tempRoot, 'team-1', 0, 'done')).rejects.toThrow('has no active plan');
|
|
450
|
+
});
|
|
451
|
+
it('updateActivePlanSlice throws when slice index does not exist', async () => {
|
|
452
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
453
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
454
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
455
|
+
await orchestrator.setActivePlan(tempRoot, 'team-1', {
|
|
456
|
+
summary: 'Two-slice task',
|
|
457
|
+
taskSize: 'large',
|
|
458
|
+
slices: ['slice A', 'slice B'],
|
|
459
|
+
preAuthorized: false,
|
|
460
|
+
});
|
|
461
|
+
// Index 5 does not exist (only 0 and 1 exist)
|
|
462
|
+
await expect(orchestrator.updateActivePlanSlice(tempRoot, 'team-1', 5, 'done')).rejects.toThrow('slice index 5 does not exist');
|
|
463
|
+
});
|
|
464
|
+
it('updateActivePlanSlice throws when active plan has no slices', async () => {
|
|
465
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
466
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
467
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
468
|
+
await orchestrator.setActivePlan(tempRoot, 'team-1', {
|
|
469
|
+
summary: 'No-slice plan',
|
|
470
|
+
taskSize: 'simple',
|
|
471
|
+
slices: [],
|
|
472
|
+
preAuthorized: false,
|
|
473
|
+
});
|
|
474
|
+
await expect(orchestrator.updateActivePlanSlice(tempRoot, 'team-1', 0, 'done')).rejects.toThrow('plan has no slices');
|
|
475
|
+
});
|
|
476
|
+
it('getActivePlan returns null for a new team with no active plan', async () => {
|
|
477
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
478
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
479
|
+
const plan = await orchestrator.getActivePlan(tempRoot, 'team-1');
|
|
480
|
+
expect(plan).toBeNull();
|
|
481
|
+
});
|
|
482
|
+
it('normalizeTeamRecord preserves activePlan from persisted records', async () => {
|
|
483
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
484
|
+
const store = new TeamStateStore('.state');
|
|
485
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
486
|
+
await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
487
|
+
await orchestrator.setActivePlan(tempRoot, 'team-1', {
|
|
488
|
+
summary: 'Persist test',
|
|
489
|
+
taskSize: 'simple',
|
|
490
|
+
slices: [],
|
|
491
|
+
preAuthorized: false,
|
|
492
|
+
});
|
|
493
|
+
// Re-read via getOrCreateTeam (triggers normalizeTeamRecord)
|
|
494
|
+
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
495
|
+
expect(team.activePlan).toBeDefined();
|
|
496
|
+
expect(team.activePlan.summary).toBe('Persist test');
|
|
497
|
+
});
|
|
342
498
|
it('selectPlanEngineers excludes BrowserQA from planner selection', async () => {
|
|
343
499
|
tempRoot = await mkdtemp(join(tmpdir(), 'planner-exclude-browserqa-'));
|
|
344
500
|
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
@@ -51,22 +51,4 @@ describe('TeamStateStore', () => {
|
|
|
51
51
|
{ id: 'older' },
|
|
52
52
|
]);
|
|
53
53
|
});
|
|
54
|
-
it('returns null for active team when no active-team.json exists', async () => {
|
|
55
|
-
tempRoot = await mkdtemp(join(tmpdir(), 'team-store-'));
|
|
56
|
-
const store = new TeamStateStore('.state');
|
|
57
|
-
await expect(store.getActiveTeam(tempRoot)).resolves.toBeNull();
|
|
58
|
-
});
|
|
59
|
-
it('persists and reads back the active team ID', async () => {
|
|
60
|
-
tempRoot = await mkdtemp(join(tmpdir(), 'team-store-'));
|
|
61
|
-
const store = new TeamStateStore('.state');
|
|
62
|
-
await store.setActiveTeam(tempRoot, 'team-abc');
|
|
63
|
-
await expect(store.getActiveTeam(tempRoot)).resolves.toBe('team-abc');
|
|
64
|
-
});
|
|
65
|
-
it('overwrites the active team ID on subsequent writes', async () => {
|
|
66
|
-
tempRoot = await mkdtemp(join(tmpdir(), 'team-store-'));
|
|
67
|
-
const store = new TeamStateStore('.state');
|
|
68
|
-
await store.setActiveTeam(tempRoot, 'team-first');
|
|
69
|
-
await store.setActiveTeam(tempRoot, 'team-second');
|
|
70
|
-
await expect(store.getActiveTeam(tempRoot)).resolves.toBe('team-second');
|
|
71
|
-
});
|
|
72
54
|
});
|
|
@@ -116,6 +116,24 @@ export interface SessionContextSnapshot {
|
|
|
116
116
|
warningLevel: ContextWarningLevel;
|
|
117
117
|
compactionCount: number;
|
|
118
118
|
}
|
|
119
|
+
export type TaskSize = 'trivial' | 'simple' | 'large';
|
|
120
|
+
export interface PlanSlice {
|
|
121
|
+
index: number;
|
|
122
|
+
description: string;
|
|
123
|
+
status: 'pending' | 'in_progress' | 'done' | 'skipped';
|
|
124
|
+
completedAt?: string;
|
|
125
|
+
}
|
|
126
|
+
export interface ActivePlan {
|
|
127
|
+
id: string;
|
|
128
|
+
summary: string;
|
|
129
|
+
taskSize: TaskSize;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
confirmedAt: string | null;
|
|
132
|
+
preAuthorized: boolean;
|
|
133
|
+
slices: PlanSlice[];
|
|
134
|
+
/** Null when the plan has no slices (trivial/simple tasks). */
|
|
135
|
+
currentSliceIndex: number | null;
|
|
136
|
+
}
|
|
119
137
|
export interface TeamEngineerRecord {
|
|
120
138
|
name: EngineerName;
|
|
121
139
|
wrapperSessionId: string | null;
|
|
@@ -142,6 +160,7 @@ export interface TeamRecord {
|
|
|
142
160
|
createdAt: string;
|
|
143
161
|
updatedAt: string;
|
|
144
162
|
engineers: TeamEngineerRecord[];
|
|
163
|
+
activePlan?: ActivePlan;
|
|
145
164
|
}
|
|
146
165
|
export interface EngineerTaskResult {
|
|
147
166
|
teamId: string;
|