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