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