@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,396 +1,206 @@
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_BUILD, AGENT_ENGINEER_PLAN, buildCtoAgentConfig, buildEngineerBuildAgentConfig, buildEngineerPlanAgentConfig, 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 cwd = args.cwd ?? context.worktree;
10
- if (args.freshSession) {
11
- await services.manager.clearSession(cwd);
12
- }
13
- const hasActiveSession = services.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: services.manager.getStatus().sessionId,
22
- prompt: promptPreview,
23
- },
24
- });
25
- let turnsSoFar;
26
- let costSoFar;
27
- const result = await services.manager.sendMessage(cwd, args.message, {
28
- model: args.model,
29
- effort: args.effort,
30
- mode: args.mode,
31
- abortSignal: context.abort,
32
- }, (event) => {
33
- if (event.turns !== undefined) {
34
- turnsSoFar = event.turns;
35
- }
36
- if (event.totalCostUsd !== undefined) {
37
- costSoFar = event.totalCostUsd;
38
- }
39
- const usageSuffix = formatLiveUsage(turnsSoFar, costSoFar);
40
- if (event.type === 'tool_call') {
41
- let toolName = 'tool';
42
- let inputPreview = '';
43
- try {
44
- const parsed = JSON.parse(event.text);
45
- toolName = parsed.name ?? 'tool';
46
- if (parsed.input) {
47
- const inputStr = typeof parsed.input === 'string' ? parsed.input : JSON.stringify(parsed.input);
48
- inputPreview = inputStr.length > 150 ? inputStr.slice(0, 150) + '...' : inputStr;
49
- }
50
- }
51
- catch {
52
- // ignore parse errors
53
- }
54
- context.metadata({
55
- title: `⚡ Claude Code: Running ${toolName}...${usageSuffix}`,
56
- metadata: {
57
- status: 'running',
58
- sessionId: event.sessionId,
59
- type: event.type,
60
- tool: toolName,
61
- input: inputPreview,
62
- },
63
- });
64
- }
65
- else if (event.type === 'assistant') {
66
- const thinkingPreview = event.text.length > 150 ? event.text.slice(0, 150) + '...' : event.text;
67
- context.metadata({
68
- title: `⚡ Claude Code: Thinking...${usageSuffix}`,
69
- metadata: {
70
- status: 'running',
71
- sessionId: event.sessionId,
72
- type: event.type,
73
- thinking: thinkingPreview,
74
- },
75
- });
76
- }
77
- else if (event.type === 'init') {
78
- context.metadata({
79
- title: `⚡ Claude Code: Session started`,
80
- metadata: {
81
- status: 'running',
82
- sessionId: event.sessionId,
83
- prompt: promptPreview,
84
- },
85
- });
86
- }
87
- else if (event.type === 'user') {
88
- const preview = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
89
- const outputPreview = formatToolOutputPreview(event.text);
90
- context.metadata({
91
- title: `⚡ Claude Code: ${outputPreview}${usageSuffix}`,
92
- metadata: {
93
- status: 'running',
94
- sessionId: event.sessionId,
95
- type: event.type,
96
- output: preview,
97
- },
98
- });
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);
99
20
  }
100
- else if (event.type === 'tool_progress') {
101
- let toolName = 'tool';
102
- let elapsed = 0;
103
- let progressCurrent;
104
- let progressTotal;
105
- try {
106
- const parsed = JSON.parse(event.text);
107
- toolName = parsed.name ?? 'tool';
108
- elapsed = parsed.elapsed ?? 0;
109
- progressCurrent = parsed.current;
110
- progressTotal = parsed.total;
111
- }
112
- catch {
113
- // 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);
114
32
  }
115
- const progressInfo = progressCurrent !== undefined && progressTotal !== undefined
116
- ? ` [${progressCurrent}/${progressTotal}]`
117
- : '';
118
- context.metadata({
119
- title: `⚡ Claude Code: ${toolName} running ${elapsed > 0 ? `(${elapsed.toFixed(0)}s)` : ''}${progressInfo}...${usageSuffix}`,
120
- metadata: {
121
- status: 'running',
122
- sessionId: event.sessionId,
123
- type: event.type,
124
- tool: toolName,
125
- elapsed,
126
- },
127
- });
33
+ return;
128
34
  }
129
- else if (event.type === 'tool_summary') {
130
- const summary = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
131
- context.metadata({
132
- title: `✅ Claude Code: Tool done${usageSuffix}`,
133
- metadata: {
134
- status: 'success',
135
- sessionId: event.sessionId,
136
- type: event.type,
137
- summary,
138
- },
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,
139
44
  });
45
+ await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
140
46
  }
141
- else if (event.type === 'partial') {
142
- const delta = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
143
- context.metadata({
144
- title: `⚡ Claude Code: Writing...${usageSuffix}`,
145
- metadata: {
146
- status: 'running',
147
- sessionId: event.sessionId,
148
- type: event.type,
149
- delta,
150
- },
151
- });
152
- }
153
- else if (event.type === 'error') {
154
- context.metadata({
155
- title: `❌ Claude Code: Error`,
156
- metadata: {
157
- status: 'error',
158
- sessionId: event.sessionId,
159
- error: event.text.slice(0, 200),
160
- },
161
- });
162
- 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;
163
51
  }
164
- });
165
- const costLabel = `$${(result.totalCostUsd ?? 0).toFixed(4)}`;
166
- const turns = result.turns ?? 0;
167
- const contextWarning = formatContextWarning(result.context);
168
- if (contextWarning) {
169
- context.metadata({
170
- title: `⚠️ Claude Code: Context at ${result.context.estimatedContextPercent}% (${turns} turns)`,
171
- metadata: { status: 'warning', sessionId: result.sessionId, contextWarning },
172
- });
173
- showToastIfAvailable(context, `⚠️ Context usage at ${result.context.estimatedContextPercent}% — consider compacting`);
174
- }
175
- else {
176
- context.metadata({
177
- title: `✅ Claude Code: Complete (${turns} turns, ${costLabel})`,
178
- metadata: { status: 'success', sessionId: result.sessionId },
179
- });
180
- showToastIfAvailable(context, `✅ Session complete (${turns} turns, ${costLabel})`);
181
- }
182
- let toolOutputs = [];
183
- if (result.sessionId) {
184
- try {
185
- toolOutputs = await services.liveTailer.getToolOutputPreview(result.sessionId, cwd, 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;
186
57
  }
187
- catch {
188
- // 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);
189
61
  }
190
- }
191
- return JSON.stringify({
192
- sessionId: result.sessionId,
193
- finalText: result.finalText,
194
- turns: result.turns,
195
- totalCostUsd: result.totalCostUsd,
196
- inputTokens: result.inputTokens,
197
- outputTokens: result.outputTokens,
198
- contextWindowSize: result.contextWindowSize,
199
- context: result.context,
200
- contextWarning,
201
- toolOutputs: toolOutputs.length > 0 ? toolOutputs : undefined,
202
- }, null, 2);
203
- }
204
- return {
205
- config: async (config) => {
206
- config.agent ??= {};
207
- config.permission ??= {};
208
- denyRestrictedToolsGlobally(config.permission);
209
- // Discover project Claude files and build derived wrapper prompts.
210
- const claudeFiles = await discoverProjectClaudeFiles(worktree);
211
- const derivedPrompts = {
212
- ...managerPromptRegistry,
213
- engineerPlanPrompt: composeWrapperPrompt(managerPromptRegistry.engineerPlanPrompt, claudeFiles),
214
- engineerBuildPrompt: composeWrapperPrompt(managerPromptRegistry.engineerBuildPrompt, claudeFiles),
215
- };
216
- config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
217
- config.agent[AGENT_ENGINEER_PLAN] ??= buildEngineerPlanAgentConfig(derivedPrompts);
218
- config.agent[AGENT_ENGINEER_BUILD] ??= buildEngineerBuildAgentConfig(derivedPrompts);
219
62
  },
220
63
  tool: {
221
- explore: tool({
222
- description: 'Investigate and analyze code without making edits. ' +
223
- 'Read-only exploration of the codebase. ' +
224
- 'Preferred first step before implementation.',
225
- args: {
226
- message: tool.schema.string().min(1),
227
- model: tool.schema
228
- .enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
229
- .optional(),
230
- effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
231
- freshSession: tool.schema.boolean().default(false),
232
- cwd: tool.schema.string().optional(),
233
- },
234
- async execute(args, context) {
235
- return executeDelegate({ ...args, mode: 'plan' }, context);
236
- },
237
- }),
238
- implement: tool({
239
- description: 'Implement code changes - can read, edit, and create files. ' +
240
- 'Use after exploration to make changes.',
64
+ claude: tool({
65
+ description: "Run work through this named engineer's persistent Claude Code session. The session remembers prior turns for this engineer.",
241
66
  args: {
67
+ mode: tool.schema.enum(MODE_ENUM),
242
68
  message: tool.schema.string().min(1),
243
- model: tool.schema
244
- .enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
245
- .optional(),
246
- effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
247
- freshSession: tool.schema.boolean().default(false),
248
- cwd: tool.schema.string().optional(),
69
+ model: tool.schema.enum(MODEL_ENUM).optional(),
249
70
  },
250
71
  async execute(args, context) {
251
- return executeDelegate({ ...args, mode: 'free' }, 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;
252
90
  },
253
91
  }),
254
- compact_context: tool({
255
- description: 'Compress session history to reclaim context window space. ' +
256
- '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.',
257
94
  args: {
258
- cwd: tool.schema.string().optional(),
95
+ teamId: tool.schema.string().optional(),
259
96
  },
260
97
  async execute(args, context) {
261
- const cwd = args.cwd ?? context.worktree;
262
- annotateToolRun(context, 'Compacting session', {});
263
- const result = await services.manager.compactSession(cwd);
264
- const snap = services.manager.getStatus();
265
- const contextWarning = formatContextWarning(snap);
266
- context.metadata({
267
- title: contextWarning
268
- ? `⚠️ Claude Code: Compacted — context at ${snap.estimatedContextPercent}%`
269
- : `✅ Claude Code: Compacted (${snap.totalTurns} turns, $${(snap.totalCostUsd ?? 0).toFixed(4)})`,
270
- metadata: {
271
- status: contextWarning ? 'warning' : 'success',
272
- sessionId: result.sessionId,
273
- },
98
+ const teamId = args.teamId ?? getActiveTeamSession(context.worktree) ?? context.sessionID;
99
+ annotateToolRun(context, 'Reading team status', {
100
+ teamId,
274
101
  });
275
- return JSON.stringify({
276
- sessionId: result.sessionId,
277
- finalText: result.finalText,
278
- turns: result.turns,
279
- totalCostUsd: result.totalCostUsd,
280
- context: snap,
281
- contextWarning,
282
- }, null, 2);
102
+ const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
103
+ return JSON.stringify(team, null, 2);
283
104
  },
284
105
  }),
285
106
  git_diff: tool({
286
- description: 'Run git diff to see all current changes (staged + unstaged) relative to HEAD.',
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.',
287
108
  args: {
288
- cwd: tool.schema.string().optional(),
109
+ paths: tool.schema.string().array().optional(),
110
+ staged: tool.schema.boolean().optional(),
111
+ ref: tool.schema.string().optional(),
289
112
  },
290
- async execute(_args, context) {
291
- annotateToolRun(context, 'Running git diff', {});
292
- const result = await services.manager.gitDiff();
113
+ async execute(args, context) {
114
+ annotateToolRun(context, 'Running git diff', {
115
+ paths: args.paths,
116
+ staged: args.staged,
117
+ ref: args.ref,
118
+ });
119
+ const result = await services.manager.gitDiff({
120
+ paths: args.paths?.filter((path) => path !== undefined),
121
+ staged: args.staged,
122
+ ref: args.ref,
123
+ });
293
124
  return JSON.stringify(result, null, 2);
294
125
  },
295
126
  }),
296
127
  git_commit: tool({
297
- description: 'Stage all changes and commit with the given message.',
128
+ description: 'Stage all changes and create a commit with the given message.',
298
129
  args: {
299
130
  message: tool.schema.string().min(1),
300
- cwd: tool.schema.string().optional(),
301
131
  },
302
132
  async execute(args, context) {
303
- annotateToolRun(context, 'Committing changes', {
304
- message: args.message,
305
- });
133
+ annotateToolRun(context, 'Committing changes', { message: args.message });
306
134
  const result = await services.manager.gitCommit(args.message);
307
135
  return JSON.stringify(result, null, 2);
308
136
  },
309
137
  }),
310
138
  git_reset: tool({
311
- description: 'Run git reset --hard HEAD and git clean -fd to discard ALL uncommitted changes and untracked files.',
312
- args: {
313
- cwd: tool.schema.string().optional(),
314
- },
139
+ description: 'Discard all uncommitted changes by running git reset --hard HEAD and git clean -fd.',
140
+ args: {},
315
141
  async execute(_args, context) {
316
142
  annotateToolRun(context, 'Resetting working directory', {});
317
143
  const result = await services.manager.gitReset();
318
144
  return JSON.stringify(result, null, 2);
319
145
  },
320
146
  }),
321
- clear_session: tool({
322
- description: 'Clear the active session to start fresh. ' +
323
- 'Use when context is full or starting a new task.',
324
- args: {
325
- cwd: tool.schema.string().optional(),
326
- reason: tool.schema.string().optional(),
327
- },
328
- async execute(args, context) {
329
- annotateToolRun(context, 'Clearing session', {
330
- reason: args.reason,
331
- });
332
- const clearedId = await services.manager.clearSession(args.cwd ?? context.worktree);
333
- return JSON.stringify({ clearedSessionId: clearedId });
147
+ git_status: tool({
148
+ description: 'Show working tree status in short format and whether the tree is clean.',
149
+ args: {},
150
+ async execute(_args, context) {
151
+ annotateToolRun(context, 'Checking git status', {});
152
+ const result = await services.manager.gitStatus();
153
+ return JSON.stringify(result, null, 2);
334
154
  },
335
155
  }),
336
- session_health: tool({
337
- description: 'Check session health metrics: context usage %, turn count, cost, and session ID.',
156
+ git_log: tool({
157
+ description: 'Show recent commits in short format. Defaults to 5 commits.',
338
158
  args: {
339
- cwd: tool.schema.string().optional(),
159
+ count: tool.schema.number().optional(),
340
160
  },
341
- async execute(_args, context) {
342
- annotateToolRun(context, 'Checking session status', {});
343
- const status = services.manager.getStatus();
344
- return JSON.stringify({
345
- ...status,
346
- transcriptFile: status.sessionId
347
- ? `.claude-manager/transcripts/${status.sessionId}.json`
348
- : null,
349
- contextWarning: formatContextWarning(status),
350
- }, null, 2);
161
+ async execute(args, context) {
162
+ annotateToolRun(context, 'Fetching git log', { count: args.count });
163
+ return services.manager.gitLog(args.count ?? 5);
351
164
  },
352
165
  }),
353
166
  list_transcripts: tool({
354
- 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.',
355
168
  args: {
356
- cwd: tool.schema.string().optional(),
357
169
  sessionId: tool.schema.string().optional(),
358
170
  },
359
171
  async execute(args, context) {
360
172
  annotateToolRun(context, 'Inspecting Claude session history', {});
361
- const cwd = args.cwd ?? context.worktree;
362
173
  if (args.sessionId) {
363
174
  const [sdkTranscript, localEvents] = await Promise.all([
364
- services.sessions.getTranscript(args.sessionId, cwd),
365
- services.manager.getTranscriptEvents(cwd, args.sessionId),
175
+ services.sessions.getTranscript(args.sessionId, context.worktree),
176
+ services.manager.getTranscriptEvents(context.worktree, args.sessionId),
366
177
  ]);
367
178
  return JSON.stringify({
368
179
  sdkTranscript,
369
180
  localEvents: localEvents.length > 0 ? localEvents : undefined,
370
181
  }, null, 2);
371
182
  }
372
- const sessions = await services.sessions.listSessions(cwd);
183
+ const sessions = await services.sessions.listSessions(context.worktree);
373
184
  return JSON.stringify(sessions, null, 2);
374
185
  },
375
186
  }),
376
187
  list_history: tool({
377
- 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.',
378
189
  args: {
379
- cwd: tool.schema.string().optional(),
380
- runId: tool.schema.string().optional(),
190
+ teamId: tool.schema.string().optional(),
381
191
  },
382
192
  async execute(args, context) {
383
- annotateToolRun(context, 'Reading manager run state', {});
384
- if (args.runId) {
385
- const run = await services.manager.getRun(args.cwd ?? context.worktree, args.runId);
386
- 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);
387
197
  }
388
- const runs = await services.manager.listRuns(args.cwd ?? context.worktree);
389
- return JSON.stringify(runs, null, 2);
198
+ const teams = await services.orchestrator.listTeams(context.worktree);
199
+ return JSON.stringify(teams, null, 2);
390
200
  },
391
201
  }),
392
202
  approval_policy: tool({
393
- description: 'View the current tool approval policy: rules, default action, and enabled status.',
203
+ description: 'View the current tool approval policy.',
394
204
  args: {},
395
205
  async execute(_args, context) {
396
206
  annotateToolRun(context, 'Reading approval policy', {});
@@ -398,8 +208,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
398
208
  },
399
209
  }),
400
210
  approval_decisions: tool({
401
- description: 'View recent tool approval decisions. Shows what tools were allowed or denied. ' +
402
- 'Use deniedOnly to see only denied calls.',
211
+ description: 'View recent tool approval decisions. Use deniedOnly to show only denied calls.',
403
212
  args: {
404
213
  limit: tool.schema.number().optional(),
405
214
  deniedOnly: tool.schema.boolean().optional(),
@@ -413,8 +222,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
413
222
  },
414
223
  }),
415
224
  approval_update: tool({
416
- description: 'Update the tool approval policy. Add/remove rules, change default action, or enable/disable. ' +
417
- '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.',
418
226
  args: {
419
227
  action: tool.schema.enum([
420
228
  'addRule',
@@ -455,13 +263,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
455
263
  return JSON.stringify({ error: 'removeRule requires ruleId' });
456
264
  }
457
265
  const removed = services.approvalManager.removeRule(args.ruleId);
458
- return JSON.stringify({ removed });
266
+ return JSON.stringify({ removed }, null, 2);
459
267
  }
460
268
  else if (args.action === 'setDefault') {
461
269
  if (!args.defaultAction) {
462
- return JSON.stringify({
463
- error: 'setDefault requires defaultAction',
464
- });
270
+ return JSON.stringify({ error: 'setDefault requires defaultAction' });
465
271
  }
466
272
  services.approvalManager.setDefaultAction(args.defaultAction);
467
273
  }
@@ -480,84 +286,144 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
480
286
  },
481
287
  };
482
288
  };
483
- function annotateToolRun(context, title, metadata, status) {
484
- 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);
485
306
  context.metadata({
486
- title: `${emoji}${title}`,
487
- 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
+ },
488
316
  });
317
+ return result;
489
318
  }
490
- function formatLiveUsage(turns, cost) {
491
- if (turns === undefined && cost === undefined) {
492
- return '';
493
- }
494
- const parts = [];
495
- if (turns !== undefined) {
496
- parts.push(`🔄 ${turns} turns`);
497
- }
498
- if (cost !== undefined) {
499
- 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}.`);
500
324
  }
501
- return ` (${parts.join(', ')})`;
325
+ return engineer;
502
326
  }
503
- function formatContextWarning(context) {
504
- const { warningLevel, estimatedContextPercent, totalTurns, totalCostUsd } = context;
505
- if (warningLevel === 'ok' || estimatedContextPercent === null) {
506
- return null;
507
- }
508
- const templates = managerPromptRegistry.contextWarnings;
509
- const template = warningLevel === 'critical'
510
- ? templates.critical
511
- : warningLevel === 'high'
512
- ? templates.high
513
- : templates.moderate;
514
- return template
515
- .replace('{percent}', String(estimatedContextPercent))
516
- .replace('{turns}', String(totalTurns))
517
- .replace('{cost}', totalCostUsd.toFixed(2));
327
+ function isEngineerAgent(agentId) {
328
+ return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
518
329
  }
519
- function formatStatusEmoji(status) {
520
- switch (status) {
521
- case 'running':
522
- return '⚡';
523
- case 'success':
524
- return '✅';
525
- case 'error':
526
- return '❌';
527
- case 'warning':
528
- return '⚠️';
529
- }
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;
530
337
  }
531
- function formatToolOutputPreview(text) {
532
- const lower = text.toLowerCase();
533
- let prefix;
534
- if (lower.includes('"tool":"read"') ||
535
- lower.includes('"name":"read"') ||
536
- lower.includes('file contents')) {
537
- 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;
538
349
  }
539
- else if (lower.includes('"tool":"grep"') ||
540
- lower.includes('"name":"grep"') ||
541
- lower.includes('matches found')) {
542
- 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;
543
359
  }
544
- else if (lower.includes('"tool":"write"') ||
545
- lower.includes('"name":"write"') ||
546
- lower.includes('"tool":"edit"') ||
547
- lower.includes('"name":"edit"') ||
548
- lower.includes('file written') ||
549
- lower.includes('file updated')) {
550
- 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;
551
398
  }
552
- else {
553
- 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
+ });
554
408
  }
555
- const snippet = text.replace(/\s+/g, ' ').trim();
556
- const truncated = snippet.length > 60 ? snippet.slice(0, 60) + '...' : snippet;
557
- return `${prefix}${truncated}`;
558
409
  }
559
- function showToastIfAvailable(context, message) {
560
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
561
- const client = context.client;
562
- 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));
563
429
  }