@doingdev/opencode-claude-manager-plugin 0.1.44 → 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 (53) hide show
  1. package/README.md +29 -31
  2. package/dist/index.d.ts +1 -1
  3. package/dist/manager/persistent-manager.d.ts +3 -23
  4. package/dist/manager/persistent-manager.js +2 -95
  5. package/dist/manager/team-orchestrator.d.ts +50 -0
  6. package/dist/manager/team-orchestrator.js +360 -0
  7. package/dist/plugin/agent-hierarchy.d.ts +12 -34
  8. package/dist/plugin/agent-hierarchy.js +36 -129
  9. package/dist/plugin/claude-manager.plugin.js +190 -445
  10. package/dist/plugin/service-factory.d.ts +18 -3
  11. package/dist/plugin/service-factory.js +32 -1
  12. package/dist/prompts/registry.d.ts +1 -10
  13. package/dist/prompts/registry.js +42 -270
  14. package/dist/src/claude/claude-agent-sdk-adapter.js +2 -1
  15. package/dist/src/claude/session-live-tailer.js +2 -2
  16. package/dist/src/index.d.ts +1 -1
  17. package/dist/src/manager/git-operations.d.ts +10 -1
  18. package/dist/src/manager/git-operations.js +18 -3
  19. package/dist/src/manager/persistent-manager.d.ts +18 -6
  20. package/dist/src/manager/persistent-manager.js +19 -13
  21. package/dist/src/manager/session-controller.d.ts +7 -10
  22. package/dist/src/manager/session-controller.js +12 -62
  23. package/dist/src/manager/team-orchestrator.d.ts +50 -0
  24. package/dist/src/manager/team-orchestrator.js +360 -0
  25. package/dist/src/plugin/agent-hierarchy.d.ts +12 -26
  26. package/dist/src/plugin/agent-hierarchy.js +36 -99
  27. package/dist/src/plugin/claude-manager.plugin.js +214 -393
  28. package/dist/src/plugin/service-factory.d.ts +18 -3
  29. package/dist/src/plugin/service-factory.js +33 -9
  30. package/dist/src/prompts/registry.d.ts +1 -10
  31. package/dist/src/prompts/registry.js +41 -246
  32. package/dist/src/state/team-state-store.d.ts +14 -0
  33. package/dist/src/state/team-state-store.js +85 -0
  34. package/dist/src/team/roster.d.ts +5 -0
  35. package/dist/src/team/roster.js +38 -0
  36. package/dist/src/types/contracts.d.ts +55 -13
  37. package/dist/src/types/contracts.js +1 -1
  38. package/dist/state/team-state-store.d.ts +14 -0
  39. package/dist/state/team-state-store.js +85 -0
  40. package/dist/team/roster.d.ts +5 -0
  41. package/dist/team/roster.js +38 -0
  42. package/dist/test/claude-manager.plugin.test.js +55 -280
  43. package/dist/test/git-operations.test.js +65 -1
  44. package/dist/test/persistent-manager.test.js +3 -3
  45. package/dist/test/prompt-registry.test.js +32 -252
  46. package/dist/test/session-controller.test.js +27 -27
  47. package/dist/test/team-orchestrator.test.d.ts +1 -0
  48. package/dist/test/team-orchestrator.test.js +146 -0
  49. package/dist/test/team-state-store.test.d.ts +1 -0
  50. package/dist/test/team-state-store.test.js +54 -0
  51. package/dist/types/contracts.d.ts +50 -23
  52. package/dist/types/contracts.js +1 -1
  53. package/package.json +1 -1
@@ -1,333 +1,102 @@
1
1
  import { tool } from '@opencode-ai/plugin';
2
- import { composeWrapperPrompt, managerPromptRegistry } from '../prompts/registry.js';
2
+ import { managerPromptRegistry } from '../prompts/registry.js';
3
3
  import { discoverProjectClaudeFiles } from '../util/project-context.js';
