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

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