@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.
- package/README.md +29 -31
- package/dist/index.d.ts +1 -1
- package/dist/manager/persistent-manager.d.ts +3 -23
- package/dist/manager/persistent-manager.js +2 -95
- 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 -445
- 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 -270
- 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 +50 -23
- package/dist/types/contracts.js +1 -1
- package/package.json +1 -1
|
@@ -1,333 +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
|
-
title: hasActiveSession
|
|
18
|
-
? '⚡ Claude Code: Resuming session...'
|
|
19
|
-
: '⚡ Claude Code: Initializing...',
|
|
20
|
-
metadata: {
|
|
21
|
-
status: 'running',
|
|
22
|
-
sessionId: managerStatus.context.sessionId,
|
|
23
|
-
prompt: promptPreview,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
let turnsSoFar;
|
|
27
|
-
let costSoFar;
|
|
28
|
-
const result = await wrapperServices.manager.sendMessage(context.worktree, args.message, {
|
|
29
|
-
model: args.model,
|
|
30
|
-
effort: args.effort,
|
|
31
|
-
mode: args.mode,
|
|
32
|
-
sessionSystemPrompt: args.sessionSystemPrompt,
|
|
33
|
-
abortSignal: context.abort,
|
|
34
|
-
}, (event) => {
|
|
35
|
-
if (event.turns !== undefined) {
|
|
36
|
-
turnsSoFar = event.turns;
|
|
37
|
-
}
|
|
38
|
-
if (event.totalCostUsd !== undefined) {
|
|
39
|
-
costSoFar = event.totalCostUsd;
|
|
40
|
-
}
|
|
41
|
-
const usageSuffix = formatLiveUsage(turnsSoFar, costSoFar);
|
|
42
|
-
if (event.type === 'tool_call') {
|
|
43
|
-
let toolName = 'tool';
|
|
44
|
-
let inputPreview = '';
|
|
45
|
-
try {
|
|
46
|
-
const parsed = JSON.parse(event.text);
|
|
47
|
-
toolName = parsed.name ?? 'tool';
|
|
48
|
-
if (parsed.input) {
|
|
49
|
-
const inputStr = typeof parsed.input === 'string' ? parsed.input : JSON.stringify(parsed.input);
|
|
50
|
-
inputPreview = inputStr.length > 150 ? inputStr.slice(0, 150) + '...' : inputStr;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// ignore parse errors
|
|
55
|
-
}
|
|
56
|
-
context.metadata({
|
|
57
|
-
title: `⚡ Claude Code: Running ${toolName}...${usageSuffix}`,
|
|
58
|
-
metadata: {
|
|
59
|
-
status: 'running',
|
|
60
|
-
sessionId: event.sessionId,
|
|
61
|
-
type: event.type,
|
|
62
|
-
tool: toolName,
|
|
63
|
-
input: inputPreview,
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
else if (event.type === 'assistant') {
|
|
68
|
-
const thinkingPreview = event.text.length > 150 ? event.text.slice(0, 150) + '...' : event.text;
|
|
69
|
-
context.metadata({
|
|
70
|
-
title: `⚡ Claude Code: Thinking...${usageSuffix}`,
|
|
71
|
-
metadata: {
|
|
72
|
-
status: 'running',
|
|
73
|
-
sessionId: event.sessionId,
|
|
74
|
-
type: event.type,
|
|
75
|
-
thinking: thinkingPreview,
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
else if (event.type === 'init') {
|
|
80
|
-
context.metadata({
|
|
81
|
-
title: `⚡ Claude Code: Session started`,
|
|
82
|
-
metadata: {
|
|
83
|
-
status: 'running',
|
|
84
|
-
sessionId: event.sessionId,
|
|
85
|
-
prompt: promptPreview,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
else if (event.type === 'user') {
|
|
90
|
-
const preview = event.text.length > 200 ? event.text.slice(0, 200) + '...' : event.text;
|
|
91
|
-
const outputPreview = formatToolOutputPreview(event.text);
|
|
92
|
-
context.metadata({
|
|
93
|
-
title: `⚡ Claude Code: ${outputPreview}${usageSuffix}`,
|
|
94
|
-
metadata: {
|
|
95
|
-
status: 'running',
|
|
96
|
-
sessionId: event.sessionId,
|
|
97
|
-
type: event.type,
|
|
98
|
-
output: preview,
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
else if (event.type === 'tool_progress') {
|
|
103
|
-
let toolName = 'tool';
|
|
104
|
-
let elapsed = 0;
|
|
105
|
-
let progressCurrent;
|
|
106
|
-
let progressTotal;
|
|
107
|
-
try {
|
|
108
|
-
const parsed = JSON.parse(event.text);
|
|
109
|
-
toolName = parsed.name ?? 'tool';
|
|
110
|
-
elapsed = parsed.elapsed ?? 0;
|
|
111
|
-
progressCurrent = parsed.current;
|
|
112
|
-
progressTotal = parsed.total;
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// ignore
|
|
116
|
-
}
|
|
117
|
-
const progressInfo = progressCurrent !== undefined && progressTotal !== undefined
|
|
118
|
-
? ` [${progressCurrent}/${progressTotal}]`
|
|
119
|
-
: '';
|
|
120
|
-
context.metadata({
|
|
121
|
-
title: `⚡ Claude Code: ${toolName} running ${elapsed > 0 ? `(${elapsed.toFixed(0)}s)` : ''}${progressInfo}...${usageSuffix}`,
|
|
122
|
-
metadata: {
|
|
123
|
-
status: 'running',
|
|
124
|
-
sessionId: event.sessionId,
|
|
125
|
-
type: event.type,
|
|
126
|
-
tool: toolName,
|
|
127
|
-
elapsed,
|
|
128
|
-
},
|
|
129
|
-
});
|
|
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);
|
|
130
20
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
status: 'success',
|
|
137
|
-
sessionId: event.sessionId,
|
|
138
|
-
type: event.type,
|
|
139
|
-
summary,
|
|
140
|
-
},
|
|
141
|
-
});
|
|
21
|
+
},
|
|
22
|
+
'chat.message': async (input) => {
|
|
23
|
+
if (input.agent === AGENT_CTO) {
|
|
24
|
+
setActiveTeamSession(worktree, input.sessionID);
|
|
25
|
+
return;
|
|
142
26
|
}
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
},
|
|
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,
|
|
153
36
|
});
|
|
37
|
+
await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
|
|
154
38
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
status: 'error',
|
|
160
|
-
sessionId: event.sessionId,
|
|
161
|
-
error: event.text.slice(0, 200),
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
showToastIfAvailable(context, `Claude Code error: ${event.text.slice(0, 100)}`);
|
|
39
|
+
},
|
|
40
|
+
'experimental.chat.system.transform': async (input, output) => {
|
|
41
|
+
if (!input.sessionID) {
|
|
42
|
+
return;
|
|
165
43
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
context.metadata({
|
|
172
|
-
title: `⚠️ Claude Code: Context at ${result.context.estimatedContextPercent}% (${turns} turns)`,
|
|
173
|
-
metadata: { status: 'warning', sessionId: result.sessionId, contextWarning },
|
|
174
|
-
});
|
|
175
|
-
showToastIfAvailable(context, `⚠️ Context usage at ${result.context.estimatedContextPercent}% — consider compacting`);
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
context.metadata({
|
|
179
|
-
title: `✅ Claude Code: Complete (${turns} turns, ${costLabel})`,
|
|
180
|
-
metadata: { status: 'success', sessionId: result.sessionId },
|
|
181
|
-
});
|
|
182
|
-
showToastIfAvailable(context, `✅ Session complete (${turns} turns, ${costLabel})`);
|
|
183
|
-
}
|
|
184
|
-
let toolOutputs = [];
|
|
185
|
-
if (result.sessionId) {
|
|
186
|
-
try {
|
|
187
|
-
toolOutputs = await wrapperServices.liveTailer.getToolOutputPreview(result.sessionId, context.worktree, 3);
|
|
44
|
+
const existing = getWrapperSessionMapping(worktree, input.sessionID);
|
|
45
|
+
const persisted = existing ??
|
|
46
|
+
(await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
|
|
47
|
+
if (!persisted) {
|
|
48
|
+
return;
|
|
188
49
|
}
|
|
189
|
-
|
|
190
|
-
|
|
50
|
+
const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree, persisted.teamId, persisted.engineer);
|
|
51
|
+
if (wrapperContext) {
|
|
52
|
+
output.system.push(wrapperContext);
|
|
191
53
|
}
|
|
192
|
-
}
|
|
193
|
-
return JSON.stringify({
|
|
194
|
-
sessionId: result.sessionId,
|
|
195
|
-
finalText: result.finalText,
|
|
196
|
-
turns: result.turns,
|
|
197
|
-
totalCostUsd: result.totalCostUsd,
|
|
198
|
-
inputTokens: result.inputTokens,
|
|
199
|
-
outputTokens: result.outputTokens,
|
|
200
|
-
contextWindowSize: result.contextWindowSize,
|
|
201
|
-
context: result.context,
|
|
202
|
-
contextWarning,
|
|
203
|
-
toolOutputs: toolOutputs.length > 0 ? toolOutputs : undefined,
|
|
204
|
-
}, null, 2);
|
|
205
|
-
}
|
|
206
|
-
return {
|
|
207
|
-
config: async (config) => {
|
|
208
|
-
config.agent ??= {};
|
|
209
|
-
config.permission ??= {};
|
|
210
|
-
denyRestrictedToolsGlobally(config.permission);
|
|
211
|
-
// Discover project Claude files and build derived wrapper prompts.
|
|
212
|
-
const claudeFiles = await discoverProjectClaudeFiles(worktree);
|
|
213
|
-
const derivedPrompts = {
|
|
214
|
-
...managerPromptRegistry,
|
|
215
|
-
engineerExplorePrompt: composeWrapperPrompt(managerPromptRegistry.engineerExplorePrompt, claudeFiles),
|
|
216
|
-
engineerImplementPrompt: composeWrapperPrompt(managerPromptRegistry.engineerImplementPrompt, claudeFiles),
|
|
217
|
-
engineerVerifyPrompt: composeWrapperPrompt(managerPromptRegistry.engineerVerifyPrompt, claudeFiles),
|
|
218
|
-
};
|
|
219
|
-
config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
|
|
220
|
-
config.agent[AGENT_ENGINEER_EXPLORE] ??= buildEngineerExploreAgentConfig(derivedPrompts);
|
|
221
|
-
config.agent[AGENT_ENGINEER_IMPLEMENT] ??= buildEngineerImplementAgentConfig(derivedPrompts);
|
|
222
|
-
config.agent[AGENT_ENGINEER_VERIFY] ??= buildEngineerVerifyAgentConfig(derivedPrompts);
|
|
223
54
|
},
|
|
224
55
|
tool: {
|
|
225
|
-
|
|
226
|
-
description:
|
|
227
|
-
'Read-only exploration of the codebase. ' +
|
|
228
|
-
'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.",
|
|
229
58
|
args: {
|
|
59
|
+
mode: tool.schema.enum(MODE_ENUM),
|
|
230
60
|
message: tool.schema.string().min(1),
|
|
231
|
-
model: tool.schema
|
|
232
|
-
.enum(['claude-opus-4-6', 'claude-sonnet-4-6'])
|
|
233
|
-
.optional(),
|
|
234
|
-
effort: tool.schema.enum(['medium', 'high', 'max']).default('high'),
|
|
235
|
-
freshSession: tool.schema.boolean().default(false),
|
|
236
|
-
sessionSystemPrompt: tool.schema.string().optional(),
|
|
61
|
+
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
237
62
|
},
|
|
238
63
|
async execute(args, context) {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
effort: tool.schema.enum(['medium', 'high', 'max']).default('high'),
|
|
258
|
-
freshSession: tool.schema.boolean().default(false),
|
|
259
|
-
sessionSystemPrompt: tool.schema.string().optional(),
|
|
260
|
-
},
|
|
261
|
-
async execute(args, context) {
|
|
262
|
-
const taskDesc = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
|
|
263
|
-
await services.manager.acquireSlot('verify', taskDesc);
|
|
264
|
-
try {
|
|
265
|
-
return await executeDelegate({ ...args, mode: 'free', wrapperType: 'engineer_verify' }, context);
|
|
266
|
-
}
|
|
267
|
-
finally {
|
|
268
|
-
services.manager.releaseSlot('verify', taskDesc);
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
}),
|
|
272
|
-
implement: tool({
|
|
273
|
-
description: 'Implement code changes - can read, edit, and create files. ' +
|
|
274
|
-
'Use after exploration to make changes.',
|
|
275
|
-
args: {
|
|
276
|
-
message: tool.schema.string().min(1),
|
|
277
|
-
model: tool.schema
|
|
278
|
-
.enum(['claude-opus-4-6', 'claude-sonnet-4-6'])
|
|
279
|
-
.optional(),
|
|
280
|
-
effort: tool.schema.enum(['medium', 'high', 'max']).default('high'),
|
|
281
|
-
freshSession: tool.schema.boolean().default(false),
|
|
282
|
-
sessionSystemPrompt: tool.schema.string().optional(),
|
|
283
|
-
},
|
|
284
|
-
async execute(args, context) {
|
|
285
|
-
const taskDesc = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
|
|
286
|
-
await services.manager.acquireSlot('implement', taskDesc);
|
|
287
|
-
try {
|
|
288
|
-
return await executeDelegate({ ...args, mode: 'free', wrapperType: 'implement' }, context);
|
|
289
|
-
}
|
|
290
|
-
finally {
|
|
291
|
-
services.manager.releaseSlot('implement', taskDesc);
|
|
292
|
-
}
|
|
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);
|
|
293
82
|
},
|
|
294
83
|
}),
|
|
295
|
-
|
|
296
|
-
description: '
|
|
297
|
-
'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.',
|
|
298
86
|
args: {
|
|
299
|
-
|
|
87
|
+
teamId: tool.schema.string().optional(),
|
|
300
88
|
},
|
|
301
89
|
async execute(args, context) {
|
|
302
|
-
const
|
|
303
|
-
annotateToolRun(context, '
|
|
304
|
-
|
|
305
|
-
const snap = wrapperServices.manager.getStatus();
|
|
306
|
-
const contextWarning = formatContextWarning(snap.context);
|
|
307
|
-
context.metadata({
|
|
308
|
-
title: contextWarning
|
|
309
|
-
? `⚠️ Claude Code: Compacted — context at ${snap.context.estimatedContextPercent}%`
|
|
310
|
-
: `✅ Claude Code: Compacted (${snap.context.totalTurns} turns, $${(snap.context.totalCostUsd ?? 0).toFixed(4)})`,
|
|
311
|
-
metadata: {
|
|
312
|
-
status: contextWarning ? 'warning' : 'success',
|
|
313
|
-
sessionId: result.sessionId,
|
|
314
|
-
},
|
|
90
|
+
const teamId = args.teamId ?? getActiveTeamSession(context.worktree) ?? context.sessionID;
|
|
91
|
+
annotateToolRun(context, 'Reading team status', {
|
|
92
|
+
teamId,
|
|
315
93
|
});
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
finalText: result.finalText,
|
|
319
|
-
turns: result.turns,
|
|
320
|
-
totalCostUsd: result.totalCostUsd,
|
|
321
|
-
context: snap.context,
|
|
322
|
-
contextWarning,
|
|
323
|
-
}, null, 2);
|
|
94
|
+
const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
|
|
95
|
+
return JSON.stringify(team, null, 2);
|
|
324
96
|
},
|
|
325
97
|
}),
|
|
326
98
|
git_diff: tool({
|
|
327
|
-
description: 'Show diff of uncommitted changes. '
|
|
328
|
-
'Use paths to filter to specific files/dirs. ' +
|
|
329
|
-
'Use staged=true to see staged changes. ' +
|
|
330
|
-
'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.',
|
|
331
100
|
args: {
|
|
332
101
|
paths: tool.schema.string().array().optional(),
|
|
333
102
|
staged: tool.schema.boolean().optional(),
|
|
@@ -339,9 +108,8 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
339
108
|
staged: args.staged,
|
|
340
109
|
ref: args.ref,
|
|
341
110
|
});
|
|
342
|
-
const paths = args.paths?.filter((p) => p !== undefined);
|
|
343
111
|
const result = await services.manager.gitDiff({
|
|
344
|
-
paths,
|
|
112
|
+
paths: args.paths?.filter((path) => path !== undefined),
|
|
345
113
|
staged: args.staged,
|
|
346
114
|
ref: args.ref,
|
|
347
115
|
});
|
|
@@ -349,20 +117,18 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
349
117
|
},
|
|
350
118
|
}),
|
|
351
119
|
git_commit: tool({
|
|
352
|
-
description: 'Stage all changes and commit with the given message.',
|
|
120
|
+
description: 'Stage all changes and create a commit with the given message.',
|
|
353
121
|
args: {
|
|
354
122
|
message: tool.schema.string().min(1),
|
|
355
123
|
},
|
|
356
124
|
async execute(args, context) {
|
|
357
|
-
annotateToolRun(context, 'Committing changes', {
|
|
358
|
-
message: args.message,
|
|
359
|
-
});
|
|
125
|
+
annotateToolRun(context, 'Committing changes', { message: args.message });
|
|
360
126
|
const result = await services.manager.gitCommit(args.message);
|
|
361
127
|
return JSON.stringify(result, null, 2);
|
|
362
128
|
},
|
|
363
129
|
}),
|
|
364
130
|
git_reset: tool({
|
|
365
|
-
description: 'Discard all uncommitted changes
|
|
131
|
+
description: 'Discard all uncommitted changes by running git reset --hard HEAD and git clean -fd.',
|
|
366
132
|
args: {},
|
|
367
133
|
async execute(_args, context) {
|
|
368
134
|
annotateToolRun(context, 'Resetting working directory', {});
|
|
@@ -371,8 +137,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
371
137
|
},
|
|
372
138
|
}),
|
|
373
139
|
git_status: tool({
|
|
374
|
-
description: 'Show working tree status
|
|
375
|
-
'Returns isClean=true if nothing changed.',
|
|
140
|
+
description: 'Show working tree status in short format and whether the tree is clean.',
|
|
376
141
|
args: {},
|
|
377
142
|
async execute(_args, context) {
|
|
378
143
|
annotateToolRun(context, 'Checking git status', {});
|
|
@@ -381,93 +146,53 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
381
146
|
},
|
|
382
147
|
}),
|
|
383
148
|
git_log: tool({
|
|
384
|
-
description: 'Show recent commits in short format. '
|
|
385
|
-
'Default shows last 5 commits. Use count to change.',
|
|
149
|
+
description: 'Show recent commits in short format. Defaults to 5 commits.',
|
|
386
150
|
args: {
|
|
387
151
|
count: tool.schema.number().optional(),
|
|
388
152
|
},
|
|
389
153
|
async execute(args, context) {
|
|
390
154
|
annotateToolRun(context, 'Fetching git log', { count: args.count });
|
|
391
|
-
|
|
392
|
-
return result;
|
|
393
|
-
},
|
|
394
|
-
}),
|
|
395
|
-
clear_session: tool({
|
|
396
|
-
description: 'Clear the active session to start fresh. ' +
|
|
397
|
-
'Use when context is full or starting a new task.',
|
|
398
|
-
args: {
|
|
399
|
-
wrapperType: tool.schema.string().optional(),
|
|
400
|
-
reason: tool.schema.string().optional(),
|
|
401
|
-
},
|
|
402
|
-
async execute(args, context) {
|
|
403
|
-
const wrapperServices = getOrCreatePluginServices(context.worktree);
|
|
404
|
-
annotateToolRun(context, 'Clearing session', {
|
|
405
|
-
reason: args.reason,
|
|
406
|
-
});
|
|
407
|
-
const clearedId = await wrapperServices.manager.clearSession();
|
|
408
|
-
return JSON.stringify({ clearedSessionId: clearedId });
|
|
409
|
-
},
|
|
410
|
-
}),
|
|
411
|
-
session_health: tool({
|
|
412
|
-
description: 'Check session health metrics: context usage %, turn count, cost, and session ID.',
|
|
413
|
-
args: {
|
|
414
|
-
wrapperType: tool.schema.string().optional(),
|
|
415
|
-
},
|
|
416
|
-
async execute(args, context) {
|
|
417
|
-
const wrapperServices = getOrCreatePluginServices(context.worktree);
|
|
418
|
-
annotateToolRun(context, 'Checking session status', {});
|
|
419
|
-
const status = wrapperServices.manager.getStatus();
|
|
420
|
-
return JSON.stringify({
|
|
421
|
-
...status,
|
|
422
|
-
transcriptFile: status.context.sessionId
|
|
423
|
-
? `.claude-manager/transcripts/${status.context.sessionId}.json`
|
|
424
|
-
: null,
|
|
425
|
-
contextWarning: formatContextWarning(status.context),
|
|
426
|
-
}, null, 2);
|
|
155
|
+
return services.manager.gitLog(args.count ?? 5);
|
|
427
156
|
},
|
|
428
157
|
}),
|
|
429
158
|
list_transcripts: tool({
|
|
430
|
-
description: 'List available session transcripts or inspect
|
|
159
|
+
description: 'List available Claude session transcripts or inspect one transcript by session ID.',
|
|
431
160
|
args: {
|
|
432
|
-
wrapperType: tool.schema.string().optional(),
|
|
433
161
|
sessionId: tool.schema.string().optional(),
|
|
434
162
|
},
|
|
435
163
|
async execute(args, context) {
|
|
436
|
-
const wrapperServices = getOrCreatePluginServices(context.worktree);
|
|
437
164
|
annotateToolRun(context, 'Inspecting Claude session history', {});
|
|
438
165
|
if (args.sessionId) {
|
|
439
166
|
const [sdkTranscript, localEvents] = await Promise.all([
|
|
440
|
-
|
|
441
|
-
|
|
167
|
+
services.sessions.getTranscript(args.sessionId, context.worktree),
|
|
168
|
+
services.manager.getTranscriptEvents(context.worktree, args.sessionId),
|
|
442
169
|
]);
|
|
443
170
|
return JSON.stringify({
|
|
444
171
|
sdkTranscript,
|
|
445
172
|
localEvents: localEvents.length > 0 ? localEvents : undefined,
|
|
446
173
|
}, null, 2);
|
|
447
174
|
}
|
|
448
|
-
const sessions = await
|
|
175
|
+
const sessions = await services.sessions.listSessions(context.worktree);
|
|
449
176
|
return JSON.stringify(sessions, null, 2);
|
|
450
177
|
},
|
|
451
178
|
}),
|
|
452
179
|
list_history: tool({
|
|
453
|
-
description: 'List
|
|
180
|
+
description: 'List saved CTO teams for this worktree or inspect one team by ID.',
|
|
454
181
|
args: {
|
|
455
|
-
|
|
456
|
-
runId: tool.schema.string().optional(),
|
|
182
|
+
teamId: tool.schema.string().optional(),
|
|
457
183
|
},
|
|
458
184
|
async execute(args, context) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
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);
|
|
464
189
|
}
|
|
465
|
-
const
|
|
466
|
-
return JSON.stringify(
|
|
190
|
+
const teams = await services.orchestrator.listTeams(context.worktree);
|
|
191
|
+
return JSON.stringify(teams, null, 2);
|
|
467
192
|
},
|
|
468
193
|
}),
|
|
469
194
|
approval_policy: tool({
|
|
470
|
-
description: 'View the current tool approval policy
|
|
195
|
+
description: 'View the current tool approval policy.',
|
|
471
196
|
args: {},
|
|
472
197
|
async execute(_args, context) {
|
|
473
198
|
annotateToolRun(context, 'Reading approval policy', {});
|
|
@@ -475,8 +200,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
475
200
|
},
|
|
476
201
|
}),
|
|
477
202
|
approval_decisions: tool({
|
|
478
|
-
description: 'View recent tool approval decisions.
|
|
479
|
-
'Use deniedOnly to see only denied calls.',
|
|
203
|
+
description: 'View recent tool approval decisions. Use deniedOnly to show only denied calls.',
|
|
480
204
|
args: {
|
|
481
205
|
limit: tool.schema.number().optional(),
|
|
482
206
|
deniedOnly: tool.schema.boolean().optional(),
|
|
@@ -490,8 +214,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
490
214
|
},
|
|
491
215
|
}),
|
|
492
216
|
approval_update: tool({
|
|
493
|
-
description: 'Update the tool approval policy. Add
|
|
494
|
-
'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.',
|
|
495
218
|
args: {
|
|
496
219
|
action: tool.schema.enum([
|
|
497
220
|
'addRule',
|
|
@@ -532,13 +255,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
532
255
|
return JSON.stringify({ error: 'removeRule requires ruleId' });
|
|
533
256
|
}
|
|
534
257
|
const removed = services.approvalManager.removeRule(args.ruleId);
|
|
535
|
-
return JSON.stringify({ removed });
|
|
258
|
+
return JSON.stringify({ removed }, null, 2);
|
|
536
259
|
}
|
|
537
260
|
else if (args.action === 'setDefault') {
|
|
538
261
|
if (!args.defaultAction) {
|
|
539
|
-
return JSON.stringify({
|
|
540
|
-
error: 'setDefault requires defaultAction',
|
|
541
|
-
});
|
|
262
|
+
return JSON.stringify({ error: 'setDefault requires defaultAction' });
|
|
542
263
|
}
|
|
543
264
|
services.approvalManager.setDefaultAction(args.defaultAction);
|
|
544
265
|
}
|
|
@@ -557,83 +278,107 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
557
278
|
},
|
|
558
279
|
};
|
|
559
280
|
};
|
|
560
|
-
function
|
|
561
|
-
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);
|
|
562
298
|
context.metadata({
|
|
563
|
-
title:
|
|
564
|
-
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
|
+
},
|
|
565
308
|
});
|
|
309
|
+
return result;
|
|
566
310
|
}
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (turns !== undefined) {
|
|
573
|
-
parts.push(`🔄 ${turns} turns`);
|
|
574
|
-
}
|
|
575
|
-
if (cost !== undefined) {
|
|
576
|
-
parts.push(`💰 $${cost.toFixed(4)}`);
|
|
577
|
-
}
|
|
578
|
-
return ` (${parts.join(', ')})`;
|
|
579
|
-
}
|
|
580
|
-
function formatContextWarning(context) {
|
|
581
|
-
const { warningLevel, estimatedContextPercent, totalTurns, totalCostUsd } = context;
|
|
582
|
-
if (warningLevel === 'ok' || estimatedContextPercent === null) {
|
|
583
|
-
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}.`);
|
|
584
316
|
}
|
|
585
|
-
|
|
586
|
-
const template = warningLevel === 'critical'
|
|
587
|
-
? templates.critical
|
|
588
|
-
: warningLevel === 'high'
|
|
589
|
-
? templates.high
|
|
590
|
-
: templates.moderate;
|
|
591
|
-
return template
|
|
592
|
-
.replace('{percent}', String(estimatedContextPercent))
|
|
593
|
-
.replace('{turns}', String(totalTurns))
|
|
594
|
-
.replace('{cost}', totalCostUsd.toFixed(2));
|
|
317
|
+
return engineer;
|
|
595
318
|
}
|
|
596
|
-
function
|
|
597
|
-
|
|
598
|
-
case 'running':
|
|
599
|
-
return '⚡';
|
|
600
|
-
case 'success':
|
|
601
|
-
return '✅';
|
|
602
|
-
case 'error':
|
|
603
|
-
return '❌';
|
|
604
|
-
case 'warning':
|
|
605
|
-
return '⚠️';
|
|
606
|
-
}
|
|
319
|
+
function isEngineerAgent(agentId) {
|
|
320
|
+
return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
|
|
607
321
|
}
|
|
608
|
-
function
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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;
|
|
615
333
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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;
|
|
620
343
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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;
|
|
628
353
|
}
|
|
629
|
-
|
|
630
|
-
|
|
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
|
+
});
|
|
631
363
|
}
|
|
632
|
-
const snippet = text.replace(/\s+/g, ' ').trim();
|
|
633
|
-
const truncated = snippet.length > 60 ? snippet.slice(0, 60) + '...' : snippet;
|
|
634
|
-
return `${prefix}${truncated}`;
|
|
635
364
|
}
|
|
636
|
-
function
|
|
637
|
-
|
|
638
|
-
|
|
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));
|
|
639
384
|
}
|