@doingdev/opencode-claude-manager-plugin 0.1.10 → 0.1.12
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 +26 -29
- package/dist/claude/claude-agent-sdk-adapter.d.ts +2 -1
- package/dist/claude/claude-agent-sdk-adapter.js +248 -61
- package/dist/index.d.ts +1 -1
- package/dist/manager/context-tracker.d.ts +33 -0
- package/dist/manager/context-tracker.js +108 -0
- package/dist/manager/git-operations.d.ts +12 -0
- package/dist/manager/git-operations.js +76 -0
- package/dist/manager/manager-orchestrator.d.ts +1 -4
- package/dist/manager/manager-orchestrator.js +37 -53
- package/dist/manager/persistent-manager.d.ts +71 -0
- package/dist/manager/persistent-manager.js +167 -0
- package/dist/manager/session-controller.d.ts +38 -0
- package/dist/manager/session-controller.js +135 -0
- package/dist/manager/task-planner.d.ts +2 -2
- package/dist/manager/task-planner.js +4 -31
- package/dist/plugin/claude-manager.plugin.d.ts +0 -2
- package/dist/plugin/claude-manager.plugin.js +202 -199
- package/dist/plugin/service-factory.d.ts +4 -3
- package/dist/plugin/service-factory.js +14 -4
- package/dist/prompts/registry.js +42 -8
- package/dist/state/file-run-state-store.d.ts +5 -5
- package/dist/state/file-run-state-store.js +1 -11
- package/dist/state/transcript-store.d.ts +15 -0
- package/dist/state/transcript-store.js +44 -0
- package/dist/types/contracts.d.ts +68 -45
- package/dist/util/fs-helpers.d.ts +2 -0
- package/dist/util/fs-helpers.js +12 -0
- package/dist/util/transcript-append.d.ts +7 -0
- package/dist/util/transcript-append.js +29 -0
- package/package.json +12 -10
|
@@ -2,11 +2,15 @@ import { tool } from '@opencode-ai/plugin';
|
|
|
2
2
|
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
3
3
|
import { getOrCreatePluginServices } from './service-factory.js';
|
|
4
4
|
const MANAGER_TOOL_IDS = [
|
|
5
|
-
'
|
|
5
|
+
'claude_manager_send',
|
|
6
|
+
'claude_manager_git_diff',
|
|
7
|
+
'claude_manager_git_commit',
|
|
8
|
+
'claude_manager_git_reset',
|
|
9
|
+
'claude_manager_clear',
|
|
10
|
+
'claude_manager_status',
|
|
6
11
|
'claude_manager_metadata',
|
|
7
12
|
'claude_manager_sessions',
|
|
8
13
|
'claude_manager_runs',
|
|
9
|
-
'claude_manager_cleanup_run',
|
|
10
14
|
];
|
|
11
15
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
12
16
|
const services = getOrCreatePluginServices(worktree);
|
|
@@ -16,37 +20,29 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
16
20
|
config.command ??= {};
|
|
17
21
|
config.permission ??= {};
|
|
18
22
|
const globalPermissions = config.permission;
|
|
19
|
-
const managerPermissions = {
|
|
20
|
-
|
|
21
|
-
claude_manager_metadata: 'allow',
|
|
22
|
-
claude_manager_sessions: 'allow',
|
|
23
|
-
claude_manager_runs: 'allow',
|
|
24
|
-
claude_manager_cleanup_run: 'allow',
|
|
25
|
-
};
|
|
26
|
-
const researchPermissions = {
|
|
27
|
-
claude_manager_run: 'deny',
|
|
28
|
-
claude_manager_metadata: 'allow',
|
|
29
|
-
claude_manager_sessions: 'allow',
|
|
30
|
-
claude_manager_runs: 'allow',
|
|
31
|
-
claude_manager_cleanup_run: 'deny',
|
|
32
|
-
};
|
|
23
|
+
const managerPermissions = {};
|
|
24
|
+
const researchPermissions = {};
|
|
33
25
|
for (const toolId of MANAGER_TOOL_IDS) {
|
|
34
26
|
globalPermissions[toolId] ??= 'deny';
|
|
27
|
+
managerPermissions[toolId] = 'allow';
|
|
28
|
+
// Research agent can inspect but not send or modify
|
|
29
|
+
researchPermissions[toolId] =
|
|
30
|
+
toolId === 'claude_manager_send' ||
|
|
31
|
+
toolId === 'claude_manager_git_commit' ||
|
|
32
|
+
toolId === 'claude_manager_git_reset' ||
|
|
33
|
+
toolId === 'claude_manager_clear'
|
|
34
|
+
? 'deny'
|
|
35
|
+
: 'allow';
|
|
35
36
|
}
|
|
36
37
|
config.agent['claude-manager'] ??= {
|
|
37
|
-
description: 'Primary agent that
|
|
38
|
+
description: 'Primary agent that operates Claude Code through a persistent session, reviews work via git diff, and commits/resets changes.',
|
|
38
39
|
mode: 'primary',
|
|
39
40
|
color: 'accent',
|
|
40
41
|
permission: {
|
|
41
42
|
'*': 'deny',
|
|
42
43
|
...managerPermissions,
|
|
43
44
|
},
|
|
44
|
-
prompt:
|
|
45
|
-
managerPromptRegistry.managerSystemPrompt,
|
|
46
|
-
'When Claude Code delegation is useful, prefer the claude_manager_run tool instead of simulating the work yourself.',
|
|
47
|
-
'Use claude_manager_metadata to inspect available Claude commands, skills, and hooks before making assumptions.',
|
|
48
|
-
'Use claude_manager_sessions and claude_manager_runs to inspect prior work before starting a new Claude session.',
|
|
49
|
-
].join(' '),
|
|
45
|
+
prompt: managerPromptRegistry.managerSystemPrompt,
|
|
50
46
|
};
|
|
51
47
|
config.agent['claude-manager-research'] ??= {
|
|
52
48
|
description: 'Subagent that inspects Claude metadata, prior sessions, and manager runs without changing repository state.',
|
|
@@ -57,11 +53,20 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
57
53
|
...researchPermissions,
|
|
58
54
|
},
|
|
59
55
|
prompt: [
|
|
60
|
-
managerPromptRegistry.subagentSystemPrompt,
|
|
61
56
|
'Focus on inspection and summarization.',
|
|
62
|
-
'
|
|
57
|
+
'Use claude_manager_status, claude_manager_metadata, claude_manager_sessions, and claude_manager_runs to gather information.',
|
|
63
58
|
].join(' '),
|
|
64
59
|
};
|
|
60
|
+
config.command['claude-run'] ??= {
|
|
61
|
+
description: 'Send a task to the persistent Claude Code session.',
|
|
62
|
+
agent: 'claude-manager',
|
|
63
|
+
subtask: true,
|
|
64
|
+
template: [
|
|
65
|
+
'Use claude_manager_send to send the following task to Claude Code:',
|
|
66
|
+
'$ARGUMENTS',
|
|
67
|
+
'After it completes, review the result and use claude_manager_git_diff if changes were expected. Commit or reset accordingly.',
|
|
68
|
+
].join('\n\n'),
|
|
69
|
+
};
|
|
65
70
|
config.command['claude-metadata'] ??= {
|
|
66
71
|
description: 'Inspect bundled Claude commands, skills, hooks, and agents.',
|
|
67
72
|
agent: 'claude-manager-research',
|
|
@@ -71,16 +76,6 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
71
76
|
'Summarize Claude commands, skills, hooks, discovered agents, and important config files.',
|
|
72
77
|
].join(' '),
|
|
73
78
|
};
|
|
74
|
-
config.command['claude-run'] ??= {
|
|
75
|
-
description: 'Delegate a task to Claude Code through the manager plugin.',
|
|
76
|
-
agent: 'claude-manager',
|
|
77
|
-
subtask: true,
|
|
78
|
-
template: [
|
|
79
|
-
'Call claude_manager_run immediately for the following task:',
|
|
80
|
-
'$ARGUMENTS',
|
|
81
|
-
'Avoid planning narration before the tool call. After it completes, return a concise result summary.',
|
|
82
|
-
].join('\n\n'),
|
|
83
|
-
};
|
|
84
79
|
config.command['claude-sessions'] ??= {
|
|
85
80
|
description: 'Inspect Claude session history and manager run records.',
|
|
86
81
|
agent: 'claude-manager-research',
|
|
@@ -99,41 +94,153 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
99
94
|
}
|
|
100
95
|
},
|
|
101
96
|
tool: {
|
|
102
|
-
|
|
103
|
-
description: '
|
|
97
|
+
claude_manager_send: tool({
|
|
98
|
+
description: 'Send a message to the persistent Claude Code session. ' +
|
|
99
|
+
'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
|
|
100
|
+
'Returns the assistant response and current context health snapshot.',
|
|
104
101
|
args: {
|
|
105
|
-
|
|
106
|
-
mode: tool.schema.enum(['auto', 'single', 'split']).default('auto'),
|
|
107
|
-
maxSubagents: tool.schema.number().int().min(1).max(8).default(3),
|
|
108
|
-
useWorktrees: tool.schema.boolean().default(true),
|
|
109
|
-
includeProjectSettings: tool.schema.boolean().default(true),
|
|
102
|
+
message: tool.schema.string().min(1),
|
|
110
103
|
model: tool.schema.string().optional(),
|
|
111
104
|
cwd: tool.schema.string().optional(),
|
|
112
105
|
},
|
|
113
106
|
async execute(args, context) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
const cwd = args.cwd ?? context.worktree;
|
|
108
|
+
const hasActiveSession = services.manager.getStatus().sessionId !== null;
|
|
109
|
+
context.metadata({
|
|
110
|
+
title: hasActiveSession
|
|
111
|
+
? 'Claude Code: Resuming session...'
|
|
112
|
+
: 'Claude Code: Initializing...',
|
|
113
|
+
metadata: { sessionId: services.manager.getStatus().sessionId },
|
|
117
114
|
});
|
|
118
|
-
let
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
115
|
+
let turnsSoFar = 0;
|
|
116
|
+
let costSoFar = 0;
|
|
117
|
+
const result = await services.manager.sendMessage(cwd, args.message, { model: args.model }, (event) => {
|
|
118
|
+
if (event.turns !== undefined) {
|
|
119
|
+
turnsSoFar = event.turns;
|
|
120
|
+
}
|
|
121
|
+
if (event.totalCostUsd !== undefined) {
|
|
122
|
+
costSoFar = event.totalCostUsd;
|
|
123
|
+
}
|
|
124
|
+
const costLabel = `$${costSoFar.toFixed(4)}`;
|
|
125
|
+
if (event.type === 'tool_call') {
|
|
126
|
+
let toolName = 'tool';
|
|
127
|
+
try {
|
|
128
|
+
toolName = JSON.parse(event.text).name ?? 'tool';
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// ignore parse errors
|
|
132
|
+
}
|
|
133
|
+
context.metadata({
|
|
134
|
+
title: `Claude Code: Running ${toolName}... (${turnsSoFar} turns, ${costLabel})`,
|
|
135
|
+
metadata: {
|
|
136
|
+
sessionId: event.sessionId,
|
|
137
|
+
type: event.type,
|
|
138
|
+
preview: event.text.slice(0, 200),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
132
141
|
}
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
else if (event.type === 'assistant') {
|
|
143
|
+
context.metadata({
|
|
144
|
+
title: `Claude Code: Thinking... (${turnsSoFar} turns, ${costLabel})`,
|
|
145
|
+
metadata: {
|
|
146
|
+
sessionId: event.sessionId,
|
|
147
|
+
type: event.type,
|
|
148
|
+
preview: event.text.slice(0, 200),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
const costLabel = `$${(result.totalCostUsd ?? 0).toFixed(4)}`;
|
|
154
|
+
const turns = result.turns ?? 0;
|
|
155
|
+
const contextWarning = formatContextWarning(result.context);
|
|
156
|
+
if (contextWarning) {
|
|
157
|
+
context.metadata({
|
|
158
|
+
title: `Claude Code: Context at ${result.context.estimatedContextPercent}% (${turns} turns)`,
|
|
159
|
+
metadata: { sessionId: result.sessionId, contextWarning },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
context.metadata({
|
|
164
|
+
title: `Claude Code: Complete (${turns} turns, ${costLabel})`,
|
|
165
|
+
metadata: { sessionId: result.sessionId },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return JSON.stringify({
|
|
169
|
+
sessionId: result.sessionId,
|
|
170
|
+
finalText: result.finalText,
|
|
171
|
+
turns: result.turns,
|
|
172
|
+
totalCostUsd: result.totalCostUsd,
|
|
173
|
+
context: result.context,
|
|
174
|
+
contextWarning,
|
|
175
|
+
}, null, 2);
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
claude_manager_git_diff: tool({
|
|
179
|
+
description: 'Run git diff to see all current changes (staged + unstaged) relative to HEAD.',
|
|
180
|
+
args: {
|
|
181
|
+
cwd: tool.schema.string().optional(),
|
|
182
|
+
},
|
|
183
|
+
async execute(_args, context) {
|
|
184
|
+
annotateToolRun(context, 'Running git diff', {});
|
|
185
|
+
const result = await services.manager.gitDiff();
|
|
186
|
+
return JSON.stringify(result, null, 2);
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
claude_manager_git_commit: tool({
|
|
190
|
+
description: 'Stage all changes and commit with the given message.',
|
|
191
|
+
args: {
|
|
192
|
+
message: tool.schema.string().min(1),
|
|
193
|
+
cwd: tool.schema.string().optional(),
|
|
194
|
+
},
|
|
195
|
+
async execute(args, context) {
|
|
196
|
+
annotateToolRun(context, 'Committing changes', {
|
|
197
|
+
message: args.message,
|
|
198
|
+
});
|
|
199
|
+
const result = await services.manager.gitCommit(args.message);
|
|
200
|
+
return JSON.stringify(result, null, 2);
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
claude_manager_git_reset: tool({
|
|
204
|
+
description: 'Run git reset --hard HEAD and git clean -fd to discard ALL uncommitted changes and untracked files.',
|
|
205
|
+
args: {
|
|
206
|
+
cwd: tool.schema.string().optional(),
|
|
207
|
+
},
|
|
208
|
+
async execute(_args, context) {
|
|
209
|
+
annotateToolRun(context, 'Resetting working directory', {});
|
|
210
|
+
const result = await services.manager.gitReset();
|
|
211
|
+
return JSON.stringify(result, null, 2);
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
claude_manager_clear: tool({
|
|
215
|
+
description: 'Clear the active Claude Code session. The next send will start a fresh session. ' +
|
|
216
|
+
'Use when context is full, the session is confused, or starting a different task.',
|
|
217
|
+
args: {
|
|
218
|
+
cwd: tool.schema.string().optional(),
|
|
219
|
+
reason: tool.schema.string().optional(),
|
|
220
|
+
},
|
|
221
|
+
async execute(args, context) {
|
|
222
|
+
annotateToolRun(context, 'Clearing session', {
|
|
223
|
+
reason: args.reason,
|
|
135
224
|
});
|
|
136
|
-
|
|
225
|
+
const clearedId = await services.manager.clearSession(args.cwd ?? context.worktree);
|
|
226
|
+
return JSON.stringify({ clearedSessionId: clearedId });
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
claude_manager_status: tool({
|
|
230
|
+
description: 'Get the current persistent session status: context usage %, turns, cost, active session ID.',
|
|
231
|
+
args: {
|
|
232
|
+
cwd: tool.schema.string().optional(),
|
|
233
|
+
},
|
|
234
|
+
async execute(_args, context) {
|
|
235
|
+
annotateToolRun(context, 'Checking session status', {});
|
|
236
|
+
const status = services.manager.getStatus();
|
|
237
|
+
return JSON.stringify({
|
|
238
|
+
...status,
|
|
239
|
+
transcriptFile: status.sessionId
|
|
240
|
+
? `.claude-manager/transcripts/${status.sessionId}.json`
|
|
241
|
+
: null,
|
|
242
|
+
contextWarning: formatContextWarning(status),
|
|
243
|
+
}, null, 2);
|
|
137
244
|
},
|
|
138
245
|
}),
|
|
139
246
|
claude_manager_metadata: tool({
|
|
@@ -153,23 +260,31 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
153
260
|
},
|
|
154
261
|
}),
|
|
155
262
|
claude_manager_sessions: tool({
|
|
156
|
-
description: 'List Claude sessions or inspect a saved transcript.'
|
|
263
|
+
description: 'List Claude sessions or inspect a saved transcript. ' +
|
|
264
|
+
'When sessionId is provided, returns both SDK transcript and local manager events.',
|
|
157
265
|
args: {
|
|
158
266
|
cwd: tool.schema.string().optional(),
|
|
159
267
|
sessionId: tool.schema.string().optional(),
|
|
160
268
|
},
|
|
161
269
|
async execute(args, context) {
|
|
162
270
|
annotateToolRun(context, 'Inspecting Claude session history', {});
|
|
271
|
+
const cwd = args.cwd ?? context.worktree;
|
|
163
272
|
if (args.sessionId) {
|
|
164
|
-
const
|
|
165
|
-
|
|
273
|
+
const [sdkTranscript, localEvents] = await Promise.all([
|
|
274
|
+
services.sessions.getTranscript(args.sessionId, cwd),
|
|
275
|
+
services.manager.getTranscriptEvents(cwd, args.sessionId),
|
|
276
|
+
]);
|
|
277
|
+
return JSON.stringify({
|
|
278
|
+
sdkTranscript,
|
|
279
|
+
localEvents: localEvents.length > 0 ? localEvents : undefined,
|
|
280
|
+
}, null, 2);
|
|
166
281
|
}
|
|
167
|
-
const sessions = await services.sessions.listSessions(
|
|
282
|
+
const sessions = await services.sessions.listSessions(cwd);
|
|
168
283
|
return JSON.stringify(sessions, null, 2);
|
|
169
284
|
},
|
|
170
285
|
}),
|
|
171
286
|
claude_manager_runs: tool({
|
|
172
|
-
description: 'List manager
|
|
287
|
+
description: 'List persistent manager run records.',
|
|
173
288
|
args: {
|
|
174
289
|
cwd: tool.schema.string().optional(),
|
|
175
290
|
runId: tool.schema.string().optional(),
|
|
@@ -184,20 +299,6 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
184
299
|
return JSON.stringify(runs, null, 2);
|
|
185
300
|
},
|
|
186
301
|
}),
|
|
187
|
-
claude_manager_cleanup_run: tool({
|
|
188
|
-
description: 'Explicitly remove git worktrees created for a recorded manager run.',
|
|
189
|
-
args: {
|
|
190
|
-
runId: tool.schema.string().min(1),
|
|
191
|
-
cwd: tool.schema.string().optional(),
|
|
192
|
-
},
|
|
193
|
-
async execute(args, context) {
|
|
194
|
-
annotateToolRun(context, 'Cleaning manager worktrees', {
|
|
195
|
-
runId: args.runId,
|
|
196
|
-
});
|
|
197
|
-
const run = await services.manager.cleanupRunWorktrees(args.cwd ?? context.worktree, args.runId);
|
|
198
|
-
return JSON.stringify(run, null, 2);
|
|
199
|
-
},
|
|
200
|
-
}),
|
|
201
302
|
},
|
|
202
303
|
};
|
|
203
304
|
};
|
|
@@ -208,11 +309,11 @@ function buildCommandText(command, rawArguments) {
|
|
|
208
309
|
const argumentsText = rawArguments.trim();
|
|
209
310
|
if (command === 'claude-run') {
|
|
210
311
|
return [
|
|
211
|
-
'
|
|
312
|
+
'Use `claude_manager_send` to send the following task to Claude Code:',
|
|
212
313
|
argumentsText
|
|
213
|
-
?
|
|
214
|
-
: '
|
|
215
|
-
'
|
|
314
|
+
? argumentsText
|
|
315
|
+
: 'Inspect the current repository and wait for follow-up instructions.',
|
|
316
|
+
'After it completes, review the result. If code changes were expected, use `claude_manager_git_diff` to review, then `claude_manager_git_commit` or `claude_manager_git_reset` accordingly.',
|
|
216
317
|
].join('\n\n');
|
|
217
318
|
}
|
|
218
319
|
if (command === 'claude-metadata') {
|
|
@@ -237,125 +338,27 @@ function buildCommandText(command, rawArguments) {
|
|
|
237
338
|
}
|
|
238
339
|
function rewriteCommandParts(parts, text) {
|
|
239
340
|
let hasRewrittenText = false;
|
|
240
|
-
|
|
341
|
+
return parts.map((part) => {
|
|
241
342
|
if (part.type !== 'text' || hasRewrittenText) {
|
|
242
343
|
return part;
|
|
243
344
|
}
|
|
244
345
|
hasRewrittenText = true;
|
|
245
|
-
return {
|
|
246
|
-
...part,
|
|
247
|
-
text,
|
|
248
|
-
};
|
|
346
|
+
return { ...part, text };
|
|
249
347
|
});
|
|
250
|
-
return rewrittenParts;
|
|
251
348
|
}
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
: finalSummary;
|
|
257
|
-
return JSON.stringify({
|
|
258
|
-
runId: run.id,
|
|
259
|
-
status: run.status,
|
|
260
|
-
output,
|
|
261
|
-
finalSummary,
|
|
262
|
-
sessions: run.sessions.map((session) => ({
|
|
263
|
-
title: session.title,
|
|
264
|
-
status: session.status,
|
|
265
|
-
output: resolveSessionOutput(session),
|
|
266
|
-
claudeSessionId: session.claudeSessionId,
|
|
267
|
-
worktreeMode: session.worktreeMode,
|
|
268
|
-
branchName: session.branchName,
|
|
269
|
-
turns: session.turns,
|
|
270
|
-
totalCostUsd: session.totalCostUsd,
|
|
271
|
-
})),
|
|
272
|
-
inspectRun: {
|
|
273
|
-
tool: 'claude_manager_runs',
|
|
274
|
-
runId: run.id,
|
|
275
|
-
},
|
|
276
|
-
}, null, 2);
|
|
277
|
-
}
|
|
278
|
-
function buildRunProgressView(run) {
|
|
279
|
-
const completed = run.sessions.filter((session) => session.status === 'completed').length;
|
|
280
|
-
const failed = run.sessions.filter((session) => session.status === 'failed').length;
|
|
281
|
-
const running = run.sessions.filter((session) => session.status === 'running').length;
|
|
282
|
-
const pending = run.sessions.filter((session) => session.status === 'pending').length;
|
|
283
|
-
const total = run.sessions.length;
|
|
284
|
-
return {
|
|
285
|
-
title: buildRunProgressTitle(run, { completed, failed, running, total }),
|
|
286
|
-
metadata: {
|
|
287
|
-
runId: run.id,
|
|
288
|
-
status: run.status,
|
|
289
|
-
progress: `${completed}/${total} completed`,
|
|
290
|
-
active: running,
|
|
291
|
-
pending,
|
|
292
|
-
failed,
|
|
293
|
-
sessions: run.sessions.map(formatSessionActivity),
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
function buildRunProgressTitle(run, counts) {
|
|
298
|
-
const suffix = `(${counts.completed}/${counts.total} complete` +
|
|
299
|
-
(counts.running > 0 ? `, ${counts.running} active` : '') +
|
|
300
|
-
(counts.failed > 0 ? `, ${counts.failed} failed` : '') +
|
|
301
|
-
')';
|
|
302
|
-
if (run.status === 'completed') {
|
|
303
|
-
return `Claude manager completed ${suffix}`;
|
|
304
|
-
}
|
|
305
|
-
if (run.status === 'failed') {
|
|
306
|
-
return `Claude manager failed ${suffix}`;
|
|
307
|
-
}
|
|
308
|
-
if (counts.running > 0) {
|
|
309
|
-
return `Claude manager running ${suffix}`;
|
|
310
|
-
}
|
|
311
|
-
return `Claude manager queued ${suffix}`;
|
|
312
|
-
}
|
|
313
|
-
function formatSessionActivity(session) {
|
|
314
|
-
const parts = [session.title, session.status];
|
|
315
|
-
if (session.claudeSessionId) {
|
|
316
|
-
parts.push(session.claudeSessionId);
|
|
349
|
+
function formatContextWarning(context) {
|
|
350
|
+
const { warningLevel, estimatedContextPercent, totalTurns, totalCostUsd } = context;
|
|
351
|
+
if (warningLevel === 'ok' || estimatedContextPercent === null) {
|
|
352
|
+
return null;
|
|
317
353
|
}
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return parts.join(' | ');
|
|
329
|
-
}
|
|
330
|
-
function findLatestDisplayEvent(events) {
|
|
331
|
-
const reversedEvents = [...events].reverse();
|
|
332
|
-
const preferredEvent = reversedEvents.find((event) => (event.type === 'result' ||
|
|
333
|
-
event.type === 'error' ||
|
|
334
|
-
event.type === 'assistant') &&
|
|
335
|
-
Boolean(event.text.trim()));
|
|
336
|
-
if (preferredEvent) {
|
|
337
|
-
return preferredEvent;
|
|
338
|
-
}
|
|
339
|
-
return [...events]
|
|
340
|
-
.reverse()
|
|
341
|
-
.find((event) => event.type !== 'partial' && Boolean(event.text.trim()));
|
|
342
|
-
}
|
|
343
|
-
function truncateForDisplay(text, maxLength) {
|
|
344
|
-
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
345
|
-
if (normalized.length <= maxLength) {
|
|
346
|
-
return normalized;
|
|
347
|
-
}
|
|
348
|
-
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
349
|
-
}
|
|
350
|
-
function summarizeSessionOutputs(sessions) {
|
|
351
|
-
return sessions
|
|
352
|
-
.map((session) => `${session.title}: ${resolveSessionOutput(session)}`)
|
|
353
|
-
.join('\n');
|
|
354
|
-
}
|
|
355
|
-
function resolveSessionOutput(session) {
|
|
356
|
-
const latestEvent = findLatestDisplayEvent(session.events);
|
|
357
|
-
return (session.finalText?.trim() ||
|
|
358
|
-
session.error?.trim() ||
|
|
359
|
-
latestEvent?.text.trim() ||
|
|
360
|
-
session.status);
|
|
354
|
+
const templates = managerPromptRegistry.contextWarnings;
|
|
355
|
+
const template = warningLevel === 'critical'
|
|
356
|
+
? templates.critical
|
|
357
|
+
: warningLevel === 'high'
|
|
358
|
+
? templates.high
|
|
359
|
+
: templates.moderate;
|
|
360
|
+
return template
|
|
361
|
+
.replace('{percent}', String(estimatedContextPercent))
|
|
362
|
+
.replace('{turns}', String(totalTurns))
|
|
363
|
+
.replace('{cost}', totalCostUsd.toFixed(2));
|
|
361
364
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
manager:
|
|
2
|
+
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
3
|
+
interface ClaudeManagerPluginServices {
|
|
4
|
+
manager: PersistentManager;
|
|
5
5
|
sessions: ClaudeSessionService;
|
|
6
6
|
}
|
|
7
7
|
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
8
|
+
export {};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
2
2
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
3
|
-
import { ManagerOrchestrator } from '../manager/manager-orchestrator.js';
|
|
4
|
-
import { TaskPlanner } from '../manager/task-planner.js';
|
|
5
3
|
import { ClaudeMetadataService } from '../metadata/claude-metadata.service.js';
|
|
6
4
|
import { RepoClaudeConfigReader } from '../metadata/repo-claude-config-reader.js';
|
|
7
5
|
import { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
8
|
-
import {
|
|
6
|
+
import { TranscriptStore } from '../state/transcript-store.js';
|
|
7
|
+
import { ContextTracker } from '../manager/context-tracker.js';
|
|
8
|
+
import { GitOperations } from '../manager/git-operations.js';
|
|
9
|
+
import { SessionController } from '../manager/session-controller.js';
|
|
10
|
+
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
11
|
+
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
9
12
|
const serviceCache = new Map();
|
|
10
13
|
export function getOrCreatePluginServices(worktree) {
|
|
11
14
|
const cachedServices = serviceCache.get(worktree);
|
|
@@ -15,7 +18,14 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
15
18
|
const sdkAdapter = new ClaudeAgentSdkAdapter();
|
|
16
19
|
const metadataService = new ClaudeMetadataService(new RepoClaudeConfigReader(), sdkAdapter);
|
|
17
20
|
const sessionService = new ClaudeSessionService(sdkAdapter, metadataService);
|
|
18
|
-
const
|
|
21
|
+
const contextTracker = new ContextTracker();
|
|
22
|
+
const sessionController = new SessionController(sdkAdapter, contextTracker, managerPromptRegistry.claudeCodeSessionPrompt);
|
|
23
|
+
const gitOps = new GitOperations(worktree);
|
|
24
|
+
const stateStore = new FileRunStateStore();
|
|
25
|
+
const transcriptStore = new TranscriptStore();
|
|
26
|
+
const manager = new PersistentManager(sessionController, gitOps, stateStore, contextTracker, transcriptStore);
|
|
27
|
+
// Try to restore active session state (fire and forget)
|
|
28
|
+
manager.tryRestore(worktree).catch(() => { });
|
|
19
29
|
const services = {
|
|
20
30
|
manager,
|
|
21
31
|
sessions: sessionService,
|
package/dist/prompts/registry.js
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
export const managerPromptRegistry = {
|
|
2
2
|
managerSystemPrompt: [
|
|
3
|
-
'You
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
|
|
3
|
+
'You orchestrate Claude Code through a persistent session. You are a user proxy —',
|
|
4
|
+
'your job is to make Claude Code do the work, not to do it yourself.',
|
|
5
|
+
'',
|
|
6
|
+
'## Workflow',
|
|
7
|
+
'1. claude_manager_send — send clear, specific instructions',
|
|
8
|
+
'2. claude_manager_git_diff — review what changed',
|
|
9
|
+
'3. claude_manager_git_commit — checkpoint good work',
|
|
10
|
+
'4. claude_manager_git_reset — discard bad work',
|
|
11
|
+
'5. claude_manager_clear — fresh session when needed',
|
|
12
|
+
'',
|
|
13
|
+
'## Context management',
|
|
14
|
+
'Check the context snapshot in each send result:',
|
|
15
|
+
'- Under 50%: proceed freely',
|
|
16
|
+
'- 50-70%: consider if next task should reuse or start fresh',
|
|
17
|
+
'- Over 70%: compact or clear before heavy work',
|
|
18
|
+
'- Over 85% or 200k tokens: clear immediately',
|
|
19
|
+
'',
|
|
20
|
+
'## Delegation principles',
|
|
21
|
+
'- Write specific task descriptions with file paths, function names, error messages',
|
|
22
|
+
'- For large features, send sequential focused instructions',
|
|
23
|
+
'- Tell Claude Code to use subagents for parallel/independent parts',
|
|
24
|
+
'- After implementation, always review with git diff before committing',
|
|
25
|
+
'- If work is wrong, send a correction (specific, not "try again") or reset',
|
|
26
|
+
].join('\n'),
|
|
27
|
+
claudeCodeSessionPrompt: [
|
|
28
|
+
'You are being directed by an expert automated operator. Treat each message',
|
|
29
|
+
'as a precise instruction from a skilled Claude Code user.',
|
|
30
|
+
'',
|
|
31
|
+
'Key behaviors:',
|
|
32
|
+
'- Execute instructions directly without asking for clarification',
|
|
33
|
+
'- Use the Agent tool to spawn subagents for parallel/independent work',
|
|
34
|
+
'- Be concise — no preamble, no restating the task',
|
|
35
|
+
'- Do NOT run git commit, git push, or git reset — the operator handles git',
|
|
36
|
+
'- After completing work, end with a brief verification summary',
|
|
37
|
+
'- When context is heavy, prefer targeted file reads over reading entire files',
|
|
38
|
+
'- Report blockers immediately and specifically',
|
|
39
|
+
].join('\n'),
|
|
40
|
+
contextWarnings: {
|
|
41
|
+
moderate: 'Session context is filling up ({percent}% estimated). Consider whether a fresh session would be more efficient.',
|
|
42
|
+
high: 'Session context is heavy ({percent}% estimated, {turns} turns, ${cost}). Start a new session or compact first.',
|
|
43
|
+
critical: 'Session context is near capacity ({percent}% estimated). Clear the session immediately before continuing.',
|
|
44
|
+
},
|
|
11
45
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PersistentRunRecord } from '../types/contracts.js';
|
|
2
2
|
export declare class FileRunStateStore {
|
|
3
3
|
private readonly baseDirectoryName;
|
|
4
4
|
private readonly writeQueues;
|
|
5
5
|
constructor(baseDirectoryName?: string);
|
|
6
|
-
saveRun(run:
|
|
7
|
-
getRun(cwd: string, runId: string): Promise<
|
|
8
|
-
listRuns(cwd: string): Promise<
|
|
9
|
-
updateRun(cwd: string, runId: string, update: (run:
|
|
6
|
+
saveRun(run: PersistentRunRecord): Promise<void>;
|
|
7
|
+
getRun(cwd: string, runId: string): Promise<PersistentRunRecord | null>;
|
|
8
|
+
listRuns(cwd: string): Promise<PersistentRunRecord[]>;
|
|
9
|
+
updateRun(cwd: string, runId: string, update: (run: PersistentRunRecord) => PersistentRunRecord): Promise<PersistentRunRecord>;
|
|
10
10
|
private getRunKey;
|
|
11
11
|
private getRunsDirectory;
|
|
12
12
|
private getRunPath;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { promises as fs } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { isFileNotFoundError, writeJsonAtomically, } from '../util/fs-helpers.js';
|
|
4
4
|
export class FileRunStateStore {
|
|
5
5
|
baseDirectoryName;
|
|
6
6
|
writeQueues = new Map();
|
|
@@ -85,13 +85,3 @@ export class FileRunStateStore {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
async function writeJsonAtomically(filePath, data) {
|
|
89
|
-
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
90
|
-
await fs.writeFile(tempPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
91
|
-
await fs.rename(tempPath, filePath);
|
|
92
|
-
}
|
|
93
|
-
function isFileNotFoundError(error) {
|
|
94
|
-
return (error instanceof Error &&
|
|
95
|
-
'code' in error &&
|
|
96
|
-
error.code === 'ENOENT');
|
|
97
|
-
}
|