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