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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +29 -31
  2. package/dist/index.d.ts +1 -1
  3. package/dist/manager/persistent-manager.d.ts +3 -23
  4. package/dist/manager/persistent-manager.js +2 -95
  5. package/dist/manager/team-orchestrator.d.ts +50 -0
  6. package/dist/manager/team-orchestrator.js +360 -0
  7. package/dist/plugin/agent-hierarchy.d.ts +12 -34
  8. package/dist/plugin/agent-hierarchy.js +36 -129
  9. package/dist/plugin/claude-manager.plugin.js +190 -445
  10. package/dist/plugin/service-factory.d.ts +18 -3
  11. package/dist/plugin/service-factory.js +32 -1
  12. package/dist/prompts/registry.d.ts +1 -10
  13. package/dist/prompts/registry.js +42 -270
  14. package/dist/src/claude/claude-agent-sdk-adapter.js +2 -1
  15. package/dist/src/claude/session-live-tailer.js +2 -2
  16. package/dist/src/index.d.ts +1 -1
  17. package/dist/src/manager/git-operations.d.ts +10 -1
  18. package/dist/src/manager/git-operations.js +18 -3
  19. package/dist/src/manager/persistent-manager.d.ts +18 -6
  20. package/dist/src/manager/persistent-manager.js +19 -13
  21. package/dist/src/manager/session-controller.d.ts +7 -10
  22. package/dist/src/manager/session-controller.js +12 -62
  23. package/dist/src/manager/team-orchestrator.d.ts +50 -0
  24. package/dist/src/manager/team-orchestrator.js +360 -0
  25. package/dist/src/plugin/agent-hierarchy.d.ts +12 -26
  26. package/dist/src/plugin/agent-hierarchy.js +36 -99
  27. package/dist/src/plugin/claude-manager.plugin.js +214 -393
  28. package/dist/src/plugin/service-factory.d.ts +18 -3
  29. package/dist/src/plugin/service-factory.js +33 -9
  30. package/dist/src/prompts/registry.d.ts +1 -10
  31. package/dist/src/prompts/registry.js +41 -246
  32. package/dist/src/state/team-state-store.d.ts +14 -0
  33. package/dist/src/state/team-state-store.js +85 -0
  34. package/dist/src/team/roster.d.ts +5 -0
  35. package/dist/src/team/roster.js +38 -0
  36. package/dist/src/types/contracts.d.ts +55 -13
  37. package/dist/src/types/contracts.js +1 -1
  38. package/dist/state/team-state-store.d.ts +14 -0
  39. package/dist/state/team-state-store.js +85 -0
  40. package/dist/team/roster.d.ts +5 -0
  41. package/dist/team/roster.js +38 -0
  42. package/dist/test/claude-manager.plugin.test.js +55 -280
  43. package/dist/test/git-operations.test.js +65 -1
  44. package/dist/test/persistent-manager.test.js +3 -3
  45. package/dist/test/prompt-registry.test.js +32 -252
  46. package/dist/test/session-controller.test.js +27 -27
  47. package/dist/test/team-orchestrator.test.d.ts +1 -0
  48. package/dist/test/team-orchestrator.test.js +146 -0
  49. package/dist/test/team-state-store.test.d.ts +1 -0
  50. package/dist/test/team-state-store.test.js +54 -0
  51. package/dist/types/contracts.d.ts +50 -23
  52. package/dist/types/contracts.js +1 -1
  53. package/package.json +1 -1
@@ -1,396 +1,198 @@
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_BUILD, AGENT_ENGINEER_PLAN, buildCtoAgentConfig, buildEngineerBuildAgentConfig, buildEngineerPlanAgentConfig, 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 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
114
- }
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
- });
21
+ },
22
+ 'chat.message': async (input) => {
23
+ if (input.agent === AGENT_CTO) {
24
+ setActiveTeamSession(worktree, input.sessionID);
25
+ return;
128
26
  }
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
- },
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,
139
36
  });
