@doingdev/opencode-claude-manager-plugin 0.1.4 → 0.1.5

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.
@@ -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.stateStore.updateRun(request.cwd, runId, (currentRun) => ({
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.stateStore.updateRun(cwd, runId, (run) => ({
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
- 'Use claude_manager_run to delegate the following task to Claude Code:',
73
+ 'Call claude_manager_run immediately for the following task:',
73
74
  '$ARGUMENTS',
74
- 'Default to worktrees for multi-part work and return a concise result summary.',
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,6 +118,14 @@ 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
130
  return JSON.stringify(result.run, null, 2);
115
131
  },
@@ -182,3 +198,112 @@ 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
+ function buildRunProgressView(run) {
247
+ const completed = run.sessions.filter((session) => session.status === 'completed').length;
248
+ const failed = run.sessions.filter((session) => session.status === 'failed').length;
249
+ const running = run.sessions.filter((session) => session.status === 'running').length;
250
+ const pending = run.sessions.filter((session) => session.status === 'pending').length;
251
+ const total = run.sessions.length;
252
+ return {
253
+ title: buildRunProgressTitle(run, { completed, failed, running, total }),
254
+ metadata: {
255
+ runId: run.id,
256
+ status: run.status,
257
+ progress: `${completed}/${total} completed`,
258
+ active: running,
259
+ pending,
260
+ failed,
261
+ sessions: run.sessions.map(formatSessionActivity),
262
+ },
263
+ };
264
+ }
265
+ function buildRunProgressTitle(run, counts) {
266
+ const suffix = `(${counts.completed}/${counts.total} complete` +
267
+ (counts.running > 0 ? `, ${counts.running} active` : '') +
268
+ (counts.failed > 0 ? `, ${counts.failed} failed` : '') +
269
+ ')';
270
+ if (run.status === 'completed') {
271
+ return `Claude manager completed ${suffix}`;
272
+ }
273
+ if (run.status === 'failed') {
274
+ return `Claude manager failed ${suffix}`;
275
+ }
276
+ if (counts.running > 0) {
277
+ return `Claude manager running ${suffix}`;
278
+ }
279
+ return `Claude manager queued ${suffix}`;
280
+ }
281
+ function formatSessionActivity(session) {
282
+ const parts = [session.title, session.status];
283
+ if (session.claudeSessionId) {
284
+ parts.push(session.claudeSessionId);
285
+ }
286
+ const latestEvent = findLatestDisplayEvent(session.events);
287
+ if (latestEvent) {
288
+ parts.push(`${latestEvent.type}: ${truncateForDisplay(latestEvent.text, 120)}`);
289
+ }
290
+ else if (session.finalText) {
291
+ parts.push(truncateForDisplay(session.finalText, 120));
292
+ }
293
+ else if (session.error) {
294
+ parts.push(truncateForDisplay(session.error, 120));
295
+ }
296
+ return parts.join(' | ');
297
+ }
298
+ function findLatestDisplayEvent(events) {
299
+ return [...events]
300
+ .reverse()
301
+ .find((event) => event.type !== 'partial' && Boolean(event.text.trim()));
302
+ }
303
+ function truncateForDisplay(text, maxLength) {
304
+ const normalized = text.replace(/\s+/g, ' ').trim();
305
+ if (normalized.length <= maxLength) {
306
+ return normalized;
307
+ }
308
+ return `${normalized.slice(0, maxLength - 3)}...`;
309
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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"