@doingdev/opencode-claude-manager-plugin 0.1.46 → 0.1.47

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 (51) hide show
  1. package/README.md +29 -31
  2. package/dist/index.d.ts +1 -1
  3. package/dist/manager/team-orchestrator.d.ts +50 -0
  4. package/dist/manager/team-orchestrator.js +360 -0
  5. package/dist/plugin/agent-hierarchy.d.ts +12 -34
  6. package/dist/plugin/agent-hierarchy.js +36 -129
  7. package/dist/plugin/claude-manager.plugin.js +190 -423
  8. package/dist/plugin/service-factory.d.ts +18 -3
  9. package/dist/plugin/service-factory.js +32 -1
  10. package/dist/prompts/registry.d.ts +1 -10
  11. package/dist/prompts/registry.js +42 -261
  12. package/dist/src/claude/claude-agent-sdk-adapter.js +2 -1
  13. package/dist/src/claude/session-live-tailer.js +2 -2
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/manager/git-operations.d.ts +10 -1
  16. package/dist/src/manager/git-operations.js +18 -3
  17. package/dist/src/manager/persistent-manager.d.ts +18 -6
  18. package/dist/src/manager/persistent-manager.js +19 -13
  19. package/dist/src/manager/session-controller.d.ts +7 -10
  20. package/dist/src/manager/session-controller.js +12 -62
  21. package/dist/src/manager/team-orchestrator.d.ts +50 -0
  22. package/dist/src/manager/team-orchestrator.js +360 -0
  23. package/dist/src/plugin/agent-hierarchy.d.ts +12 -26
  24. package/dist/src/plugin/agent-hierarchy.js +36 -99
  25. package/dist/src/plugin/claude-manager.plugin.js +214 -393
  26. package/dist/src/plugin/service-factory.d.ts +18 -3
  27. package/dist/src/plugin/service-factory.js +33 -9
  28. package/dist/src/prompts/registry.d.ts +1 -10
  29. package/dist/src/prompts/registry.js +41 -246
  30. package/dist/src/state/team-state-store.d.ts +14 -0
  31. package/dist/src/state/team-state-store.js +85 -0
  32. package/dist/src/team/roster.d.ts +5 -0
  33. package/dist/src/team/roster.js +38 -0
  34. package/dist/src/types/contracts.d.ts +55 -13
  35. package/dist/src/types/contracts.js +1 -1
  36. package/dist/state/team-state-store.d.ts +14 -0
  37. package/dist/state/team-state-store.js +85 -0
  38. package/dist/team/roster.d.ts +5 -0
  39. package/dist/team/roster.js +38 -0
  40. package/dist/test/claude-manager.plugin.test.js +55 -280
  41. package/dist/test/git-operations.test.js +65 -1
  42. package/dist/test/persistent-manager.test.js +3 -3
  43. package/dist/test/prompt-registry.test.js +32 -252
  44. package/dist/test/session-controller.test.js +27 -27
  45. package/dist/test/team-orchestrator.test.d.ts +1 -0
  46. package/dist/test/team-orchestrator.test.js +146 -0
  47. package/dist/test/team-state-store.test.d.ts +1 -0
  48. package/dist/test/team-state-store.test.js +54 -0
  49. package/dist/types/contracts.d.ts +54 -3
  50. package/dist/types/contracts.js +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,146 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { TeamOrchestrator } from '../src/manager/team-orchestrator.js';
