@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.
@@ -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
- 'claude_manager_run',
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
- claude_manager_run: 'allow',
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 manages Claude Code sessions through the bundled plugin tools.',
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
- 'Prefer claude_manager_metadata, claude_manager_sessions, and claude_manager_runs over guesswork.',
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
- claude_manager_run: tool({
103
- description: 'Delegate a task to Claude Code with optional subagents and worktrees.',
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
- task: tool.schema.string().min(1),
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
- annotateToolRun(context, 'Delegating task to Claude manager', {
115
- task: args.task,
116
- mode: args.mode,
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 lastProgressSignature = '';
119
- const result = await services.manager.run({
120
- cwd: args.cwd ?? context.worktree,
121
- task: args.task,
122
- mode: args.mode,
123
- maxSubagents: args.maxSubagents,
124
- useWorktrees: args.useWorktrees,
125
- includeProjectSettings: args.includeProjectSettings,
126
- model: args.model,
127
- }, async (run) => {
128
- const progressView = buildRunProgressView(run);
129
- const signature = JSON.stringify(progressView);
130
- if (signature === lastProgressSignature) {
131
- return;
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
- lastProgressSignature = signature;
134
- context.metadata(progressView);
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
- return formatManagerRunToolResult(result.run);
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 transcript = await services.sessions.getTranscript(args.sessionId, args.cwd ?? context.worktree);
165
- return JSON.stringify(transcript, null, 2);
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(args.cwd ?? context.worktree);
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 runs previously recorded for this repo.',
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
- 'Call `claude_manager_run` immediately.',
312
+ 'Use `claude_manager_send` to send the following task to Claude Code:',
212
313
  argumentsText
213
- ? `Task: ${argumentsText}`
214
- : 'Task: Inspect the current repository and wait for follow-up instructions.',
215
- 'Do not add planning text before the tool call. After it completes, give a short result summary.',
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
- const rewrittenParts = parts.map((part) => {
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
- export function formatManagerRunToolResult(run) {
253
- const finalSummary = run.finalSummary ?? summarizeSessionOutputs(run.sessions);
254
- const output = run.sessions.length === 1
255
- ? resolveSessionOutput(run.sessions[0])
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 latestEvent = findLatestDisplayEvent(session.events);
319
- if (session.finalText) {
320
- parts.push(truncateForDisplay(session.finalText, 120));
321
- }
322
- else if (session.error) {
323
- parts.push(truncateForDisplay(session.error, 120));
324
- }
325
- else if (latestEvent) {
326
- parts.push(`${latestEvent.type}: ${truncateForDisplay(latestEvent.text, 120)}`);
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 { ManagerOrchestrator } from '../manager/manager-orchestrator.js';
3
- export interface ClaudeManagerPluginServices {
4
- manager: ManagerOrchestrator;
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 { WorktreeCoordinator } from '../worktree/worktree-coordinator.js';
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 manager = new ManagerOrchestrator(sessionService, new FileRunStateStore(), new WorktreeCoordinator(), new TaskPlanner());
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,
@@ -1,11 +1,45 @@
1
1
  export const managerPromptRegistry = {
2
2
  managerSystemPrompt: [
3
- 'You are the OpenCode manager for Claude Code sessions.',
4
- 'Plan first, delegate carefully, and keep user-visible progress concise.',
5
- 'Prefer deterministic orchestration in code over prompt-only control flow.',
6
- ].join(' '),
7
- subagentSystemPrompt: [
8
- 'You are a Claude execution worker managed by OpenCode.',
9
- 'Stay within assigned scope, report blockers explicitly, and summarize changes with verification notes.',
10
- ].join(' '),
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 { ManagerRunRecord } from '../types/contracts.js';
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: ManagerRunRecord): Promise<void>;
7
- getRun(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
8
- listRuns(cwd: string): Promise<ManagerRunRecord[]>;
9
- updateRun(cwd: string, runId: string, update: (run: ManagerRunRecord) => ManagerRunRecord): Promise<ManagerRunRecord>;
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
- }