@doingdev/opencode-claude-manager-plugin 0.1.46 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -31
- package/dist/index.d.ts +1 -1
- package/dist/manager/team-orchestrator.d.ts +50 -0
- package/dist/manager/team-orchestrator.js +360 -0
- package/dist/plugin/agent-hierarchy.d.ts +12 -34
- package/dist/plugin/agent-hierarchy.js +36 -129
- package/dist/plugin/claude-manager.plugin.js +233 -421
- package/dist/plugin/service-factory.d.ts +20 -3
- package/dist/plugin/service-factory.js +46 -1
- package/dist/prompts/registry.d.ts +1 -10
- package/dist/prompts/registry.js +42 -261
- package/dist/src/claude/claude-agent-sdk-adapter.js +2 -1
- package/dist/src/claude/session-live-tailer.js +2 -2
- package/dist/src/index.d.ts +1 -1
- package/dist/src/manager/git-operations.d.ts +10 -1
- package/dist/src/manager/git-operations.js +18 -3
- package/dist/src/manager/persistent-manager.d.ts +18 -6
- package/dist/src/manager/persistent-manager.js +19 -13
- package/dist/src/manager/session-controller.d.ts +7 -10
- package/dist/src/manager/session-controller.js +12 -62
- package/dist/src/manager/team-orchestrator.d.ts +50 -0
- package/dist/src/manager/team-orchestrator.js +360 -0
- package/dist/src/plugin/agent-hierarchy.d.ts +12 -26
- package/dist/src/plugin/agent-hierarchy.js +36 -99
- package/dist/src/plugin/claude-manager.plugin.js +257 -391
- package/dist/src/plugin/service-factory.d.ts +20 -3
- package/dist/src/plugin/service-factory.js +47 -9
- package/dist/src/prompts/registry.d.ts +1 -10
- package/dist/src/prompts/registry.js +41 -246
- package/dist/src/state/team-state-store.d.ts +17 -0
- package/dist/src/state/team-state-store.js +107 -0
- package/dist/src/team/roster.d.ts +5 -0
- package/dist/src/team/roster.js +38 -0
- package/dist/src/types/contracts.d.ts +55 -13
- package/dist/src/types/contracts.js +1 -1
- package/dist/state/team-state-store.d.ts +17 -0
- package/dist/state/team-state-store.js +107 -0
- package/dist/team/roster.d.ts +5 -0
- package/dist/team/roster.js +38 -0
- package/dist/test/claude-manager.plugin.test.js +55 -280
- package/dist/test/cto-active-team.test.d.ts +1 -0
- package/dist/test/cto-active-team.test.js +52 -0
- package/dist/test/git-operations.test.js +65 -1
- package/dist/test/persistent-manager.test.js +3 -3
- package/dist/test/prompt-registry.test.js +32 -252
- package/dist/test/report-claude-event.test.d.ts +1 -0
- package/dist/test/report-claude-event.test.js +246 -0
- package/dist/test/session-controller.test.js +27 -27
- package/dist/test/team-orchestrator.test.d.ts +1 -0
- package/dist/test/team-orchestrator.test.js +146 -0
- package/dist/test/team-state-store.test.d.ts +1 -0
- package/dist/test/team-state-store.test.js +72 -0
- package/dist/types/contracts.d.ts +54 -3
- package/dist/types/contracts.js +1 -1
- package/package.json +1 -1
|
@@ -1,396 +1,206 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin';
|
|
2
|
-
import {
|
|
2
|
+
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
3
|
+
import { isEngineerName } from '../team/roster.js';
|
|
3
4
|
import { discoverProjectClaudeFiles } from '../util/project-context.js';
|
|
4
|
-
import { AGENT_CTO,
|
|
5
|
-
import { getOrCreatePluginServices } from './service-factory.js';
|
|
5
|
+
import { AGENT_CTO, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
|
|
6
|
+
import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
|
|
7
|
+
const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
|
|
8
|
+
const MODE_ENUM = ['explore', 'implement', 'verify'];
|
|
6
9
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
? '⚡ Claude Code: Resuming session...'
|
|
18
|
-
: '⚡ Claude Code: Initializing...',
|
|
19
|
-
metadata: {
|
|
20
|
-
status: 'running',
|
|
21
|
-
sessionId: 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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// ignore
|
|
21
|
+
},
|
|
22
|
+
'chat.message': async (input) => {
|
|
23
|
+
if (input.agent === AGENT_CTO) {
|
|
24
|
+
// Adopt the persisted active team if one exists, so a new CTO session
|
|
25
|
+
// does not orphan previously created engineers and wrapper memory.
|
|
26
|
+
const persistedTeamId = await getPersistedActiveTeam(worktree);
|
|
27
|
+
const activeTeamId = persistedTeamId ?? input.sessionID;
|
|
28
|
+
setActiveTeamSession(worktree, activeTeamId);
|
|
29
|
+
if (!persistedTeamId) {
|
|
30
|
+
// First CTO session for this worktree — persist this session as active team.
|
|
31
|
+
await setPersistedActiveTeam(worktree, activeTeamId);
|
|
114
32
|
}
|
|
115
|
-
|
|
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
|
-
});
|
|
33
|
+
return;
|
|
128
34
|
}
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
},
|
|
35
|
+
if (input.agent && isEngineerAgent(input.agent)) {
|
|
36
|
+
const engineer = engineerFromAgent(input.agent);
|
|
37
|
+
const existing = getWrapperSessionMapping(worktree, input.sessionID);
|
|
38
|
+
const persisted = existing ??
|
|
39
|
+
(await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
|
|
40
|
+
const teamId = persisted?.teamId ?? (await resolveTeamId(worktree, input.sessionID));
|
|
41
|
+
setWrapperSessionMapping(worktree, input.sessionID, {
|
|
42
|
+
teamId,
|
|
43
|
+
engineer,
|
|
139
44
|
});
|
|
45
|
+
await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
|
|
140
46
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
metadata: {
|
|
146
|
-
status: 'running',
|
|
147
|
-
sessionId: event.sessionId,
|
|
148
|
-
type: event.type,
|
|
149
|
-
delta,
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
else if (event.type === 'error') {
|
|
154
|
-
context.metadata({
|
|
155
|
-
title: `❌ Claude Code: Error`,
|
|
156
|
-
metadata: {
|
|
157
|
-
status: 'error',
|
|
158
|
-
sessionId: event.sessionId,
|
|
159
|
-
error: event.text.slice(0, 200),
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
showToastIfAvailable(context, `Claude Code error: ${event.text.slice(0, 100)}`);
|
|
47
|
+
},
|
|
48
|
+
'experimental.chat.system.transform': async (input, output) => {
|
|
49
|
+
if (!input.sessionID) {
|
|
50
|
+
return;
|
|
163
51
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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);
|
|
52
|
+
const existing = getWrapperSessionMapping(worktree, input.sessionID);
|
|
53
|
+
const persisted = existing ??
|
|
54
|
+
(await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
|
|
55
|
+
if (!persisted) {
|
|
56
|
+
return;
|
|
186
57
|
}
|
|
187
|
-
|
|
188
|
-
|
|
58
|
+
const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree, persisted.teamId, persisted.engineer);
|
|
59
|
+
if (wrapperContext) {
|
|
60
|
+
output.system.push(wrapperContext);
|
|
189
61
|
}
|
|
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
62
|
},
|
|
220
63
|
tool: {
|
|
221
|
-
|
|
222
|
-
description:
|
|
223
|
-
'Read-only exploration of the codebase. ' +
|
|
224
|
-
'Preferred first step before implementation.',
|
|
225
|
-
args: {
|
|
226
|
-
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(),
|
|
233
|
-
},
|
|
234
|
-
async execute(args, context) {
|
|
235
|
-
return executeDelegate({ ...args, mode: 'plan' }, context);
|
|
236
|
-
},
|
|
237
|
-
}),
|
|
238
|
-
implement: tool({
|
|
239
|
-
description: 'Implement code changes - can read, edit, and create files. ' +
|
|
240
|
-
'Use after exploration to make changes.',
|
|
64
|
+
claude: tool({
|
|
65
|
+
description: "Run work through this named engineer's persistent Claude Code session. The session remembers prior turns for this engineer.",
|
|
241
66
|
args: {
|
|
67
|
+
mode: tool.schema.enum(MODE_ENUM),
|
|
242
68
|
message: tool.schema.string().min(1),
|
|
243
|
-
model: tool.schema
|
|
244
|
-
.enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
|
|
245
|
-
.optional(),
|
|
246
|
-
effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
|
|
247
|
-
freshSession: tool.schema.boolean().default(false),
|
|
248
|
-
cwd: tool.schema.string().optional(),
|
|
69
|
+
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
249
70
|
},
|
|
250
71
|
async execute(args, context) {
|
|
251
|
-
|
|
72
|
+
const engineer = engineerFromAgent(context.agent);
|
|
73
|
+
const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
|
|
74
|
+
const persisted = existing ??
|
|
75
|
+
(await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
|
|
76
|
+
const teamId = persisted?.teamId ?? (await resolveTeamId(context.worktree, context.sessionID));
|
|
77
|
+
setWrapperSessionMapping(context.worktree, context.sessionID, {
|
|
78
|
+
teamId,
|
|
79
|
+
engineer,
|
|
80
|
+
});
|
|
81
|
+
await services.orchestrator.recordWrapperSession(context.worktree, teamId, engineer, context.sessionID);
|
|
82
|
+
const result = await runEngineerAssignment({
|
|
83
|
+
teamId,
|
|
84
|
+
engineer,
|
|
85
|
+
mode: args.mode,
|
|
86
|
+
message: args.message,
|
|
87
|
+
model: args.model,
|
|
88
|
+
}, context);
|
|
89
|
+
return result.finalText;
|
|
252
90
|
},
|
|
253
91
|
}),
|
|
254
|
-
|
|
255
|
-
description: '
|
|
256
|
-
'Preserves state while reducing token usage.',
|
|
92
|
+
team_status: tool({
|
|
93
|
+
description: 'Show the current CTO team state: named engineers, wrapper session IDs, Claude session IDs, busy flags, wrapper memory, and context snapshots.',
|
|
257
94
|
args: {
|
|
258
|
-
|
|
95
|
+
teamId: tool.schema.string().optional(),
|
|
259
96
|
},
|
|
260
97
|
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
|
-
},
|
|
98
|
+
const teamId = args.teamId ?? getActiveTeamSession(context.worktree) ?? context.sessionID;
|
|
99
|
+
annotateToolRun(context, 'Reading team status', {
|
|
100
|
+
teamId,
|
|
274
101
|
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
finalText: result.finalText,
|
|
278
|
-
turns: result.turns,
|
|
279
|
-
totalCostUsd: result.totalCostUsd,
|
|
280
|
-
context: snap,
|
|
281
|
-
contextWarning,
|
|
282
|
-
}, null, 2);
|
|
102
|
+
const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
|
|
103
|
+
return JSON.stringify(team, null, 2);
|
|
283
104
|
},
|
|
284
105
|
}),
|
|
285
106
|
git_diff: tool({
|
|
286
|
-
description: '
|
|
107
|
+
description: 'Show diff of uncommitted changes. Use paths to filter to specific files or use ref to compare against another branch, tag, or commit.',
|
|
287
108
|
args: {
|
|
288
|
-
|
|
109
|
+
paths: tool.schema.string().array().optional(),
|
|
110
|
+
staged: tool.schema.boolean().optional(),
|
|
111
|
+
ref: tool.schema.string().optional(),
|
|
289
112
|
},
|
|
290
|
-
async execute(
|
|
291
|
-
annotateToolRun(context, 'Running git diff', {
|
|
292
|
-
|
|
113
|
+
async execute(args, context) {
|
|
114
|
+
annotateToolRun(context, 'Running git diff', {
|
|
115
|
+
paths: args.paths,
|
|
116
|
+
staged: args.staged,
|
|
117
|
+
ref: args.ref,
|
|
118
|
+
});
|
|
119
|
+
const result = await services.manager.gitDiff({
|
|
120
|
+
paths: args.paths?.filter((path) => path !== undefined),
|
|
121
|
+
staged: args.staged,
|
|
122
|
+
ref: args.ref,
|
|
123
|
+
});
|
|
293
124
|
return JSON.stringify(result, null, 2);
|
|
294
125
|
},
|
|
295
126
|
}),
|
|
296
127
|
git_commit: tool({
|
|
297
|
-
description: 'Stage all changes and commit with the given message.',
|
|
128
|
+
description: 'Stage all changes and create a commit with the given message.',
|
|
298
129
|
args: {
|
|
299
130
|
message: tool.schema.string().min(1),
|
|
300
|
-
cwd: tool.schema.string().optional(),
|
|
301
131
|
},
|
|
302
132
|
async execute(args, context) {
|
|
303
|
-
annotateToolRun(context, 'Committing changes', {
|
|
304
|
-
message: args.message,
|
|
305
|
-
});
|
|
133
|
+
annotateToolRun(context, 'Committing changes', { message: args.message });
|
|
306
134
|
const result = await services.manager.gitCommit(args.message);
|
|
307
135
|
return JSON.stringify(result, null, 2);
|
|
308
136
|
},
|
|
309
137
|
}),
|
|
310
138
|
git_reset: tool({
|
|
311
|
-
description: '
|
|
312
|
-
args: {
|
|
313
|
-
cwd: tool.schema.string().optional(),
|
|
314
|
-
},
|
|
139
|
+
description: 'Discard all uncommitted changes by running git reset --hard HEAD and git clean -fd.',
|
|
140
|
+
args: {},
|
|
315
141
|
async execute(_args, context) {
|
|
316
142
|
annotateToolRun(context, 'Resetting working directory', {});
|
|
317
143
|
const result = await services.manager.gitReset();
|
|
318
144
|
return JSON.stringify(result, null, 2);
|
|
319
145
|
},
|
|
320
146
|
}),
|
|
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 });
|
|
147
|
+
git_status: tool({
|
|
148
|
+
description: 'Show working tree status in short format and whether the tree is clean.',
|
|
149
|
+
args: {},
|
|
150
|
+
async execute(_args, context) {
|
|
151
|
+
annotateToolRun(context, 'Checking git status', {});
|
|
152
|
+
const result = await services.manager.gitStatus();
|
|
153
|
+
return JSON.stringify(result, null, 2);
|
|
334
154
|
},
|
|
335
155
|
}),
|
|
336
|
-
|
|
337
|
-
description: '
|
|
156
|
+
git_log: tool({
|
|
157
|
+
description: 'Show recent commits in short format. Defaults to 5 commits.',
|
|
338
158
|
args: {
|
|
339
|
-
|
|
159
|
+
count: tool.schema.number().optional(),
|
|
340
160
|
},
|
|
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);
|
|
161
|
+
async execute(args, context) {
|
|
162
|
+
annotateToolRun(context, 'Fetching git log', { count: args.count });
|
|
163
|
+
return services.manager.gitLog(args.count ?? 5);
|
|
351
164
|
},
|
|
352
165
|
}),
|
|
353
166
|
list_transcripts: tool({
|
|
354
|
-
description: 'List available session transcripts or inspect
|
|
167
|
+
description: 'List available Claude session transcripts or inspect one transcript by session ID.',
|
|
355
168
|
args: {
|
|
356
|
-
cwd: tool.schema.string().optional(),
|
|
357
169
|
sessionId: tool.schema.string().optional(),
|
|
358
170
|
},
|
|
359
171
|
async execute(args, context) {
|
|
360
172
|
annotateToolRun(context, 'Inspecting Claude session history', {});
|
|
361
|
-
const cwd = args.cwd ?? context.worktree;
|
|
362
173
|
if (args.sessionId) {
|
|
363
174
|
const [sdkTranscript, localEvents] = await Promise.all([
|
|
364
|
-
services.sessions.getTranscript(args.sessionId,
|
|
365
|
-
services.manager.getTranscriptEvents(
|
|
175
|
+
services.sessions.getTranscript(args.sessionId, context.worktree),
|
|
176
|
+
services.manager.getTranscriptEvents(context.worktree, args.sessionId),
|
|
366
177
|
]);
|
|
367
178
|
return JSON.stringify({
|
|
368
179
|
sdkTranscript,
|
|
369
180
|
localEvents: localEvents.length > 0 ? localEvents : undefined,
|
|
370
181
|
}, null, 2);
|
|
371
182
|
}
|
|
372
|
-
const sessions = await services.sessions.listSessions(
|
|
183
|
+
const sessions = await services.sessions.listSessions(context.worktree);
|
|
373
184
|
return JSON.stringify(sessions, null, 2);
|
|
374
185
|
},
|
|
375
186
|
}),
|
|
376
187
|
list_history: tool({
|
|
377
|
-
description: 'List
|
|
188
|
+
description: 'List saved CTO teams for this worktree or inspect one team by ID.',
|
|
378
189
|
args: {
|
|
379
|
-
|
|
380
|
-
runId: tool.schema.string().optional(),
|
|
190
|
+
teamId: tool.schema.string().optional(),
|
|
381
191
|
},
|
|
382
192
|
async execute(args, context) {
|
|
383
|
-
annotateToolRun(context, 'Reading
|
|
384
|
-
if (args.
|
|
385
|
-
const
|
|
386
|
-
return JSON.stringify(
|
|
193
|
+
annotateToolRun(context, 'Reading saved team history', {});
|
|
194
|
+
if (args.teamId) {
|
|
195
|
+
const team = await services.teamStore.getTeam(context.worktree, args.teamId);
|
|
196
|
+
return JSON.stringify(team, null, 2);
|
|
387
197
|
}
|
|
388
|
-
const
|
|
389
|
-
return JSON.stringify(
|
|
198
|
+
const teams = await services.orchestrator.listTeams(context.worktree);
|
|
199
|
+
return JSON.stringify(teams, null, 2);
|
|
390
200
|
},
|
|
391
201
|
}),
|
|
392
202
|
approval_policy: tool({
|
|
393
|
-
description: 'View the current tool approval policy
|
|
203
|
+
description: 'View the current tool approval policy.',
|
|
394
204
|
args: {},
|
|
395
205
|
async execute(_args, context) {
|
|
396
206
|
annotateToolRun(context, 'Reading approval policy', {});
|
|
@@ -398,8 +208,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
398
208
|
},
|
|
399
209
|
}),
|
|
400
210
|
approval_decisions: tool({
|
|
401
|
-
description: 'View recent tool approval decisions.
|
|
402
|
-
'Use deniedOnly to see only denied calls.',
|
|
211
|
+
description: 'View recent tool approval decisions. Use deniedOnly to show only denied calls.',
|
|
403
212
|
args: {
|
|
404
213
|
limit: tool.schema.number().optional(),
|
|
405
214
|
deniedOnly: tool.schema.boolean().optional(),
|
|
@@ -413,8 +222,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
413
222
|
},
|
|
414
223
|
}),
|
|
415
224
|
approval_update: tool({
|
|
416
|
-
description: 'Update the tool approval policy. Add
|
|
417
|
-
'Rules are evaluated top-to-bottom; first match wins.',
|
|
225
|
+
description: 'Update the tool approval policy. Add or remove rules, change the default action, enable or disable approvals, or clear decision history.',
|
|
418
226
|
args: {
|
|
419
227
|
action: tool.schema.enum([
|
|
420
228
|
'addRule',
|
|
@@ -455,13 +263,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
455
263
|
return JSON.stringify({ error: 'removeRule requires ruleId' });
|
|
456
264
|
}
|
|
457
265
|
const removed = services.approvalManager.removeRule(args.ruleId);
|
|
458
|
-
return JSON.stringify({ removed });
|
|
266
|
+
return JSON.stringify({ removed }, null, 2);
|
|
459
267
|
}
|
|
460
268
|
else if (args.action === 'setDefault') {
|
|
461
269
|
if (!args.defaultAction) {
|
|
462
|
-
return JSON.stringify({
|
|
463
|
-
error: 'setDefault requires defaultAction',
|
|
464
|
-
});
|
|
270
|
+
return JSON.stringify({ error: 'setDefault requires defaultAction' });
|
|
465
271
|
}
|
|
466
272
|
services.approvalManager.setDefaultAction(args.defaultAction);
|
|
467
273
|
}
|
|
@@ -480,84 +286,144 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
480
286
|
},
|
|
481
287
|
};
|
|
482
288
|
};
|
|
483
|
-
function
|
|
484
|
-
const
|
|
289
|
+
async function runEngineerAssignment(input, context) {
|
|
290
|
+
const services = getOrCreatePluginServices(context.worktree);
|
|
291
|
+
annotateToolRun(context, `Assigning ${input.engineer}`, {
|
|
292
|
+
teamId: input.teamId,
|
|
293
|
+
mode: input.mode,
|
|
294
|
+
});
|
|
295
|
+
const result = await services.orchestrator.dispatchEngineer({
|
|
296
|
+
teamId: input.teamId,
|
|
297
|
+
cwd: context.worktree,
|
|
298
|
+
engineer: input.engineer,
|
|
299
|
+
mode: input.mode,
|
|
300
|
+
message: input.message,
|
|
301
|
+
model: input.model,
|
|
302
|
+
abortSignal: context.abort,
|
|
303
|
+
onEvent: (event) => reportClaudeEvent(context, input.engineer, event),
|
|
304
|
+
});
|
|
305
|
+
await services.orchestrator.recordWrapperExchange(context.worktree, input.teamId, input.engineer, context.sessionID, input.mode, input.message, result.finalText);
|
|
485
306
|
context.metadata({
|
|
486
|
-
title:
|
|
487
|
-
metadata: {
|
|
307
|
+
title: `✅ ${input.engineer} finished`,
|
|
308
|
+
metadata: {
|
|
309
|
+
teamId: result.teamId,
|
|
310
|
+
engineer: result.engineer,
|
|
311
|
+
mode: result.mode,
|
|
312
|
+
sessionId: result.sessionId,
|
|
313
|
+
turns: result.turns,
|
|
314
|
+
contextWarning: formatContextWarning(result.context),
|
|
315
|
+
},
|
|
488
316
|
});
|
|
317
|
+
return result;
|
|
489
318
|
}
|
|
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)}`);
|
|
319
|
+
function engineerFromAgent(agentId) {
|
|
320
|
+
const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === agentId);
|
|
321
|
+
const engineer = engineerEntry?.[0];
|
|
322
|
+
if (!engineer || !isEngineerName(engineer)) {
|
|
323
|
+
throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
|
|
500
324
|
}
|
|
501
|
-
return
|
|
325
|
+
return engineer;
|
|
502
326
|
}
|
|
503
|
-
function
|
|
504
|
-
|
|
505
|
-
if (warningLevel === 'ok' || estimatedContextPercent === null) {
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
const templates = managerPromptRegistry.contextWarnings;
|
|
509
|
-
const template = warningLevel === 'critical'
|
|
510
|
-
? templates.critical
|
|
511
|
-
: warningLevel === 'high'
|
|
512
|
-
? templates.high
|
|
513
|
-
: templates.moderate;
|
|
514
|
-
return template
|
|
515
|
-
.replace('{percent}', String(estimatedContextPercent))
|
|
516
|
-
.replace('{turns}', String(totalTurns))
|
|
517
|
-
.replace('{cost}', totalCostUsd.toFixed(2));
|
|
327
|
+
function isEngineerAgent(agentId) {
|
|
328
|
+
return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
|
|
518
329
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return '❌';
|
|
527
|
-
case 'warning':
|
|
528
|
-
return '⚠️';
|
|
529
|
-
}
|
|
330
|
+
/**
|
|
331
|
+
* Resolves the team ID for an engineer session.
|
|
332
|
+
* Reads the persisted active team first (survives process restarts), then
|
|
333
|
+
* falls back to the in-memory registry, then to the raw session ID as a last resort.
|
|
334
|
+
*/
|
|
335
|
+
async function resolveTeamId(worktree, sessionID) {
|
|
336
|
+
return (await getPersistedActiveTeam(worktree)) ?? getActiveTeamSession(worktree) ?? sessionID;
|
|
530
337
|
}
|
|
531
|
-
function
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
338
|
+
function reportClaudeEvent(context, engineer, event) {
|
|
339
|
+
if (event.type === 'error') {
|
|
340
|
+
context.metadata({
|
|
341
|
+
title: `❌ ${engineer} hit an error`,
|
|
342
|
+
metadata: {
|
|
343
|
+
engineer,
|
|
344
|
+
sessionId: event.sessionId,
|
|
345
|
+
error: event.text.slice(0, 200),
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
538
349
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
350
|
+
if (event.type === 'init') {
|
|
351
|
+
context.metadata({
|
|
352
|
+
title: `⚡ ${engineer} session ready`,
|
|
353
|
+
metadata: {
|
|
354
|
+
engineer,
|
|
355
|
+
sessionId: event.sessionId,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
543
359
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
360
|
+
if (event.type === 'tool_call') {
|
|
361
|
+
let toolName;
|
|
362
|
+
let toolId;
|
|
363
|
+
let toolArgs;
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(event.text);
|
|
366
|
+
toolName = parsed.name;
|
|
367
|
+
toolId = parsed.id;
|
|
368
|
+
// Some SDK versions serialize the input object as a JSON string inside the outer JSON.
|
|
369
|
+
// Try to double-decode it so callers always receive a plain object.
|
|
370
|
+
if (typeof parsed.input === 'string') {
|
|
371
|
+
try {
|
|
372
|
+
toolArgs = JSON.parse(parsed.input);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
toolArgs = parsed.input;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
toolArgs = parsed.input;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// event.text is not valid JSON — fall back to generic title
|
|
384
|
+
}
|
|
385
|
+
context.metadata({
|
|
386
|
+
title: toolName
|
|
387
|
+
? `⚡ ${engineer} → ${toolName}`
|
|
388
|
+
: `⚡ ${engineer} is using Claude Code tools`,
|
|
389
|
+
metadata: {
|
|
390
|
+
engineer,
|
|
391
|
+
sessionId: event.sessionId,
|
|
392
|
+
...(toolName !== undefined && { toolName }),
|
|
393
|
+
...(toolId !== undefined && { toolId }),
|
|
394
|
+
...(toolArgs !== undefined && { toolArgs }),
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
551
398
|
}
|
|
552
|
-
|
|
553
|
-
|
|
399
|
+
if (event.type === 'assistant' || event.type === 'partial') {
|
|
400
|
+
context.metadata({
|
|
401
|
+
title: `⚡ ${engineer} is working`,
|
|
402
|
+
metadata: {
|
|
403
|
+
engineer,
|
|
404
|
+
sessionId: event.sessionId,
|
|
405
|
+
preview: event.text.slice(0, 160),
|
|
406
|
+
},
|
|
407
|
+
});
|
|
554
408
|
}
|
|
555
|
-
const snippet = text.replace(/\s+/g, ' ').trim();
|
|
556
|
-
const truncated = snippet.length > 60 ? snippet.slice(0, 60) + '...' : snippet;
|
|
557
|
-
return `${prefix}${truncated}`;
|
|
558
409
|
}
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
410
|
+
function annotateToolRun(context, title, metadata) {
|
|
411
|
+
context.metadata({
|
|
412
|
+
title,
|
|
413
|
+
metadata,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function formatContextWarning(context) {
|
|
417
|
+
if (context.warningLevel === 'ok' || context.estimatedContextPercent === null) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
const template = context.warningLevel === 'critical'
|
|
421
|
+
? managerPromptRegistry.contextWarnings.critical
|
|
422
|
+
: context.warningLevel === 'high'
|
|
423
|
+
? managerPromptRegistry.contextWarnings.high
|
|
424
|
+
: managerPromptRegistry.contextWarnings.moderate;
|
|
425
|
+
return template
|
|
426
|
+
.replace('{percent}', String(context.estimatedContextPercent))
|
|
427
|
+
.replace('{turns}', String(context.totalTurns))
|
|
428
|
+
.replace('{cost}', context.totalCostUsd.toFixed(2));
|
|
563
429
|
}
|