@doingdev/opencode-claude-manager-plugin 0.1.4 → 0.1.6
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 +1 -1
- package/dist/claude/claude-agent-sdk-adapter.js +10 -6
- package/dist/manager/manager-orchestrator.d.ts +3 -1
- package/dist/manager/manager-orchestrator.js +19 -11
- package/dist/plugin/claude-manager.plugin.d.ts +2 -0
- package/dist/plugin/claude-manager.plugin.js +174 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ If you are testing locally, point OpenCode at the local package or plugin file u
|
|
|
50
50
|
|
|
51
51
|
## OpenCode tools
|
|
52
52
|
|
|
53
|
-
- `claude_manager_run` - run a task through Claude with optional splitting and worktrees
|
|
53
|
+
- `claude_manager_run` - run a task through Claude with optional splitting and worktrees; returns a compact output summary and a `runId` for deeper inspection
|
|
54
54
|
- `claude_manager_metadata` - inspect available Claude commands, skills, hooks, and settings
|
|
55
55
|
- `claude_manager_sessions` - list Claude sessions or inspect a saved transcript
|
|
56
56
|
- `claude_manager_runs` - inspect persisted manager run records
|
|
@@ -136,10 +136,14 @@ function normalizeSdkMessage(message) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
if (message.type === 'stream_event') {
|
|
139
|
+
const partialText = extractPartialEventText(message.event);
|
|
140
|
+
if (!partialText) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
139
143
|
return {
|
|
140
144
|
type: 'partial',
|
|
141
145
|
sessionId,
|
|
142
|
-
text:
|
|
146
|
+
text: partialText,
|
|
143
147
|
rawType: message.type,
|
|
144
148
|
};
|
|
145
149
|
}
|
|
@@ -191,21 +195,21 @@ function normalizeSdkMessage(message) {
|
|
|
191
195
|
}
|
|
192
196
|
function extractPartialEventText(event) {
|
|
193
197
|
if (!event || typeof event !== 'object') {
|
|
194
|
-
return
|
|
198
|
+
return null;
|
|
195
199
|
}
|
|
196
200
|
const eventRecord = event;
|
|
197
|
-
const eventType = typeof eventRecord.type === 'string' ? eventRecord.type : 'stream_event';
|
|
198
201
|
const delta = eventRecord.delta;
|
|
199
202
|
if (delta && typeof delta === 'object') {
|
|
200
203
|
const deltaRecord = delta;
|
|
201
|
-
if (typeof deltaRecord.text === 'string') {
|
|
204
|
+
if (typeof deltaRecord.text === 'string' && deltaRecord.text.length > 0) {
|
|
202
205
|
return deltaRecord.text;
|
|
203
206
|
}
|
|
204
|
-
if (typeof deltaRecord.partial_json === 'string'
|
|
207
|
+
if (typeof deltaRecord.partial_json === 'string' &&
|
|
208
|
+
deltaRecord.partial_json.length > 0) {
|
|
205
209
|
return deltaRecord.partial_json;
|
|
206
210
|
}
|
|
207
211
|
}
|
|
208
|
-
return
|
|
212
|
+
return null;
|
|
209
213
|
}
|
|
210
214
|
function extractText(payload) {
|
|
211
215
|
if (typeof payload === 'string') {
|
|
@@ -3,16 +3,18 @@ import type { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
|
3
3
|
import type { ManagerRunRecord, ManagerRunResult, ManagerTaskRequest } from '../types/contracts.js';
|
|
4
4
|
import type { WorktreeCoordinator } from '../worktree/worktree-coordinator.js';
|
|
5
5
|
import type { TaskPlanner } from './task-planner.js';
|
|
6
|
+
export type ManagerRunProgressHandler = (run: ManagerRunRecord) => void | Promise<void>;
|
|
6
7
|
export declare class ManagerOrchestrator {
|
|
7
8
|
private readonly sessionService;
|
|
8
9
|
private readonly stateStore;
|
|
9
10
|
private readonly worktreeCoordinator;
|
|
10
11
|
private readonly taskPlanner;
|
|
11
12
|
constructor(sessionService: ClaudeSessionService, stateStore: FileRunStateStore, worktreeCoordinator: WorktreeCoordinator, taskPlanner: TaskPlanner);
|
|
12
|
-
run(request: ManagerTaskRequest): Promise<ManagerRunResult>;
|
|
13
|
+
run(request: ManagerTaskRequest, onProgress?: ManagerRunProgressHandler): Promise<ManagerRunResult>;
|
|
13
14
|
listRuns(cwd: string): Promise<ManagerRunRecord[]>;
|
|
14
15
|
getRun(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
|
|
15
16
|
cleanupRunWorktrees(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
|
|
16
17
|
private executePlan;
|
|
17
18
|
private patchSession;
|
|
19
|
+
private updateRunAndNotify;
|
|
18
20
|
}
|
|
@@ -11,7 +11,7 @@ export class ManagerOrchestrator {
|
|
|
11
11
|
this.worktreeCoordinator = worktreeCoordinator;
|
|
12
12
|
this.taskPlanner = taskPlanner;
|
|
13
13
|
}
|
|
14
|
-
async run(request) {
|
|
14
|
+
async run(request, onProgress) {
|
|
15
15
|
const mode = request.mode ?? 'auto';
|
|
16
16
|
const maxSubagents = Math.max(1, request.maxSubagents ?? 3);
|
|
17
17
|
const useWorktrees = request.useWorktrees ?? maxSubagents > 1;
|
|
@@ -44,6 +44,7 @@ export class ManagerOrchestrator {
|
|
|
44
44
|
sessions: plannedAssignments.map(({ plan, assignment }) => createManagedSessionRecord(plan, assignment.cwd, assignment)),
|
|
45
45
|
};
|
|
46
46
|
await this.stateStore.saveRun(runRecord);
|
|
47
|
+
await onProgress?.(runRecord);
|
|
47
48
|
const canRunInParallel = plannedAssignments.every(({ assignment }) => assignment.mode === 'git-worktree');
|
|
48
49
|
const settledResults = canRunInParallel
|
|
49
50
|
? await Promise.allSettled(plannedAssignments.map(({ plan, assignment }) => this.executePlan({
|
|
@@ -52,6 +53,7 @@ export class ManagerOrchestrator {
|
|
|
52
53
|
plan,
|
|
53
54
|
assignment,
|
|
54
55
|
includeProjectSettings,
|
|
56
|
+
onProgress,
|
|
55
57
|
})))
|
|
56
58
|
: await runSequentially(plannedAssignments.map(({ plan, assignment }) => () => this.executePlan({
|
|
57
59
|
request,
|
|
@@ -59,14 +61,15 @@ export class ManagerOrchestrator {
|
|
|
59
61
|
plan,
|
|
60
62
|
assignment,
|
|
61
63
|
includeProjectSettings,
|
|
64
|
+
onProgress,
|
|
62
65
|
})));
|
|
63
66
|
const failedResult = settledResults.find((result) => result.status === 'rejected');
|
|
64
|
-
const finalRun = await this.
|
|
67
|
+
const finalRun = await this.updateRunAndNotify(request.cwd, runId, (currentRun) => ({
|
|
65
68
|
...currentRun,
|
|
66
69
|
status: failedResult ? 'failed' : 'completed',
|
|
67
70
|
updatedAt: new Date().toISOString(),
|
|
68
71
|
finalSummary: summarizeRun(currentRun.sessions),
|
|
69
|
-
}));
|
|
72
|
+
}), onProgress);
|
|
70
73
|
return { run: finalRun };
|
|
71
74
|
}
|
|
72
75
|
listRuns(cwd) {
|
|
@@ -94,11 +97,11 @@ export class ManagerOrchestrator {
|
|
|
94
97
|
return run;
|
|
95
98
|
}
|
|
96
99
|
async executePlan(input) {
|
|
97
|
-
const { request, runId, plan, assignment, includeProjectSettings } = input;
|
|
100
|
+
const { request, runId, plan, assignment, includeProjectSettings, onProgress, } = input;
|
|
98
101
|
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
99
102
|
...session,
|
|
100
103
|
status: 'running',
|
|
101
|
-
}));
|
|
104
|
+
}), onProgress);
|
|
102
105
|
try {
|
|
103
106
|
const sessionResult = await this.sessionService.runTask({
|
|
104
107
|
cwd: assignment.cwd,
|
|
@@ -112,7 +115,7 @@ export class ManagerOrchestrator {
|
|
|
112
115
|
...session,
|
|
113
116
|
claudeSessionId: event.sessionId ?? session.claudeSessionId,
|
|
114
117
|
events: [...session.events, compactEvent(event)],
|
|
115
|
-
}));
|
|
118
|
+
}), onProgress);
|
|
116
119
|
});
|
|
117
120
|
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
118
121
|
...session,
|
|
@@ -121,23 +124,28 @@ export class ManagerOrchestrator {
|
|
|
121
124
|
finalText: sessionResult.finalText,
|
|
122
125
|
turns: sessionResult.turns,
|
|
123
126
|
totalCostUsd: sessionResult.totalCostUsd,
|
|
124
|
-
}));
|
|
127
|
+
}), onProgress);
|
|
125
128
|
}
|
|
126
129
|
catch (error) {
|
|
127
130
|
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
128
131
|
...session,
|
|
129
132
|
status: 'failed',
|
|
130
133
|
error: error instanceof Error ? error.message : String(error),
|
|
131
|
-
}));
|
|
134
|
+
}), onProgress);
|
|
132
135
|
throw error;
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
|
-
async patchSession(cwd, runId, sessionId, update) {
|
|
136
|
-
await this.
|
|
138
|
+
async patchSession(cwd, runId, sessionId, update, onProgress) {
|
|
139
|
+
await this.updateRunAndNotify(cwd, runId, (run) => ({
|
|
137
140
|
...run,
|
|
138
141
|
updatedAt: new Date().toISOString(),
|
|
139
142
|
sessions: run.sessions.map((session) => session.id === sessionId ? update(session) : session),
|
|
140
|
-
}));
|
|
143
|
+
}), onProgress);
|
|
144
|
+
}
|
|
145
|
+
async updateRunAndNotify(cwd, runId, update, onProgress) {
|
|
146
|
+
const updatedRun = await this.stateStore.updateRun(cwd, runId, update);
|
|
147
|
+
await onProgress?.(updatedRun);
|
|
148
|
+
return updatedRun;
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
function createManagedSessionRecord(plan, cwd, assignment) {
|
|
@@ -68,10 +68,11 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
68
68
|
config.command['claude-run'] ??= {
|
|
69
69
|
description: 'Delegate a task to Claude Code through the manager plugin.',
|
|
70
70
|
agent: 'claude-manager',
|
|
71
|
+
subtask: true,
|
|
71
72
|
template: [
|
|
72
|
-
'
|
|
73
|
+
'Call claude_manager_run immediately for the following task:',
|
|
73
74
|
'$ARGUMENTS',
|
|
74
|
-
'
|
|
75
|
+
'Avoid planning narration before the tool call. After it completes, return a concise result summary.',
|
|
75
76
|
].join('\n\n'),
|
|
76
77
|
};
|
|
77
78
|
config.command['claude-sessions'] ??= {
|
|
@@ -85,6 +86,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
85
86
|
].join('\n\n'),
|
|
86
87
|
};
|
|
87
88
|
},
|
|
89
|
+
'command.execute.before': async (input, output) => {
|
|
90
|
+
const commandText = buildCommandText(input.command, input.arguments);
|
|
91
|
+
if (commandText) {
|
|
92
|
+
output.parts = rewriteCommandParts(output.parts, commandText);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
88
95
|
tool: {
|
|
89
96
|
claude_manager_run: tool({
|
|
90
97
|
description: 'Delegate a task to Claude Code with optional subagents and worktrees.',
|
|
@@ -102,6 +109,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
102
109
|
task: args.task,
|
|
103
110
|
mode: args.mode,
|
|
104
111
|
});
|
|
112
|
+
let lastProgressSignature = '';
|
|
105
113
|
const result = await services.manager.run({
|
|
106
114
|
cwd: args.cwd ?? context.worktree,
|
|
107
115
|
task: args.task,
|
|
@@ -110,8 +118,16 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
110
118
|
useWorktrees: args.useWorktrees,
|
|
111
119
|
includeProjectSettings: args.includeProjectSettings,
|
|
112
120
|
model: args.model,
|
|
121
|
+
}, async (run) => {
|
|
122
|
+
const progressView = buildRunProgressView(run);
|
|
123
|
+
const signature = JSON.stringify(progressView);
|
|
124
|
+
if (signature === lastProgressSignature) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
lastProgressSignature = signature;
|
|
128
|
+
context.metadata(progressView);
|
|
113
129
|
});
|
|
114
|
-
return
|
|
130
|
+
return formatManagerRunToolResult(result.run);
|
|
115
131
|
},
|
|
116
132
|
}),
|
|
117
133
|
claude_manager_metadata: tool({
|
|
@@ -182,3 +198,158 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
182
198
|
function annotateToolRun(context, title, metadata) {
|
|
183
199
|
context.metadata({ title, metadata });
|
|
184
200
|
}
|
|
201
|
+
function buildCommandText(command, rawArguments) {
|
|
202
|
+
const argumentsText = rawArguments.trim();
|
|
203
|
+
if (command === 'claude-run') {
|
|
204
|
+
return [
|
|
205
|
+
'Call `claude_manager_run` immediately.',
|
|
206
|
+
argumentsText
|
|
207
|
+
? `Task: ${argumentsText}`
|
|
208
|
+
: 'Task: Inspect the current repository and wait for follow-up instructions.',
|
|
209
|
+
'Do not add planning text before the tool call. After it completes, give a short result summary.',
|
|
210
|
+
].join('\n\n');
|
|
211
|
+
}
|
|
212
|
+
if (command === 'claude-metadata') {
|
|
213
|
+
return [
|
|
214
|
+
'Call `claude_manager_metadata` immediately for the current repository.',
|
|
215
|
+
argumentsText ? `Focus: ${argumentsText}` : '',
|
|
216
|
+
'Then summarize the discovered Claude commands, skills, hooks, agents, and config files briefly.',
|
|
217
|
+
]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join('\n\n');
|
|
220
|
+
}
|
|
221
|
+
if (command === 'claude-sessions') {
|
|
222
|
+
return [
|
|
223
|
+
'Call `claude_manager_sessions` and `claude_manager_runs` immediately for the current repository.',
|
|
224
|
+
argumentsText ? `Focus: ${argumentsText}` : '',
|
|
225
|
+
'Then summarize the most relevant recent Claude activity briefly.',
|
|
226
|
+
]
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.join('\n\n');
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function rewriteCommandParts(parts, text) {
|
|
233
|
+
let hasRewrittenText = false;
|
|
234
|
+
const rewrittenParts = parts.map((part) => {
|
|
235
|
+
if (part.type !== 'text' || hasRewrittenText) {
|
|
236
|
+
return part;
|
|
237
|
+
}
|
|
238
|
+
hasRewrittenText = true;
|
|
239
|
+
return {
|
|
240
|
+
...part,
|
|
241
|
+
text,
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
return rewrittenParts;
|
|
245
|
+
}
|
|
246
|
+
export function formatManagerRunToolResult(run) {
|
|
247
|
+
const finalSummary = run.finalSummary ?? summarizeSessionOutputs(run.sessions);
|
|
248
|
+
const output = run.sessions.length === 1
|
|
249
|
+
? resolveSessionOutput(run.sessions[0])
|
|
250
|
+
: finalSummary;
|
|
251
|
+
return JSON.stringify({
|
|
252
|
+
runId: run.id,
|
|
253
|
+
status: run.status,
|
|
254
|
+
output,
|
|
255
|
+
finalSummary,
|
|
256
|
+
sessions: run.sessions.map((session) => ({
|
|
257
|
+
title: session.title,
|
|
258
|
+
status: session.status,
|
|
259
|
+
output: resolveSessionOutput(session),
|
|
260
|
+
claudeSessionId: session.claudeSessionId,
|
|
261
|
+
worktreeMode: session.worktreeMode,
|
|
262
|
+
branchName: session.branchName,
|
|
263
|
+
turns: session.turns,
|
|
264
|
+
totalCostUsd: session.totalCostUsd,
|
|
265
|
+
})),
|
|
266
|
+
inspectRun: {
|
|
267
|
+
tool: 'claude_manager_runs',
|
|
268
|
+
runId: run.id,
|
|
269
|
+
},
|
|
270
|
+
}, null, 2);
|
|
271
|
+
}
|
|
272
|
+
function buildRunProgressView(run) {
|
|
273
|
+
const completed = run.sessions.filter((session) => session.status === 'completed').length;
|
|
274
|
+
const failed = run.sessions.filter((session) => session.status === 'failed').length;
|
|
275
|
+
const running = run.sessions.filter((session) => session.status === 'running').length;
|
|
276
|
+
const pending = run.sessions.filter((session) => session.status === 'pending').length;
|
|
277
|
+
const total = run.sessions.length;
|
|
278
|
+
return {
|
|
279
|
+
title: buildRunProgressTitle(run, { completed, failed, running, total }),
|
|
280
|
+
metadata: {
|
|
281
|
+
runId: run.id,
|
|
282
|
+
status: run.status,
|
|
283
|
+
progress: `${completed}/${total} completed`,
|
|
284
|
+
active: running,
|
|
285
|
+
pending,
|
|
286
|
+
failed,
|
|
287
|
+
sessions: run.sessions.map(formatSessionActivity),
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function buildRunProgressTitle(run, counts) {
|
|
292
|
+
const suffix = `(${counts.completed}/${counts.total} complete` +
|
|
293
|
+
(counts.running > 0 ? `, ${counts.running} active` : '') +
|
|
294
|
+
(counts.failed > 0 ? `, ${counts.failed} failed` : '') +
|
|
295
|
+
')';
|
|
296
|
+
if (run.status === 'completed') {
|
|
297
|
+
return `Claude manager completed ${suffix}`;
|
|
298
|
+
}
|
|
299
|
+
if (run.status === 'failed') {
|
|
300
|
+
return `Claude manager failed ${suffix}`;
|
|
301
|
+
}
|
|
302
|
+
if (counts.running > 0) {
|
|
303
|
+
return `Claude manager running ${suffix}`;
|
|
304
|
+
}
|
|
305
|
+
return `Claude manager queued ${suffix}`;
|
|
306
|
+
}
|
|
307
|
+
function formatSessionActivity(session) {
|
|
308
|
+
const parts = [session.title, session.status];
|
|
309
|
+
if (session.claudeSessionId) {
|
|
310
|
+
parts.push(session.claudeSessionId);
|
|
311
|
+
}
|
|
312
|
+
const latestEvent = findLatestDisplayEvent(session.events);
|
|
313
|
+
if (session.finalText) {
|
|
314
|
+
parts.push(truncateForDisplay(session.finalText, 120));
|
|
315
|
+
}
|
|
316
|
+
else if (session.error) {
|
|
317
|
+
parts.push(truncateForDisplay(session.error, 120));
|
|
318
|
+
}
|
|
319
|
+
else if (latestEvent) {
|
|
320
|
+
parts.push(`${latestEvent.type}: ${truncateForDisplay(latestEvent.text, 120)}`);
|
|
321
|
+
}
|
|
322
|
+
return parts.join(' | ');
|
|
323
|
+
}
|
|
324
|
+
function findLatestDisplayEvent(events) {
|
|
325
|
+
const reversedEvents = [...events].reverse();
|
|
326
|
+
const preferredEvent = reversedEvents.find((event) => (event.type === 'result' ||
|
|
327
|
+
event.type === 'error' ||
|
|
328
|
+
event.type === 'assistant') &&
|
|
329
|
+
Boolean(event.text.trim()));
|
|
330
|
+
if (preferredEvent) {
|
|
331
|
+
return preferredEvent;
|
|
332
|
+
}
|
|
333
|
+
return [...events]
|
|
334
|
+
.reverse()
|
|
335
|
+
.find((event) => event.type !== 'partial' && Boolean(event.text.trim()));
|
|
336
|
+
}
|
|
337
|
+
function truncateForDisplay(text, maxLength) {
|
|
338
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
339
|
+
if (normalized.length <= maxLength) {
|
|
340
|
+
return normalized;
|
|
341
|
+
}
|
|
342
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
343
|
+
}
|
|
344
|
+
function summarizeSessionOutputs(sessions) {
|
|
345
|
+
return sessions
|
|
346
|
+
.map((session) => `${session.title}: ${resolveSessionOutput(session)}`)
|
|
347
|
+
.join('\n');
|
|
348
|
+
}
|
|
349
|
+
function resolveSessionOutput(session) {
|
|
350
|
+
const latestEvent = findLatestDisplayEvent(session.events);
|
|
351
|
+
return (session.finalText?.trim() ||
|
|
352
|
+
session.error?.trim() ||
|
|
353
|
+
latestEvent?.text.trim() ||
|
|
354
|
+
session.status);
|
|
355
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doingdev/opencode-claude-manager-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "OpenCode plugin that orchestrates Claude Code sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
31
|
"lint": "eslint .",
|
|
32
32
|
"format": "prettier --write .",
|
|
33
|
-
"test": "vitest run"
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"release": "npm run build && npm version patch && npm publish"
|
|
34
35
|
},
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=22.0.0"
|