4
- import { AGENT_CTO, AGENT_ENGINEER_EXPLORE, AGENT_ENGINEER_IMPLEMENT, AGENT_ENGINEER_VERIFY, buildCtoAgentConfig, buildEngineerExploreAgentConfig, buildEngineerImplementAgentConfig, buildEngineerVerifyAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
5
- import { getOrCreatePluginServices } from './service-factory.js';
4
+ import { AGENT_CTO, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
5
+ import { getActiveTeamSession, getOrCreatePluginServices, getWrapperSessionMapping, setActiveTeamSession, setWrapperSessionMapping, } from './service-factory.js';
6
+ import { isEngineerName } from '../team/roster.js';
7
+ const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
8
+ const MODE_ENUM = ['explore', 'implement', 'verify'];
6
9
  export const ClaudeManagerPlugin = async ({ worktree }) => {
7
- const services = getOrCreatePluginServices(worktree);
8
- async function executeDelegate(args, context) {
9
- const wrapperServices = getOrCreatePluginServices(context.worktree);
10
- if (args.freshSession) {
11
- await wrapperServices.manager.clearSession();
12
- }
13
- const managerStatus = wrapperServices.manager.getStatus();
14
- const hasActiveSession = managerStatus.context.sessionId !== null;
15
- const promptPreview = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
16
- context.metadata({
17
- title: hasActiveSession
18
- ? '⚡ Claude Code: Resuming session...'
19
- : '⚡ Claude Code: Initializing...',
20
- metadata: {
21
- status: 'running',
22
- sessionId: managerStatus.context.sessionId,
23
- prompt: promptPreview,
24
- },
25
- });
26
- let turnsSoFar;
27
- let costSoFar;
28
- const result = await wrapperServices.manager.sendMessage(context.worktree, args.message, {
29
- model: args.model,
30
- effort: args.effort,
31
- mode: args.mode,
32
- sessionSystemPrompt: args.sessionSystemPrompt,
33
- abortSignal: context.abort,
34
- }, (event) => {
35
- if (event.turns !== undefined) {
36
- turnsSoFar = event.turns;
37
- }
38
- if (event.totalCostUsd !== undefined) {
39
- costSoFar = event.totalCostUsd;
40
- }
41
- const usageSuffix = formatLiveUsage(turnsSoFar, costSoFar);
42
- if (event.type === 'tool_call') {
43
- let toolName = 'tool';
44
- let inputPreview = '';
45
- try {
46
- const parsed = JSON.parse(event.text);
47
- toolName = parsed.name ?? 'tool';
48
- if (parsed.input) {
49
- const inputStr = typeof parsed.input === 'string' ? parsed.input : JSON.stringify(parsed.input);
50
- inputPreview = inputStr.length > 150 ? inputStr.slice(0, 150) + '...' : inputStr;
51
- }
52
- }
53
- catch {
54
- // ignore parse errors
55
- }
56
- context.metadata({
57
- title: `⚡ Claude Code: Running ${toolName}...${usageSuffix}`,
58
- metadata: {
59
- status: 'running',
60
- sessionId: event.sessionId,
61
- type: event.type,
62
- tool: toolName,
63
- input: inputPreview,
64
- },
65
- });
66
- }
67
- else if (event.type === 'assistant') {
68
- const thinkingPreview = event.text.length > 150 ? event.text.slice(0, 150) + '...' : event.text;
69
- context.metadata({
70
- title: `⚡ Claude Code: Thinking...${usageSuffix}`,
71
- metadata: {
72
- status: 'running',
73
- sessionId: event.sessionId,
74
- type: event.type,
75
- thinking: thinkingPreview,
76
- },
77
- });
78
- }
79
- else if (event.type === 'init') {
80
- context.metadata({
81
- title: `⚡ Claude Code: Session started`,
82
- metadata: {
83
- status: 'running',
84
- sessionId: event.sessionId,
85
- prompt: promptPreview,
86
- },
87
- });
88
- }
89
- else if (event.type === 'user') {
90
- const preview = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
91
- const outputPreview = formatToolOutputPreview(event.text);
92
- context.metadata({
93
- title: `⚡ Claude Code: ${outputPreview}${usageSuffix}`,
94
- metadata: {
95
- status: 'running',
96
- sessionId: event.sessionId,
97
- type: event.type,
98
- output: preview,
99
- },
100
- });
101
- }
102
- else if (event.type === 'tool_progress') {
103
- let toolName = 'tool';
104
- let elapsed = 0;
105
- let progressCurrent;
106
- let progressTotal;
107
- try {
108
- const parsed = JSON.parse(event.text);
109
- toolName = parsed.name ?? 'tool';
110
- elapsed = parsed.elapsed ?? 0;
111
- progressCurrent = parsed.current;
112
- progressTotal = parsed.total;
113
- }
114
- catch {
115
- // ignore
116
- }
117
- const progressInfo = progressCurrent !== undefined && progressTotal !== undefined
118
- ? ` [${progressCurrent}/${progressTotal}]`
119
- : '';
120
- context.metadata({
121
- title: `⚡ Claude Code: ${toolName} running ${elapsed > 0 ? `(${elapsed.toFixed(0)}s)` : ''}${progressInfo}...${usageSuffix}`,
122
- metadata: {
123
- status: 'running',
124
- sessionId: event.sessionId,
125
- type: event.type,
126
- tool: toolName,
127
- elapsed,
128
- },
129
- });
10
+ const claudeFiles = await discoverProjectClaudeFiles(worktree);
11
+ const services = getOrCreatePluginServices(worktree, claudeFiles);
12
+ return {
13
+ config: async (config) => {
14
+ config.agent ??= {};
15
+ config.permission ??= {};
16
+ denyRestrictedToolsGlobally(config.permission);
17
+ config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
18
+ for (const engineer of ENGINEER_AGENT_NAMES) {
19
+ config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
130
20
  }
131
- else if (event.type === 'tool_summary') {
132
- const summary = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
133
- context.metadata({
134
- title: `✅ Claude Code: Tool done${usageSuffix}`,
135
- metadata: {
136
- status: 'success',
137
- sessionId: event.sessionId,
138
- type: event.type,
139
- summary,
140
- },
141
- });
21
+ },
22
+ 'chat.message': async (input) => {
23
+ if (input.agent === AGENT_CTO) {
24
+ setActiveTeamSession(worktree, input.sessionID);
25
+ return;
142
26
  }
143
- else if (event.type === 'partial') {
144
- const delta = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
145
- context.metadata({
146
- title: `⚡ Claude Code: Writing...${usageSuffix}`,
147
- metadata: {
148
- status: 'running',
149
- sessionId: event.sessionId,
150
- type: event.type,
151
- delta,
152
- },
27
+ if (input.agent && isEngineerAgent(input.agent)) {
28
+ const engineer = engineerFromAgent(input.agent);
29
+ const existing = getWrapperSessionMapping(worktree, input.sessionID);
30
+ const persisted = existing ??
31
+ (await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
32
+ const teamId = persisted?.teamId ?? getActiveTeamSession(worktree) ?? input.sessionID;
33
+ setWrapperSessionMapping(worktree, input.sessionID, {
34
+ teamId,
35
+ engineer,
153
36
  });
37
+ await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
154
38
  }
155
- else if (event.type === 'error') {
156
- context.metadata({
157
- title: `❌ Claude Code: Error`,
158
- metadata: {
159
- status: 'error',
160
- sessionId: event.sessionId,
161
- error: event.text.slice(0, 200),
162
- },
163
- });
164
- showToastIfAvailable(context, `Claude Code error: ${event.text.slice(0, 100)}`);
39
+ },
40
+ 'experimental.chat.system.transform': async (input, output) => {
41
+ if (!input.sessionID) {
42
+ return;
165
43
  }
166
- });
167
- const costLabel = `$${(result.totalCostUsd ?? 0).toFixed(4)}`;
168
- const turns = result.turns ?? 0;
169
- const contextWarning = formatContextWarning(result.context);
170
- if (contextWarning) {
171
- context.metadata({
172
- title: `⚠️ Claude Code: Context at ${result.context.estimatedContextPercent}% (${turns} turns)`,
173
- metadata: { status: 'warning', sessionId: result.sessionId, contextWarning },
174
- });
175
- showToastIfAvailable(context, `⚠️ Context usage at ${result.context.estimatedContextPercent}% — consider compacting`);
176
- }
177
- else {
178
- context.metadata({
179
- title: `✅ Claude Code: Complete (${turns} turns, ${costLabel})`,
180
- metadata: { status: 'success', sessionId: result.sessionId },
181
- });
182
- showToastIfAvailable(context, `✅ Session complete (${turns} turns, ${costLabel})`);
183
- }
184
- let toolOutputs = [];
185
- if (result.sessionId) {
186
- try {
187
- toolOutputs = await wrapperServices.liveTailer.getToolOutputPreview(result.sessionId, context.worktree, 3);
44
+ const existing = getWrapperSessionMapping(worktree, input.sessionID);
45
+ const persisted = existing ??
46
+ (await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
47
+ if (!persisted) {
48
+ return;
188
49
  }
189
- catch {
190
- // Non-critical — the JSONL file may not exist yet.
50
+ const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree, persisted.teamId, persisted.engineer);
51
+ if (wrapperContext) {
52
+ output.system.push(wrapperContext);
191
53
  }
192
- }
193
- return JSON.stringify({
194
- sessionId: result.sessionId,
195
- finalText: result.finalText,
196
- turns: result.turns,
197
- totalCostUsd: result.totalCostUsd,
198
- inputTokens: result.inputTokens,
199
- outputTokens: result.outputTokens,
200
- contextWindowSize: result.contextWindowSize,
201
- context: result.context,
202
- contextWarning,
203
- toolOutputs: toolOutputs.length > 0 ? toolOutputs : undefined,
204
- }, null, 2);
205
- }
206
- return {
207
- config: async (config) => {
208
- config.agent ??= {};
209
- config.permission ??= {};
210
- denyRestrictedToolsGlobally(config.permission);
211
- // Discover project Claude files and build derived wrapper prompts.
212
- const claudeFiles = await discoverProjectClaudeFiles(worktree);
213
- const derivedPrompts = {
214
- ...managerPromptRegistry,
215
- engineerExplorePrompt: composeWrapperPrompt(managerPromptRegistry.engineerExplorePrompt, claudeFiles),
216
- engineerImplementPrompt: composeWrapperPrompt(managerPromptRegistry.engineerImplementPrompt, claudeFiles),
217
- engineerVerifyPrompt: composeWrapperPrompt(managerPromptRegistry.engineerVerifyPrompt, claudeFiles),
218
- };
219
- config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
220
- config.agent[AGENT_ENGINEER_EXPLORE] ??= buildEngineerExploreAgentConfig(derivedPrompts);
221
- config.agent[AGENT_ENGINEER_IMPLEMENT] ??= buildEngineerImplementAgentConfig(derivedPrompts);
222
- config.agent[AGENT_ENGINEER_VERIFY] ??= buildEngineerVerifyAgentConfig(derivedPrompts);
223
54
  },
224
55
  tool: {
225
- explore: tool({
226
- description: 'Investigate and analyze code without making edits. ' +
227
- 'Read-only exploration of the codebase. ' +
228
- 'Preferred first step before implementation.',
56
+ claude: tool({
57
+ description: "Run work through this named engineer's persistent Claude Code session. The session remembers prior turns for this engineer.",
229
58
  args: {
59
+ mode: tool.schema.enum(MODE_ENUM),
230
60
  message: tool.schema.string().min(1),
231
- model: tool.schema
232
- .enum(['claude-opus-4-6', 'claude-sonnet-4-6'])
233
- .optional(),
234
- effort: tool.schema.enum(['medium', 'high', 'max']).default('high'),
235
- freshSession: tool.schema.boolean().default(false),
236
- sessionSystemPrompt: tool.schema.string().optional(),
61
+ model: tool.schema.enum(MODEL_ENUM).optional(),
237
62
  },
238
63
  async execute(args, context) {
239
- const taskDesc = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
240
- await services.manager.acquireSlot('explore', taskDesc);
241
- try {
242
- return await executeDelegate({ ...args, mode: 'plan', wrapperType: 'explore' }, context);
243
- }
244
- finally {
245
- services.manager.releaseSlot('explore', taskDesc);
246
- }
247
- },
248
- }),
249
- verify: tool({
250
- description: 'Run verification commands (tests, lint, typecheck, build) and report pass/fail. ' +
251
- 'Use after implementation to verify correctness.',
252
- args: {
253
- message: tool.schema.string().min(1),
254
- model: tool.schema
255
- .enum(['claude-opus-4-6', 'claude-sonnet-4-6'])
256
- .optional(),
257
- effort: tool.schema.enum(['medium', 'high', 'max']).default('high'),
258
- freshSession: tool.schema.boolean().default(false),
259
- sessionSystemPrompt: tool.schema.string().optional(),
260
- },
261
- async execute(args, context) {
262
- const taskDesc = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
263
- await services.manager.acquireSlot('verify', taskDesc);
264
- try {
265
- return await executeDelegate({ ...args, mode: 'free', wrapperType: 'engineer_verify' }, context);
266
- }
267
- finally {
268
- services.manager.releaseSlot('verify', taskDesc);
269
- }
270
- },
271
- }),
272
- implement: tool({
273
- description: 'Implement code changes - can read, edit, and create files. ' +
274
- 'Use after exploration to make changes.',
275
- args: {
276
- message: tool.schema.string().min(1),
277
- model: tool.schema
278
- .enum(['claude-opus-4-6', 'claude-sonnet-4-6'])
279
- .optional(),
280
- effort: tool.schema.enum(['medium', 'high', 'max']).default('high'),
281
- freshSession: tool.schema.boolean().default(false),
282
- sessionSystemPrompt: tool.schema.string().optional(),
283
- },
284
- async execute(args, context) {
285
- const taskDesc = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
286
- await services.manager.acquireSlot('implement', taskDesc);
287
- try {
288
- return await executeDelegate({ ...args, mode: 'free', wrapperType: 'implement' }, context);
289
- }
290
- finally {
291
- services.manager.releaseSlot('implement', taskDesc);
292
- }
64
+ const engineer = engineerFromAgent(context.agent);
65
+ const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
66
+ const persisted = existing ??
67
+ (await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
68
+ const teamId = persisted?.teamId ?? getActiveTeamSession(context.worktree) ?? context.sessionID;
69
+ setWrapperSessionMapping(context.worktree, context.sessionID, {
70
+ teamId,
71
+ engineer,
72
+ });
73
+ await services.orchestrator.recordWrapperSession(context.worktree, teamId, engineer, context.sessionID);
74
+ const result = await runEngineerAssignment({
75
+ teamId,
76
+ engineer,
77
+ mode: args.mode,
78
+ message: args.message,
79
+ model: args.model,
80
+ }, context);
81
+ return JSON.stringify(result, null, 2);
293
82
  },
294
83
  }),
295
- compact_context: tool({
296
- description: 'Compress session history to reclaim context window space. ' +
297
- 'Preserves state while reducing token usage.',
84
+ team_status: tool({
85
+ description: 'Show the current CTO team state: named engineers, wrapper session IDs, Claude session IDs, busy flags, wrapper memory, and context snapshots.',
298
86
  args: {
299
- wrapperType: tool.schema.string().optional(),
87
+ teamId: tool.schema.string().optional(),
300
88
  },
301
89
  async execute(args, context) {
302
- const wrapperServices = getOrCreatePluginServices(context.worktree);
303
- annotateToolRun(context, 'Compacting session', {});
304
- const result = await wrapperServices.manager.compactSession(context.worktree);
305
- const snap = wrapperServices.manager.getStatus();
306
- const contextWarning = formatContextWarning(snap.context);
307
- context.metadata({
308
- title: contextWarning
309
- ? `⚠️ Claude Code: Compacted — context at ${snap.context.estimatedContextPercent}%`
310
- : `✅ Claude Code: Compacted (${snap.context.totalTurns} turns, $${(snap.context.totalCostUsd ?? 0).toFixed(4)})`,
311
- metadata: {
312
- status: contextWarning ? 'warning' : 'success',
313
- sessionId: result.sessionId,
314
- },
90
+ const teamId = args.teamId ?? getActiveTeamSession(context.worktree) ?? context.sessionID;
91
+ annotateToolRun(context, 'Reading team status', {
92
+ teamId,
315
93
  });
316
- return JSON.stringify({
317
- sessionId: result.sessionId,
318
- finalText: result.finalText,
319
- turns: result.turns,
320
- totalCostUsd: result.totalCostUsd,
321
- context: snap.context,
322
- contextWarning,
323
- }, null, 2);
94
+ const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
95
+ return JSON.stringify(team, null, 2);
324
96
  },
325
97
  }),
326
98
  git_diff: tool({
327
- description: 'Show diff of uncommitted changes. ' +
328
- 'Use paths to filter to specific files/dirs. ' +
329
- 'Use staged=true to see staged changes. ' +
330
- 'Use ref to compare against a branch/tag/commit (e.g., ref="main").',
99
+ description: 'Show diff of uncommitted changes. Use paths to filter to specific files or use ref to compare against another branch, tag, or commit.',
331
100
  args: {
332
101
  paths: tool.schema.string().array().optional(),
333
102
  staged: tool.schema.boolean().optional(),
@@ -339,9 +108,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
339
108
  staged: args.staged,
340
109
  ref: args.ref,
341
110
  });
342
- const paths = args.paths?.filter((p) => p !== undefined);
343
111
  const result = await services.manager.gitDiff({
344
- paths,
112
+ paths: args.paths?.filter((path) => path !== undefined),
345
113
  staged: args.staged,
346
114
  ref: args.ref,
347
115
  });
@@ -349,20 +117,18 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
349
117
  },
350
118
  }),
351
119
  git_commit: tool({
352
- description: 'Stage all changes and commit with the given message.',
120
+ description: 'Stage all changes and create a commit with the given message.',
353
121
  args: {
354
122
  message: tool.schema.string().min(1),
355
123
  },
356
124
  async execute(args, context) {
357
- annotateToolRun(context, 'Committing changes', {
358
- message: args.message,
359
- });
125
+ annotateToolRun(context, 'Committing changes', { message: args.message });
360
126
  const result = await services.manager.gitCommit(args.message);
361
127
  return JSON.stringify(result, null, 2);
362
128
  },
363
129
  }),
364
130
  git_reset: tool({
365
- description: 'Discard all uncommitted changes: runs git reset --hard HEAD and git clean -fd.',
131
+ description: 'Discard all uncommitted changes by running git reset --hard HEAD and git clean -fd.',
366
132
  args: {},
367
133
  async execute(_args, context) {
368
134
  annotateToolRun(context, 'Resetting working directory', {});
@@ -371,8 +137,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
371
137
  },
372
138
  }),
373
139
  git_status: tool({
374
- description: 'Show working tree status lists changed files in short format. ' +
375
- 'Returns isClean=true if nothing changed.',
140
+ description: 'Show working tree status in short format and whether the tree is clean.',
376
141
  args: {},
377
142
  async execute(_args, context) {
378
143
  annotateToolRun(context, 'Checking git status', {});
@@ -381,93 +146,53 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
381
146
  },
382
147
  }),
383
148
  git_log: tool({
384
- description: 'Show recent commits in short format. ' +
385
- 'Default shows last 5 commits. Use count to change.',
149
+ description: 'Show recent commits in short format. Defaults to 5 commits.',
386
150
  args: {
387
151
  count: tool.schema.number().optional(),
388
152
  },
389
153
  async execute(args, context) {
390
154
  annotateToolRun(context, 'Fetching git log', { count: args.count });
391
- const result = await services.manager.gitLog(args.count ?? 5);
392
- return result;
393
- },
394
- }),
395
- clear_session: tool({
396
- description: 'Clear the active session to start fresh. ' +
397
- 'Use when context is full or starting a new task.',
398
- args: {
399
- wrapperType: tool.schema.string().optional(),
400
- reason: tool.schema.string().optional(),
401
- },
402
- async execute(args, context) {
403
- const wrapperServices = getOrCreatePluginServices(context.worktree);
404
- annotateToolRun(context, 'Clearing session', {
405
- reason: args.reason,
406
- });
407
- const clearedId = await wrapperServices.manager.clearSession();
408
- return JSON.stringify({ clearedSessionId: clearedId });
409
- },
410
- }),
411
- session_health: tool({
412
- description: 'Check session health metrics: context usage %, turn count, cost, and session ID.',
413
- args: {
414
- wrapperType: tool.schema.string().optional(),
415
- },
416
- async execute(args, context) {
417
- const wrapperServices = getOrCreatePluginServices(context.worktree);
418
- annotateToolRun(context, 'Checking session status', {});
419
- const status = wrapperServices.manager.getStatus();
420
- return JSON.stringify({
421
- ...status,
422
- transcriptFile: status.context.sessionId
423
- ? `.claude-manager/transcripts/${status.context.sessionId}.json`
424
- : null,
425
- contextWarning: formatContextWarning(status.context),
426
- }, null, 2);
155
+ return services.manager.gitLog(args.count ?? 5);
427
156
  },
428
157
  }),
429
158
  list_transcripts: tool({
430
- description: 'List available session transcripts or inspect a specific transcript by ID.',
159
+ description: 'List available Claude session transcripts or inspect one transcript by session ID.',
431
160
  args: {
432
- wrapperType: tool.schema.string().optional(),
433
161
  sessionId: tool.schema.string().optional(),
434
162
  },
435
163
  async execute(args, context) {
436
- const wrapperServices = getOrCreatePluginServices(context.worktree);
437
164
  annotateToolRun(context, 'Inspecting Claude session history', {});
438
165
  if (args.sessionId) {
439
166
  const [sdkTranscript, localEvents] = await Promise.all([
440
- wrapperServices.sessions.getTranscript(args.sessionId, context.worktree),
441
- wrapperServices.manager.getTranscriptEvents(context.worktree, args.sessionId),
167
+ services.sessions.getTranscript(args.sessionId, context.worktree),
168
+ services.manager.getTranscriptEvents(context.worktree, args.sessionId),
442
169
  ]);
443
170
  return JSON.stringify({
444
171
  sdkTranscript,
445
172
  localEvents: localEvents.length > 0 ? localEvents : undefined,
446
173
  }, null, 2);
447
174
  }
448
- const sessions = await wrapperServices.sessions.listSessions(context.worktree);
175
+ const sessions = await services.sessions.listSessions(context.worktree);
449
176
  return JSON.stringify(sessions, null, 2);
450
177
  },
451
178
  }),
452
179
  list_history: tool({
453
- description: 'List persistent run records from the manager or inspect a specific run.',
180
+ description: 'List saved CTO teams for this worktree or inspect one team by ID.',
454
181
  args: {
455
- wrapperType: tool.schema.string().optional(),
456
- runId: tool.schema.string().optional(),
182
+ teamId: tool.schema.string().optional(),
457
183
  },
458
184
  async execute(args, context) {
459
- const wrapperServices = getOrCreatePluginServices(context.worktree);
460
- annotateToolRun(context, 'Reading manager run state', {});
461
- if (args.runId) {
462
- const run = await wrapperServices.manager.getRun(context.worktree, args.runId);
463
- return JSON.stringify(run, null, 2);
185
+ annotateToolRun(context, 'Reading saved team history', {});
186
+ if (args.teamId) {
187
+ const team = await services.teamStore.getTeam(context.worktree, args.teamId);
188
+ return JSON.stringify(team, null, 2);
464
189
  }
465
- const runs = await wrapperServices.manager.listRuns(context.worktree);
466
- return JSON.stringify(runs, null, 2);
190
+ const teams = await services.orchestrator.listTeams(context.worktree);
191
+ return JSON.stringify(teams, null, 2);
467
192
  },
468
193
  }),
469
194
  approval_policy: tool({
470
- description: 'View the current tool approval policy: rules, default action, and enabled status.',
195
+ description: 'View the current tool approval policy.',
471
196
  args: {},
472
197
  async execute(_args, context) {
473
198
  annotateToolRun(context, 'Reading approval policy', {});
@@ -475,8 +200,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
475
200
  },
476
201
  }),
477
202
  approval_decisions: tool({
478
- description: 'View recent tool approval decisions. Shows what tools were allowed or denied. ' +
479
- 'Use deniedOnly to see only denied calls.',
203
+ description: 'View recent tool approval decisions. Use deniedOnly to show only denied calls.',
480
204
  args: {
481
205
  limit: tool.schema.number().optional(),
482
206
  deniedOnly: tool.schema.boolean().optional(),
@@ -490,8 +214,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
490
214
  },
491
215
  }),
492
216
  approval_update: tool({
493
- description: 'Update the tool approval policy. Add/remove rules, change default action, or enable/disable. ' +
494
- 'Rules are evaluated top-to-bottom; first match wins.',
217
+ description: 'Update the tool approval policy. Add or remove rules, change the default action, enable or disable approvals, or clear decision history.',
495
218
  args: {
496
219
  action: tool.schema.enum([
497
220
  'addRule',
@@ -532,13 +255,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
532
255
  return JSON.stringify({ error: 'removeRule requires ruleId' });
533
256
  }
534
257
  const removed = services.approvalManager.removeRule(args.ruleId);
535
- return JSON.stringify({ removed });
258
+ return JSON.stringify({ removed }, null, 2);
536
259
  }
537
260
  else if (args.action === 'setDefault') {
538
261
  if (!args.defaultAction) {
539
- return JSON.stringify({
540
- error: 'setDefault requires defaultAction',
541
- });
262
+ return JSON.stringify({ error: 'setDefault requires defaultAction' });
542
263
  }
543
264
  services.approvalManager.setDefaultAction(args.defaultAction);
544
265
  }
@@ -557,83 +278,107 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
557
278
  },
558
279
  };
559
280
  };
560
- function annotateToolRun(context, title, metadata, status) {
561
- const emoji = status ? `${formatStatusEmoji(status)} ` : '';
281
+ async function runEngineerAssignment(input, context) {
282
+ const services = getOrCreatePluginServices(context.worktree);
283
+ annotateToolRun(context, `Assigning ${input.engineer}`, {
284
+ teamId: input.teamId,
285
+ mode: input.mode,
286
+ });
287
+ const result = await services.orchestrator.dispatchEngineer({
288
+ teamId: input.teamId,
289
+ cwd: context.worktree,
290
+ engineer: input.engineer,
291
+ mode: input.mode,
292
+ message: input.message,
293
+ model: input.model,
294
+ abortSignal: context.abort,
295
+ onEvent: (event) => reportClaudeEvent(context, input.engineer, event),
296
+ });
297
+ await services.orchestrator.recordWrapperExchange(context.worktree, input.teamId, input.engineer, context.sessionID, input.mode, input.message, result.finalText);
562
298
  context.metadata({
563
- title: `${emoji}${title}`,
564
- metadata: { ...metadata, ...(status ? { status } : {}) },
299
+ title: `✅ ${input.engineer} finished`,
300
+ metadata: {
301
+ teamId: result.teamId,
302
+ engineer: result.engineer,
303
+ mode: result.mode,
304
+ sessionId: result.sessionId,
305
+ turns: result.turns,
306
+ contextWarning: formatContextWarning(result.context),
307
+ },
565
308
  });
309
+ return result;
566
310
  }
567
- function formatLiveUsage(turns, cost) {
568
- if (turns === undefined && cost === undefined) {
569
- return '';
570
- }
571
- const parts = [];
572
- if (turns !== undefined) {
573
- parts.push(`🔄 ${turns} turns`);
574
- }
575
- if (cost !== undefined) {
576
- parts.push(`💰 $${cost.toFixed(4)}`);
577
- }
578
- return ` (${parts.join(', ')})`;
579
- }
580
- function formatContextWarning(context) {
581
- const { warningLevel, estimatedContextPercent, totalTurns, totalCostUsd } = context;
582
- if (warningLevel === 'ok' || estimatedContextPercent === null) {
583
- return null;
311
+ function engineerFromAgent(agentId) {
312
+ const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === agentId);
313
+ const engineer = engineerEntry?.[0];
314
+ if (!engineer || !isEngineerName(engineer)) {
315
+ throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
584
316
  }
585
- const templates = managerPromptRegistry.contextWarnings;
586
- const template = warningLevel === 'critical'
587
- ? templates.critical
588
- : warningLevel === 'high'
589
- ? templates.high
590
- : templates.moderate;
591
- return template
592
- .replace('{percent}', String(estimatedContextPercent))
593
- .replace('{turns}', String(totalTurns))
594
- .replace('{cost}', totalCostUsd.toFixed(2));
317
+ return engineer;
595
318
  }
596
- function formatStatusEmoji(status) {
597
- switch (status) {
598
- case 'running':
599
- return '⚡';
600
- case 'success':
601
- return '✅';
602
- case 'error':
603
- return '❌';
604
- case 'warning':
605
- return '⚠️';
606
- }
319
+ function isEngineerAgent(agentId) {
320
+ return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
607
321
  }
608
- function formatToolOutputPreview(text) {
609
- const lower = text.toLowerCase();
610
- let prefix;
611
- if (lower.includes('"tool":"read"') ||
612
- lower.includes('"name":"read"') ||
613
- lower.includes('file contents')) {
614
- prefix = '↳ Read: ';
322
+ function reportClaudeEvent(context, engineer, event) {
323
+ if (event.type === 'error') {
324
+ context.metadata({
325
+ title: `❌ ${engineer} hit an error`,
326
+ metadata: {
327
+ engineer,
328
+ sessionId: event.sessionId,
329
+ error: event.text.slice(0, 200),
330
+ },
331
+ });
332
+ return;
615
333
  }
616
- else if (lower.includes('"tool":"grep"') ||
617
- lower.includes('"name":"grep"') ||
618
- lower.includes('matches found')) {
619
- prefix = '↳ Found: ';
334
+ if (event.type === 'init') {
335
+ context.metadata({
336
+ title: `⚡ ${engineer} session ready`,
337
+ metadata: {
338
+ engineer,
339
+ sessionId: event.sessionId,
340
+ },
341
+ });
342
+ return;
620
343
  }
621
- else if (lower.includes('"tool":"write"') ||
622
- lower.includes('"name":"write"') ||
623
- lower.includes('"tool":"edit"') ||
624
- lower.includes('"name":"edit"') ||
625
- lower.includes('file written') ||
626
- lower.includes('file updated')) {
627
- prefix = '↳ Wrote: ';
344
+ if (event.type === 'tool_call') {
345
+ context.metadata({
346
+ title: `⚡ ${engineer} is using Claude Code tools`,
347
+ metadata: {
348
+ engineer,
349
+ sessionId: event.sessionId,
350
+ },
351
+ });
352
+ return;
628
353
  }
629
- else {
630
- prefix = '↳ Result: ';
354
+ if (event.type === 'assistant' || event.type === 'partial') {
355
+ context.metadata({
356
+ title: `⚡ ${engineer} is working`,
357
+ metadata: {
358
+ engineer,
359
+ sessionId: event.sessionId,
360
+ preview: event.text.slice(0, 160),
361
+ },
362
+ });
631
363
  }
632
- const snippet = text.replace(/\s+/g, ' ').trim();
633
- const truncated = snippet.length > 60 ? snippet.slice(0, 60) + '...' : snippet;
634
- return `${prefix}${truncated}`;
635
364
  }
636
- function showToastIfAvailable(context, message) {
637
- const ctx = context;
638
- ctx.client?.tui?.showToast?.(message);
365
+ function annotateToolRun(context, title, metadata) {
366
+ context.metadata({
367
+ title,
368
+ metadata,
369
+ });
370
+ }
371
+ function formatContextWarning(context) {
372
+ if (context.warningLevel === 'ok' || context.estimatedContextPercent === null) {
373
+ return null;
374
+ }
375
+ const template = context.warningLevel === 'critical'
376
+ ? managerPromptRegistry.contextWarnings.critical
377
+ : context.warningLevel === 'high'
378
+ ? managerPromptRegistry.contextWarnings.high
379
+ : managerPromptRegistry.contextWarnings.moderate;
380
+ return template
381
+ .replace('{percent}', String(context.estimatedContextPercent))
382
+ .replace('{turns}', String(context.totalTurns))
383
+ .replace('{cost}', context.totalCostUsd.toFixed(2));
639
384
  }