37
+ await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
140
38
  }
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
- });
39
+ },
40
+ 'experimental.chat.system.transform': async (input, output) => {
41
+ if (!input.sessionID) {
42
+ return;
152
43
  }
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)}`);
44
+ const existing = getWrapperSessionMapping(worktree, input.sessionID);
45
+ const persisted = existing ??
46
+ (await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
47
+ if (!persisted) {
48
+ return;
163
49
  }
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);
50
+ const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree, persisted.teamId, persisted.engineer);
51
+ if (wrapperContext) {
52
+ output.system.push(wrapperContext);
186
53
  }
187
- catch {
188
- // Non-critical — the JSONL file may not exist yet.
189
- }
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
54
  },
220
55
  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.',
56
+ claude: tool({
57
+ description: "Run work through this named engineer's persistent Claude Code session. The session remembers prior turns for this engineer.",
225
58
  args: {
59
+ mode: tool.schema.enum(MODE_ENUM),
226
60
  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(),
61
+ model: tool.schema.enum(MODEL_ENUM).optional(),
233
62
  },
234
63
  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.',
241
- args: {
242
- 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(),
249
- },
250
- async execute(args, context) {
251
- return executeDelegate({ ...args, mode: 'free' }, 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);
252
82
  },
253
83
  }),
254
- compact_context: tool({
255
- description: 'Compress session history to reclaim context window space. ' +
256
- '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.',
257
86
  args: {
258
- cwd: tool.schema.string().optional(),
87
+ teamId: tool.schema.string().optional(),
259
88
  },
260
89
  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
- },
90
+ const teamId = args.teamId ?? getActiveTeamSession(context.worktree) ?? context.sessionID;
91
+ annotateToolRun(context, 'Reading team status', {
92
+ teamId,
274
93
  });
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);
94
+ const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
95
+ return JSON.stringify(team, null, 2);
283
96
  },
284
97
  }),
285
98
  git_diff: tool({
286
- description: 'Run git diff to see all current changes (staged + unstaged) relative to HEAD.',
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.',
287
100
  args: {
288
- cwd: tool.schema.string().optional(),
101
+ paths: tool.schema.string().array().optional(),
102
+ staged: tool.schema.boolean().optional(),
103
+ ref: tool.schema.string().optional(),
289
104
  },
290
- async execute(_args, context) {
291
- annotateToolRun(context, 'Running git diff', {});
292
- const result = await services.manager.gitDiff();
105
+ async execute(args, context) {
106
+ annotateToolRun(context, 'Running git diff', {
107
+ paths: args.paths,
108
+ staged: args.staged,
109
+ ref: args.ref,
110
+ });
111
+ const result = await services.manager.gitDiff({
112
+ paths: args.paths?.filter((path) => path !== undefined),
113
+ staged: args.staged,
114
+ ref: args.ref,
115
+ });
293
116
  return JSON.stringify(result, null, 2);
294
117
  },
295
118
  }),
296
119
  git_commit: tool({
297
- description: 'Stage all changes and commit with the given message.',
120
+ description: 'Stage all changes and create a commit with the given message.',
298
121
  args: {
299
122
  message: tool.schema.string().min(1),
300
- cwd: tool.schema.string().optional(),
301
123
  },
302
124
  async execute(args, context) {
303
- annotateToolRun(context, 'Committing changes', {
304
- message: args.message,
305
- });
125
+ annotateToolRun(context, 'Committing changes', { message: args.message });
306
126
  const result = await services.manager.gitCommit(args.message);
307
127
  return JSON.stringify(result, null, 2);
308
128
  },
309
129
  }),
310
130
  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
- },
131
+ description: 'Discard all uncommitted changes by running git reset --hard HEAD and git clean -fd.',
132
+ args: {},
315
133
  async execute(_args, context) {
316
134
  annotateToolRun(context, 'Resetting working directory', {});
317
135
  const result = await services.manager.gitReset();
318
136
  return JSON.stringify(result, null, 2);
319
137
  },
320
138
  }),
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 });
139
+ git_status: tool({
140
+ description: 'Show working tree status in short format and whether the tree is clean.',
141
+ args: {},
142
+ async execute(_args, context) {
143
+ annotateToolRun(context, 'Checking git status', {});
144
+ const result = await services.manager.gitStatus();
145
+ return JSON.stringify(result, null, 2);
334
146
  },
335
147
  }),
336
- session_health: tool({
337
- description: 'Check session health metrics: context usage %, turn count, cost, and session ID.',
148
+ git_log: tool({
149
+ description: 'Show recent commits in short format. Defaults to 5 commits.',
338
150
  args: {
339
- cwd: tool.schema.string().optional(),
151
+ count: tool.schema.number().optional(),
340
152
  },
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);
153
+ async execute(args, context) {
154
+ annotateToolRun(context, 'Fetching git log', { count: args.count });
155
+ return services.manager.gitLog(args.count ?? 5);
351
156
  },
352
157
  }),
353
158
  list_transcripts: tool({
354
- 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.',
355
160
  args: {
356
- cwd: tool.schema.string().optional(),
357
161
  sessionId: tool.schema.string().optional(),
358
162
  },
359
163
  async execute(args, context) {
360
164
  annotateToolRun(context, 'Inspecting Claude session history', {});
361
- const cwd = args.cwd ?? context.worktree;
362
165
  if (args.sessionId) {
363
166
  const [sdkTranscript, localEvents] = await Promise.all([
364
- services.sessions.getTranscript(args.sessionId, cwd),
365
- services.manager.getTranscriptEvents(cwd, args.sessionId),
167
+ services.sessions.getTranscript(args.sessionId, context.worktree),
168
+ services.manager.getTranscriptEvents(context.worktree, args.sessionId),
366
169
  ]);
367
170
  return JSON.stringify({
368
171
  sdkTranscript,
369
172
  localEvents: localEvents.length > 0 ? localEvents : undefined,
370
173
  }, null, 2);
371
174
  }
372
- const sessions = await services.sessions.listSessions(cwd);
175
+ const sessions = await services.sessions.listSessions(context.worktree);
373
176
  return JSON.stringify(sessions, null, 2);
374
177
  },
375
178
  }),
376
179
  list_history: tool({
377
- 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.',
378
181
  args: {
379
- cwd: tool.schema.string().optional(),
380
- runId: tool.schema.string().optional(),
182
+ teamId: tool.schema.string().optional(),
381
183
  },
382
184
  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);
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);
387
189
  }
388
- const runs = await services.manager.listRuns(args.cwd ?? context.worktree);
389
- return JSON.stringify(runs, null, 2);
190
+ const teams = await services.orchestrator.listTeams(context.worktree);
191
+ return JSON.stringify(teams, null, 2);
390
192
  },
391
193
  }),
392
194
  approval_policy: tool({
393
- description: 'View the current tool approval policy: rules, default action, and enabled status.',
195
+ description: 'View the current tool approval policy.',
394
196
  args: {},
395
197
  async execute(_args, context) {
396
198
  annotateToolRun(context, 'Reading approval policy', {});
@@ -398,8 +200,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
398
200
  },
399
201
  }),
400
202
  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.',
203
+ description: 'View recent tool approval decisions. Use deniedOnly to show only denied calls.',
403
204
  args: {
404
205
  limit: tool.schema.number().optional(),
405
206
  deniedOnly: tool.schema.boolean().optional(),
@@ -413,8 +214,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
413
214
  },
414
215
  }),
415
216
  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.',
217
+ description: 'Update the tool approval policy. Add or remove rules, change the default action, enable or disable approvals, or clear decision history.',
418
218
  args: {
419
219
  action: tool.schema.enum([
420
220
  'addRule',
@@ -455,13 +255,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
455
255
  return JSON.stringify({ error: 'removeRule requires ruleId' });
456
256
  }
457
257
  const removed = services.approvalManager.removeRule(args.ruleId);
458
- return JSON.stringify({ removed });
258
+ return JSON.stringify({ removed }, null, 2);
459
259
  }
460
260
  else if (args.action === 'setDefault') {
461
261
  if (!args.defaultAction) {
462
- return JSON.stringify({
463
- error: 'setDefault requires defaultAction',
464
- });
262
+ return JSON.stringify({ error: 'setDefault requires defaultAction' });
465
263
  }
466
264
  services.approvalManager.setDefaultAction(args.defaultAction);
467
265
  }
@@ -480,84 +278,107 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
480
278
  },
481
279
  };
482
280
  };
483
- function annotateToolRun(context, title, metadata, status) {
484
- 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);
485
298
  context.metadata({
486
- title: `${emoji}${title}`,
487
- 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
+ },
488
308
  });
309
+ return result;
489
310
  }
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)}`);
500
- }
501
- return ` (${parts.join(', ')})`;
502
- }
503
- function formatContextWarning(context) {
504
- const { warningLevel, estimatedContextPercent, totalTurns, totalCostUsd } = context;
505
- if (warningLevel === 'ok' || estimatedContextPercent === null) {
506
- return null;
311
+ function engineerFromAgent(agentId) {
312
+ const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === agentId);
313
+ const engineer = engineerEntry?.[0];
314
+ if (!engineer || !isEngineerName(engineer)) {
315
+ throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
507
316
  }
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));
317
+ return engineer;
518
318
  }
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
- }
319
+ function isEngineerAgent(agentId) {
320
+ return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
530
321
  }
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: ';
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;
538
333
  }
539
- else if (lower.includes('"tool":"grep"') ||
540
- lower.includes('"name":"grep"') ||
541
- lower.includes('matches found')) {
542
- 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;
543
343
  }
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: ';
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;
551
353
  }
552
- else {
553
- 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
+ });
554
363
  }
555
- const snippet = text.replace(/\s+/g, ' ').trim();
556
- const truncated = snippet.length > 60 ? snippet.slice(0, 60) + '...' : snippet;
557
- return `${prefix}${truncated}`;
558
364
  }
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);
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));
563
384
  }