@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
@@ -1,311 +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 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
115
- }
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
- });
21
+ },
22
+ 'chat.message': async (input) => {
23
+ if (input.agent === AGENT_CTO) {
24
+ setActiveTeamSession(worktree, input.sessionID);
25
+ return;
129
26
  }
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
- },
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,
140
36
  });
37
+ await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
141
38
  }
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
- });
39
+ },
40
+ 'experimental.chat.system.transform': async (input, output) => {
41
+ if (!input.sessionID) {
42
+ return;
153
43
  }
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)}`);
44
+ const existing = getWrapperSessionMapping(worktree, input.sessionID);
45
+ const persisted = existing ??
46
+ (await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
47
+ if (!persisted) {
48
+ return;
164
49
  }
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);
50
+ const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree, persisted.teamId, persisted.engineer);
51
+ if (wrapperContext) {
52
+ output.system.push(wrapperContext);
187
53
  }
188
- catch {
189
- // Non-critical — the JSONL file may not exist yet.
190
- }
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
54
  },
223
55
  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.',
56
+ claude: tool({
57
+ description: "Run work through this named engineer's persistent Claude Code session. The session remembers prior turns for this engineer.",
228
58
  args: {
59
+ mode: tool.schema.enum(MODE_ENUM),
229
60
  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(),
61
+ model: tool.schema.enum(MODEL_ENUM).optional(),
236
62
  },
237
63
  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.',
244
- args: {
245
- 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(),
252
- },
253
- 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);
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);
271
82
  },
272
83
  }),
273
- compact_context: tool({
274
- description: 'Compress session history to reclaim context window space. ' +
275
- '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.',
276
86
  args: {
277
- wrapperType: tool.schema.string().optional(),
87
+ teamId: tool.schema.string().optional(),
278
88
  },
279
89
  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
- },
90
+ const teamId = args.teamId ?? getActiveTeamSession(context.worktree) ?? context.sessionID;
91
+ annotateToolRun(context, 'Reading team status', {
92
+ teamId,
293
93
  });
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);
94
+ const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
95
+ return JSON.stringify(team, null, 2);
302
96
  },
303
97
  }),
304
98
  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").',
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.',
309
100
  args: {
310
101
  paths: tool.schema.string().array().optional(),
311
102
  staged: tool.schema.boolean().optional(),
@@ -317,9 +108,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
317
108
  staged: args.staged,
318
109
  ref: args.ref,
319
110
  });
320
- const paths = args.paths?.filter((p) => p !== undefined);
321
111
  const result = await services.manager.gitDiff({
322
- paths,
112
+ paths: args.paths?.filter((path) => path !== undefined),
323
113
  staged: args.staged,
324
114
  ref: args.ref,
325
115
  });
@@ -327,20 +117,18 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
327
117
  },
328
118
  }),
329
119
  git_commit: tool({
330
- description: 'Stage all changes and commit with the given message.',
120
+ description: 'Stage all changes and create a commit with the given message.',
331
121
  args: {
332
122
  message: tool.schema.string().min(1),
333
123
  },
334
124
  async execute(args, context) {
335
- annotateToolRun(context, 'Committing changes', {
336
- message: args.message,
337
- });
125
+ annotateToolRun(context, 'Committing changes', { message: args.message });
338
126
  const result = await services.manager.gitCommit(args.message);
339
127
  return JSON.stringify(result, null, 2);
340
128
  },
341
129
  }),
342
130
  git_reset: tool({
343
- 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.',
344
132
  args: {},
345
133
  async execute(_args, context) {
346
134
  annotateToolRun(context, 'Resetting working directory', {});
@@ -349,8 +137,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
349
137
  },
350
138
  }),
351
139
  git_status: tool({
352
- description: 'Show working tree status lists changed files in short format. ' +
353
- 'Returns isClean=true if nothing changed.',
140
+ description: 'Show working tree status in short format and whether the tree is clean.',
354
141
  args: {},
355
142
  async execute(_args, context) {
356
143
  annotateToolRun(context, 'Checking git status', {});
@@ -359,93 +146,53 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
359
146
  },
360
147
  }),
361
148
  git_log: tool({
362
- description: 'Show recent commits in short format. ' +
363
- 'Default shows last 5 commits. Use count to change.',
149
+ description: 'Show recent commits in short format. Defaults to 5 commits.',
364
150
  args: {
365
151
  count: tool.schema.number().optional(),
366
152
  },
367
153
  async execute(args, context) {
368
154
  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);
155
+ return services.manager.gitLog(args.count ?? 5);
405
156
  },
406
157
  }),
407
158
  list_transcripts: tool({
408
- 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.',
409
160
  args: {
410
- wrapperType: tool.schema.string().optional(),
411
161
  sessionId: tool.schema.string().optional(),
412
162
  },
413
163
  async execute(args, context) {
414
- const wrapperServices = getOrCreatePluginServices(context.worktree);
415
164
  annotateToolRun(context, 'Inspecting Claude session history', {});
416
165
  if (args.sessionId) {
417
166
  const [sdkTranscript, localEvents] = await Promise.all([
418
- wrapperServices.sessions.getTranscript(args.sessionId, context.worktree),
419
- wrapperServices.manager.getTranscriptEvents(context.worktree, args.sessionId),
167
+ services.sessions.getTranscript(args.sessionId, context.worktree),
168
+ services.manager.getTranscriptEvents(context.worktree, args.sessionId),
420
169
  ]);
421
170
  return JSON.stringify({
422
171
  sdkTranscript,
423
172
  localEvents: localEvents.length > 0 ? localEvents : undefined,
424
173
  }, null, 2);
425
174
  }
426
- const sessions = await wrapperServices.sessions.listSessions(context.worktree);
175
+ const sessions = await services.sessions.listSessions(context.worktree);
427
176
  return JSON.stringify(sessions, null, 2);
428
177
  },
429
178
  }),
430
179
  list_history: tool({
431
- 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.',
432
181
  args: {
433
- wrapperType: tool.schema.string().optional(),
434
- runId: tool.schema.string().optional(),
182
+ teamId: tool.schema.string().optional(),
435
183
  },
436
184
  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);
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);
442
189
  }
443
- const runs = await wrapperServices.manager.listRuns(context.worktree);
444
- return JSON.stringify(runs, null, 2);
190
+ const teams = await services.orchestrator.listTeams(context.worktree);
191
+ return JSON.stringify(teams, null, 2);
445
192
  },
446
193
  }),
447
194
  approval_policy: tool({
448
- description: 'View the current tool approval policy: rules, default action, and enabled status.',
195
+ description: 'View the current tool approval policy.',
449
196
  args: {},
450
197
  async execute(_args, context) {
451
198
  annotateToolRun(context, 'Reading approval policy', {});
@@ -453,8 +200,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
453
200
  },
454
201
  }),
455
202
  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.',
203
+ description: 'View recent tool approval decisions. Use deniedOnly to show only denied calls.',
458
204
  args: {
459
205
  limit: tool.schema.number().optional(),
460
206
  deniedOnly: tool.schema.boolean().optional(),
@@ -468,8 +214,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
468
214
  },
469
215
  }),
470
216
  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.',
217
+ description: 'Update the tool approval policy. Add or remove rules, change the default action, enable or disable approvals, or clear decision history.',
473
218
  args: {
474
219
  action: tool.schema.enum([
475
220
  'addRule',
@@ -510,13 +255,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
510
255
  return JSON.stringify({ error: 'removeRule requires ruleId' });
511
256
  }
512
257
  const removed = services.approvalManager.removeRule(args.ruleId);
513
- return JSON.stringify({ removed });
258
+ return JSON.stringify({ removed }, null, 2);
514
259
  }
515
260
  else if (args.action === 'setDefault') {
516
261
  if (!args.defaultAction) {
517
- return JSON.stringify({
518
- error: 'setDefault requires defaultAction',
519
- });
262
+ return JSON.stringify({ error: 'setDefault requires defaultAction' });
520
263
  }
521
264
  services.approvalManager.setDefaultAction(args.defaultAction);
522
265
  }
@@ -535,83 +278,107 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
535
278
  },
536
279
  };
537
280
  };
538
- function annotateToolRun(context, title, metadata, status) {
539
- 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);
540
298
  context.metadata({
541
- title: `${emoji}${title}`,
542
- 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
+ },
543
308
  });
309
+ return result;
544
310
  }
545
- function formatLiveUsage(turns, cost) {
546
- if (turns === undefined && cost === undefined) {
547
- return '';
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}.`);
548
316
  }
549
- const parts = [];
550
- if (turns !== undefined) {
551
- parts.push(`🔄 ${turns} turns`);
552
- }
553
- if (cost !== undefined) {
554
- parts.push(`💰 $${cost.toFixed(4)}`);
555
- }
556
- return ` (${parts.join(', ')})`;
317
+ return engineer;
557
318
  }
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));
319
+ function isEngineerAgent(agentId) {
320
+ return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
573
321
  }
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
- }
585
- }
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: ';
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;
593
333
  }
594
- else if (lower.includes('"tool":"grep"') ||
595
- lower.includes('"name":"grep"') ||
596
- lower.includes('matches found')) {
597
- 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;
598
343
  }
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: ';
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;
606
353
  }
607
- else {
608
- 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
+ });
609
363
  }
610
- const snippet = text.replace(/\s+/g, ' ').trim();
611
- const truncated = snippet.length > 60 ? snippet.slice(0, 60) + '...' : snippet;
612
- return `${prefix}${truncated}`;
613
364
  }
614
- function showToastIfAvailable(context, message) {
615
- const ctx = context;
616
- 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));
617
384
  }