6
+ import { TeamStateStore } from '../src/state/team-state-store.js';
7
+ describe('TeamOrchestrator', () => {
8
+ let tempRoot;
9
+ afterEach(async () => {
10
+ if (tempRoot) {
11
+ await rm(tempRoot, { recursive: true, force: true });
12
+ }
13
+ });
14
+ it('dispatches work to a named engineer and persists the Claude session', async () => {
15
+ tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
16
+ const runTask = vi
17
+ .fn()
18
+ .mockResolvedValueOnce({
19
+ sessionId: 'ses_tom',
20
+ events: [{ type: 'result', text: 'done', turns: 1, totalCostUsd: 0.02 }],
21
+ finalText: 'Done.',
22
+ turns: 1,
23
+ totalCostUsd: 0.02,
24
+ inputTokens: 1000,
25
+ outputTokens: 200,
26
+ contextWindowSize: 200_000,
27
+ })
28
+ .mockResolvedValueOnce({
29
+ sessionId: 'ses_tom',
30
+ events: [{ type: 'result', text: 'done again', turns: 2, totalCostUsd: 0.03 }],
31
+ finalText: 'Done again.',
32
+ turns: 2,
33
+ totalCostUsd: 0.03,
34
+ inputTokens: 2000,
35
+ outputTokens: 300,
36
+ contextWindowSize: 200_000,
37
+ });
38
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', []);
39
+ const first = await orchestrator.dispatchEngineer({
40
+ teamId: 'team-1',
41
+ cwd: tempRoot,
42
+ engineer: 'Tom',
43
+ mode: 'explore',
44
+ message: 'Investigate the auth flow',
45
+ });
46
+ const second = await orchestrator.dispatchEngineer({
47
+ teamId: 'team-1',
48
+ cwd: tempRoot,
49
+ engineer: 'Tom',
50
+ mode: 'implement',
51
+ message: 'Implement the chosen fix',
52
+ });
53
+ expect(first.sessionId).toBe('ses_tom');
54
+ expect(second.sessionId).toBe('ses_tom');
55
+ expect(runTask.mock.calls[0]?.[0]).toMatchObject({
56
+ systemPrompt: expect.stringContaining('Assigned engineer: Tom.'),
57
+ resumeSessionId: undefined,
58
+ permissionMode: 'plan',
59
+ });
60
+ expect(runTask.mock.calls[1]?.[0]).toMatchObject({
61
+ systemPrompt: undefined,
62
+ resumeSessionId: 'ses_tom',
63
+ permissionMode: 'acceptEdits',
64
+ });
65
+ const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
66
+ expect(team.engineers.find((engineer) => engineer.name === 'Tom')).toMatchObject({
67
+ claudeSessionId: 'ses_tom',
68
+ busy: false,
69
+ lastMode: 'implement',
70
+ });
71
+ });
72
+ it('rejects work when the same engineer is already busy', async () => {
73
+ tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
74
+ const store = new TeamStateStore('.state');
75
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', []);
76
+ const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
77
+ await store.saveTeam({
78
+ ...team,
79
+ engineers: team.engineers.map((engineer) => engineer.name === 'Tom'
80
+ ? {
81
+ ...engineer,
82
+ busy: true,
83
+ }
84
+ : engineer),
85
+ });
86
+ await expect(orchestrator.dispatchEngineer({
87
+ teamId: 'team-1',
88
+ cwd: tempRoot,
89
+ engineer: 'Tom',
90
+ mode: 'explore',
91
+ message: 'Investigate again',
92
+ })).rejects.toThrow('Tom is already working on another assignment.');
93
+ });
94
+ it('creates two drafts and synthesizes them into one plan', async () => {
95
+ tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
96
+ const runTask = vi
97
+ .fn()
98
+ .mockResolvedValueOnce({
99
+ sessionId: 'ses_tom',
100
+ events: [],
101
+ finalText: '## Objective\nLead plan',
102
+ })
103
+ .mockResolvedValueOnce({
104
+ sessionId: 'ses_maya',
105
+ events: [],
106
+ finalText: '## Objective\nChallenger plan',
107
+ })
108
+ .mockResolvedValueOnce({
109
+ sessionId: undefined,
110
+ events: [],
111
+ finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
112
+ });
113
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', []);
114
+ const result = await orchestrator.planWithTeam({
115
+ teamId: 'team-1',
116
+ cwd: tempRoot,
117
+ request: 'Plan the billing refactor',
118
+ leadEngineer: 'Tom',
119
+ challengerEngineer: 'Maya',
120
+ });
121
+ expect(result.drafts).toHaveLength(2);
122
+ expect(result.synthesis).toBe('Combined plan');
123
+ expect(result.recommendedQuestion).toBe('Should we migrate now?');
124
+ expect(result.recommendedAnswer).toBe('No, defer it.');
125
+ expect(runTask).toHaveBeenCalledTimes(3);
126
+ });
127
+ it('persists wrapper session memory for an engineer', async () => {
128
+ tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
129
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', []);
130
+ await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
131
+ 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.');
132
+ const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
133
+ expect(team.engineers.find((engineer) => engineer.name === 'Tom')).toMatchObject({
134
+ wrapperSessionId: 'wrapper-tom',
135
+ lastMode: 'explore',
136
+ });
137
+ const wrapperContext = await orchestrator.getWrapperSystemContext(tempRoot, 'team-1', 'Tom');
138
+ expect(wrapperContext).toContain('Persistent wrapper memory for Tom');
139
+ expect(wrapperContext).toContain('assignment [explore]');
140
+ expect(wrapperContext).toContain('result [explore]');
141
+ await expect(orchestrator.findTeamByWrapperSession(tempRoot, 'wrapper-tom')).resolves.toEqual({
142
+ teamId: 'team-1',
143
+ engineer: 'Tom',
144
+ });
145
+ });
146
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { TeamStateStore } from '../src/state/team-state-store.js';
6
+ import { createEmptyTeamRecord } from '../src/team/roster.js';
7
+ describe('TeamStateStore', () => {
8
+ let tempRoot;
9
+ afterEach(async () => {
10
+ if (tempRoot) {
11
+ await rm(tempRoot, { recursive: true, force: true });
12
+ }
13
+ });
14
+ it('saves and reads a team record', async () => {
15
+ tempRoot = await mkdtemp(join(tmpdir(), 'team-store-'));
16
+ const store = new TeamStateStore('.state');
17
+ const team = createEmptyTeamRecord('team-1', tempRoot);
18
+ await store.saveTeam(team);
19
+ await expect(store.getTeam(tempRoot, 'team-1')).resolves.toEqual(team);
20
+ });
21
+ it('updates one engineer inside a team record', async () => {
22
+ tempRoot = await mkdtemp(join(tmpdir(), 'team-store-'));
23
+ const store = new TeamStateStore('.state');
24
+ const team = createEmptyTeamRecord('team-1', tempRoot);
25
+ await store.saveTeam(team);
26
+ const updated = await store.updateTeam(tempRoot, 'team-1', (existing) => ({
27
+ ...existing,
28
+ updatedAt: '2026-01-01T00:00:00.000Z',
29
+ engineers: existing.engineers.map((engineer) => engineer.name === 'Tom'
30
+ ? {
31
+ ...engineer,
32
+ claudeSessionId: 'ses_tom',
33
+ lastTaskSummary: 'Plan the feature',
34
+ }
35
+ : engineer),
36
+ }));
37
+ expect(updated.engineers.find((engineer) => engineer.name === 'Tom')?.claudeSessionId).toBe('ses_tom');
38
+ expect(updated.engineers.find((engineer) => engineer.name === 'John')?.claudeSessionId).toBe(null);
39
+ });
40
+ it('lists teams newest first', async () => {
41
+ tempRoot = await mkdtemp(join(tmpdir(), 'team-store-'));
42
+ const store = new TeamStateStore('.state');
43
+ const older = createEmptyTeamRecord('older', tempRoot);
44
+ older.updatedAt = '2026-01-01T00:00:00.000Z';
45
+ const newer = createEmptyTeamRecord('newer', tempRoot);
46
+ newer.updatedAt = '2026-01-02T00:00:00.000Z';
47
+ await store.saveTeam(older);
48
+ await store.saveTeam(newer);
49
+ await expect(store.listTeams(tempRoot)).resolves.toMatchObject([
50
+ { id: 'newer' },
51
+ { id: 'older' },
52
+ ]);
53
+ });
54
+ });
@@ -1,8 +1,6 @@
1
1
  export interface ManagerPromptRegistry {
2
2
  ctoSystemPrompt: string;
3
- engineerExplorePrompt: string;
4
- engineerImplementPrompt: string;
5
- engineerVerifyPrompt: string;
3
+ engineerAgentPrompt: string;
6
4
  engineerSessionPrompt: string;
7
5
  modePrefixes: {
8
6
  plan: string;
@@ -15,6 +13,15 @@ export interface ManagerPromptRegistry {
15
13
  };
16
14
  }
17
15
  export type SessionMode = 'plan' | 'free';
16
+ export declare const DEFAULT_ENGINEER_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
17
+ export type EngineerName = (typeof DEFAULT_ENGINEER_NAMES)[number];
18
+ export type EngineerWorkMode = 'explore' | 'implement' | 'verify';
19
+ export interface WrapperHistoryEntry {
20
+ timestamp: string;
21
+ type: 'assignment' | 'result';
22
+ mode?: EngineerWorkMode;
23
+ text: string;
24
+ }
18
25
  export interface ClaudeCommandMetadata {
19
26
  name: string;
20
27
  description: string;
@@ -98,6 +105,50 @@ export interface SessionContextSnapshot {
98
105
  warningLevel: ContextWarningLevel;
99
106
  compactionCount: number;
100
107
  }
108
+ export interface TeamEngineerRecord {
109
+ name: EngineerName;
110
+ wrapperSessionId: string | null;
111
+ claudeSessionId: string | null;
112
+ busy: boolean;
113
+ lastMode: EngineerWorkMode | null;
114
+ lastTaskSummary: string | null;
115
+ lastUsedAt: string | null;
116
+ wrapperHistory: WrapperHistoryEntry[];
117
+ context: SessionContextSnapshot;
118
+ }
119
+ export interface TeamRecord {
120
+ id: string;
121
+ cwd: string;
122
+ createdAt: string;
123
+ updatedAt: string;
124
+ engineers: TeamEngineerRecord[];
125
+ }
126
+ export interface EngineerTaskResult {
127
+ teamId: string;
128
+ engineer: EngineerName;
129
+ mode: EngineerWorkMode;
130
+ sessionId?: string;
131
+ finalText: string;
132
+ turns?: number;
133
+ totalCostUsd?: number;
134
+ inputTokens?: number;
135
+ outputTokens?: number;
136
+ contextWindowSize?: number;
137
+ context: SessionContextSnapshot;
138
+ }
139
+ export interface PlanDraft extends EngineerTaskResult {
140
+ request: string;
141
+ }
142
+ export interface SynthesizedPlanResult {
143
+ teamId: string;
144
+ request: string;
145
+ leadEngineer: EngineerName;
146
+ challengerEngineer: EngineerName;
147
+ drafts: [PlanDraft, PlanDraft];
148
+ synthesis: string;
149
+ recommendedQuestion: string | null;
150
+ recommendedAnswer: string | null;
151
+ }
101
152
  export interface GitDiffResult {
102
153
  hasDiff: boolean;
103
154
  diffText: string;
@@ -1 +1 @@
1
- export {};
1
+ export const DEFAULT_ENGINEER_NAMES = ['Tom', 'John', 'Maya', 'Sara', 'Alex'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",