@doingdev/opencode-claude-manager-plugin 0.1.19 → 0.1.20

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 CHANGED
@@ -4,15 +4,17 @@ This package provides an OpenCode plugin that lets an OpenCode-side manager agen
4
4
 
5
5
  ## Overview
6
6
 
7
- Use this when you want OpenCode to act as a manager over Claude Code instead of talking to Claude directly. The plugin gives OpenCode a stable tool surface for discovering Claude metadata, delegating work to Claude sessions, and splitting tasks into subagents (all using the same working directory).
7
+ Use this when you want OpenCode to act as a manager over Claude Code instead of talking to Claude directly. The plugin gives OpenCode a stable tool surface for delegating work to Claude Code sessions, managing session lifecycle (compact, clear, fresh start), reviewing changes via git, and inspecting session history.
8
8
 
9
9
  ## Features
10
10
 
11
11
  - Runs Claude Code tasks from OpenCode through `@anthropic-ai/claude-agent-sdk`.
12
+ - Persistent sessions with `freshSession`, model, and effort controls for safe task isolation.
13
+ - Context lifecycle: compact (preserve state) or clear (start fresh).
12
14
  - Discovers repo-local Claude metadata from `.claude/skills`, `.claude/commands`, `CLAUDE.md`, and settings hooks.
13
- - Splits multi-step tasks into subagents that run sequentially in the same repository directory.
14
- - Persists manager runs under `.claude-manager/runs` so sessions can be inspected later.
15
- - Exposes manager-facing tools instead of relying on undocumented plugin-defined slash commands.
15
+ - Git integration: diff, commit, and reset from the manager layer.
16
+ - Tool approval policy for governing which Claude Code tools are allowed.
17
+ - Optionally persists manager run records under `.claude-manager/runs` for post-hoc inspection (populated when tasks are executed through the run-tracking path).
16
18
 
17
19
  ## Requirements
18
20
 
@@ -49,10 +51,35 @@ If you are testing locally, point OpenCode at the local package or plugin file u
49
51
 
50
52
  ## OpenCode tools
51
53
 
52
- - `claude_manager_run` - run a task through Claude with optional splitting into subagents; returns a compact output summary and a `runId` for deeper inspection
53
- - `claude_manager_metadata` - inspect available Claude commands, skills, hooks, and settings
54
- - `claude_manager_sessions` - list Claude sessions or inspect a saved transcript
55
- - `claude_manager_runs` - inspect persisted manager run records
54
+ ### Session management
55
+
56
+ - `claude_manager_send` send a message to the persistent Claude Code session. Auto-creates on first call, resumes on subsequent calls.
57
+ - `message` (required) the instruction to send.
58
+ - `mode` — `"plan"` (read-only investigation) or `"free"` (default, normal execution with edits).
59
+ - `freshSession` — set to `true` to clear the active session before sending. Use when switching to an unrelated task or when context is contaminated.
60
+ - `model` — `"claude-opus-4-6"` (default, recommended for most coding work), `"claude-sonnet-4-6"`, or `"claude-sonnet-4-5"` (faster/lighter tasks).
61
+ - `effort` — `"high"` (default), `"medium"` (lighter tasks), `"low"`, or `"max"` (especially hard problems).
62
+ - `claude_manager_compact` — compress the active session context while preserving session state. Use before clearing when context is high but salvageable.
63
+ - `claude_manager_clear` — drop the active session entirely; next send starts fresh.
64
+ - `claude_manager_status` — get current session health: context %, turns, cost, session ID.
65
+
66
+ ### Git operations
67
+
68
+ - `claude_manager_git_diff` — review all uncommitted changes (staged + unstaged).
69
+ - `claude_manager_git_commit` — stage all changes and commit with a message.
70
+ - `claude_manager_git_reset` — hard reset + clean (destructive).
71
+
72
+ ### Inspection
73
+
74
+ - `claude_manager_metadata` — inspect available Claude commands, skills, hooks, and settings.
75
+ - `claude_manager_sessions` — list Claude sessions or inspect a saved transcript.
76
+ - `claude_manager_runs` — list or inspect persisted manager run records (may be empty if tasks were sent directly via `claude_manager_send` rather than the run-tracking path).
77
+
78
+ ### Tool approval
79
+
80
+ - `claude_manager_approval_policy` — view the current tool approval policy.
81
+ - `claude_manager_approval_decisions` — view recent tool approval decisions.
82
+ - `claude_manager_approval_update` — add/remove rules, change default action, or enable/disable approval.
56
83
 
57
84
  ## Plugin-provided agents and commands
58
85
 
@@ -69,13 +96,26 @@ These are added to OpenCode config at runtime by the plugin, so they do not requ
69
96
  Typical flow inside OpenCode:
70
97
 
71
98
  1. Inspect Claude capabilities with `claude_manager_metadata`.
72
- 2. Delegate work with `claude_manager_run`.
73
- 3. Inspect saved Claude history with `claude_manager_sessions` or prior orchestration records with `claude_manager_runs`.
99
+ 2. Delegate work with `claude_manager_send`.
100
+ 3. Review changes with `claude_manager_git_diff`, then commit or reset.
101
+ 4. Inspect saved Claude history with `claude_manager_sessions` or prior orchestration records with `claude_manager_runs`.
102
+
103
+ Example tasks:
104
+
105
+ ```text
106
+ Use claude_manager_send to implement the new validation logic in src/auth.ts, then review with claude_manager_git_diff.
107
+ ```
108
+
109
+ Start a fresh session for an unrelated task:
110
+
111
+ ```text
112
+ Use claude_manager_send with freshSession:true to investigate the failing CI test in test/api.test.ts using mode:"plan".
113
+ ```
74
114
 
75
- Example task:
115
+ Reclaim context mid-session:
76
116
 
77
117
  ```text
78
- Use claude_manager_run to split this implementation into subagents and summarize the final result.
118
+ Use claude_manager_compact to free up context, then continue with the next implementation step.
79
119
  ```
80
120
 
81
121
  ## Local Development
@@ -136,8 +176,8 @@ After trusted publishing is working, you can tighten npm package security by dis
136
176
  ## Limitations
137
177
 
138
178
  - Claude slash commands and skills come primarily from filesystem discovery; SDK probing is available but optional.
139
- - Multiple subagents share one working directory and run one after another to avoid overlapping edits.
140
- - Run state is local to the repo under `.claude-manager/` and is ignored by git.
179
+ - Session state is local to the repo under `.claude-manager/` and is ignored by git.
180
+ - Context tracking is heuristic-based; actual SDK context usage may differ slightly.
141
181
 
142
182
  ## Scripts
143
183
 
@@ -0,0 +1,49 @@
1
+ import type { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
2
+ import type { ParallelSessionJobRecord, SessionMode } from '../types/contracts.js';
3
+ /**
4
+ * Runs additional Claude Code SDK sessions concurrently with the main persistent session.
5
+ * Same worktree from multiple jobs can conflict on git — callers should use plan mode or branches.
6
+ */
7
+ export declare class ParallelSessionJobManager {
8
+ private readonly sdkAdapter;
9
+ private readonly sessionPrompt;
10
+ private readonly modePrefixes;
11
+ private readonly jobs;
12
+ constructor(sdkAdapter: ClaudeAgentSdkAdapter, sessionPrompt: string, modePrefixes: {
13
+ plan: string;
14
+ free: string;
15
+ });
16
+ static get maxConcurrent(): number;
17
+ startJob(params: {
18
+ cwd: string;
19
+ message: string;
20
+ model?: string;
21
+ mode?: SessionMode;
22
+ resumeSessionId?: string;
23
+ }): {
24
+ ok: true;
25
+ jobId: string;
26
+ } | {
27
+ ok: false;
28
+ error: string;
29
+ };
30
+ private runJob;
31
+ abortJob(jobId: string): {
32
+ ok: true;
33
+ } | {
34
+ ok: false;
35
+ error: string;
36
+ };
37
+ getJob(jobId: string): ParallelSessionJobRecord | undefined;
38
+ listJobs(): ParallelSessionJobRecord[];
39
+ private runningCount;
40
+ waitForJobs(params: {
41
+ jobIds: string[];
42
+ mode: 'any' | 'all';
43
+ timeoutMs?: number;
44
+ }): Promise<{
45
+ finishedJobIds: string[];
46
+ timedOut: boolean;
47
+ pendingJobIds: string[];
48
+ }>;
49
+ }
@@ -0,0 +1,177 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const MAX_CONCURRENT = 5;
3
+ function createCompletionLatch() {
4
+ let resolve;
5
+ const promise = new Promise((r) => {
6
+ resolve = r;
7
+ });
8
+ return { promise, resolve };
9
+ }
10
+ /**
11
+ * Runs additional Claude Code SDK sessions concurrently with the main persistent session.
12
+ * Same worktree from multiple jobs can conflict on git — callers should use plan mode or branches.
13
+ */
14
+ export class ParallelSessionJobManager {
15
+ sdkAdapter;
16
+ sessionPrompt;
17
+ modePrefixes;
18
+ jobs = new Map();
19
+ constructor(sdkAdapter, sessionPrompt, modePrefixes) {
20
+ this.sdkAdapter = sdkAdapter;
21
+ this.sessionPrompt = sessionPrompt;
22
+ this.modePrefixes = modePrefixes;
23
+ }
24
+ static get maxConcurrent() {
25
+ return MAX_CONCURRENT;
26
+ }
27
+ startJob(params) {
28
+ if (this.runningCount() >= MAX_CONCURRENT) {
29
+ return {
30
+ ok: false,
31
+ error: `At most ${MAX_CONCURRENT} parallel Claude Code jobs may run at once. Wait or abort a job first.`,
32
+ };
33
+ }
34
+ const jobId = randomUUID();
35
+ const now = new Date().toISOString();
36
+ const mode = params.mode ?? 'free';
37
+ const prefix = this.modePrefixes[mode];
38
+ const prompt = prefix ? `${prefix}\n\n${params.message}` : params.message;
39
+ const input = {
40
+ cwd: params.cwd,
41
+ prompt,
42
+ persistSession: true,
43
+ permissionMode: mode === 'plan' ? 'plan' : 'acceptEdits',
44
+ includePartialMessages: true,
45
+ model: params.model,
46
+ resumeSessionId: params.resumeSessionId,
47
+ settingSources: ['user', 'project', 'local'],
48
+ agentProgressSummaries: true,
49
+ };
50
+ if (!params.resumeSessionId) {
51
+ input.systemPrompt = this.sessionPrompt;
52
+ input.model ??= 'claude-opus-4-6';
53
+ input.effort ??= 'high';
54
+ }
55
+ const abortController = new AbortController();
56
+ input.abortSignal = abortController.signal;
57
+ const preview = params.message.length > 120
58
+ ? `${params.message.slice(0, 120)}...`
59
+ : params.message;
60
+ const { promise: completion, resolve: resolveCompletion } = createCompletionLatch();
61
+ const record = {
62
+ id: jobId,
63
+ cwd: params.cwd,
64
+ status: 'running',
65
+ createdAt: now,
66
+ updatedAt: now,
67
+ promptPreview: preview,
68
+ resumeSessionId: params.resumeSessionId,
69
+ };
70
+ this.jobs.set(jobId, {
71
+ record,
72
+ abortController,
73
+ completion,
74
+ resolveCompletion,
75
+ });
76
+ void this.runJob(jobId, input, resolveCompletion);
77
+ return { ok: true, jobId };
78
+ }
79
+ async runJob(jobId, input, resolveCompletion) {
80
+ const entry = this.jobs.get(jobId);
81
+ if (!entry) {
82
+ resolveCompletion();
83
+ return;
84
+ }
85
+ try {
86
+ const result = await this.sdkAdapter.runSession(input);
87
+ const rec = entry.record;
88
+ rec.updatedAt = new Date().toISOString();
89
+ rec.status = 'completed';
90
+ rec.sessionId = result.sessionId;
91
+ rec.finalText = result.finalText;
92
+ rec.turns = result.turns;
93
+ rec.totalCostUsd = result.totalCostUsd;
94
+ }
95
+ catch (error) {
96
+ const rec = entry.record;
97
+ rec.updatedAt = new Date().toISOString();
98
+ if (rec.status !== 'aborted') {
99
+ rec.status = 'failed';
100
+ rec.error = error instanceof Error ? error.message : String(error);
101
+ }
102
+ }
103
+ finally {
104
+ resolveCompletion();
105
+ }
106
+ }
107
+ abortJob(jobId) {
108
+ const entry = this.jobs.get(jobId);
109
+ if (!entry) {
110
+ return { ok: false, error: 'Unknown job id' };
111
+ }
112
+ if (entry.record.status !== 'running') {
113
+ return { ok: false, error: 'Job is not running' };
114
+ }
115
+ entry.record.status = 'aborted';
116
+ entry.record.updatedAt = new Date().toISOString();
117
+ entry.abortController.abort();
118
+ return { ok: true };
119
+ }
120
+ getJob(jobId) {
121
+ return this.jobs.get(jobId)?.record;
122
+ }
123
+ listJobs() {
124
+ return [...this.jobs.values()]
125
+ .map((j) => j.record)
126
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
127
+ }
128
+ runningCount() {
129
+ let n = 0;
130
+ for (const { record } of this.jobs.values()) {
131
+ if (record.status === 'running') {
132
+ n += 1;
133
+ }
134
+ }
135
+ return n;
136
+ }
137
+ async waitForJobs(params) {
138
+ const unique = [...new Set(params.jobIds)].filter((id) => this.jobs.has(id));
139
+ if (unique.length === 0) {
140
+ return {
141
+ finishedJobIds: [],
142
+ timedOut: false,
143
+ pendingJobIds: [],
144
+ };
145
+ }
146
+ const waitWork = async () => {
147
+ if (params.mode === 'any') {
148
+ await Promise.race(unique.map((id) => this.jobs.get(id).completion));
149
+ }
150
+ else {
151
+ await Promise.all(unique.map((id) => this.jobs.get(id).completion));
152
+ }
153
+ };
154
+ let timedOut = false;
155
+ if (params.timeoutMs !== undefined && params.timeoutMs > 0) {
156
+ await Promise.race([
157
+ waitWork(),
158
+ new Promise((resolve) => {
159
+ setTimeout(() => {
160
+ timedOut = true;
161
+ resolve();
162
+ }, params.timeoutMs);
163
+ }),
164
+ ]);
165
+ }
166
+ else {
167
+ await waitWork();
168
+ }
169
+ const finishedJobIds = unique.filter((id) => this.jobs.get(id)?.record.status !== 'running');
170
+ const pendingJobIds = unique.filter((id) => this.jobs.get(id)?.record.status === 'running');
171
+ return {
172
+ finishedJobIds,
173
+ timedOut,
174
+ pendingJobIds,
175
+ };
176
+ }
177
+ }
@@ -19,6 +19,7 @@ export declare class PersistentManager {
19
19
  */
20
20
  sendMessage(cwd: string, message: string, options?: {
21
21
  model?: string;
22
+ effort?: 'low' | 'medium' | 'high' | 'max';
22
23
  mode?: 'plan' | 'free';
23
24
  abortSignal?: AbortSignal;
24
25
  }, onEvent?: ClaudeSessionEventHandler): Promise<{
@@ -3,6 +3,7 @@ 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_compact',
6
7
  'claude_manager_git_diff',
7
8
  'claude_manager_git_commit',
8
9
  'claude_manager_git_reset',
@@ -31,6 +32,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
31
32
  // Research agent can inspect but not send or modify
32
33
  researchPermissions[toolId] =
33
34
  toolId === 'claude_manager_send' ||
35
+ toolId === 'claude_manager_compact' ||
34
36
  toolId === 'claude_manager_git_commit' ||
35
37
  toolId === 'claude_manager_git_reset' ||
36
38
  toolId === 'claude_manager_clear' ||
@@ -112,15 +114,27 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
112
114
  'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
113
115
  'Returns the assistant response and current context health snapshot. ' +
114
116
  'Use mode "plan" for read-only investigation and planning (no edits), ' +
115
- 'or "free" (default) for normal execution with edit permissions.',
117
+ 'or "free" (default) for normal execution with edit permissions. ' +
118
+ 'Set freshSession to clear the active session before sending (use for unrelated tasks). ' +
119
+ 'Prefer claude-opus-4-6 (default) for most coding work; use a Sonnet model for faster/lighter tasks. ' +
120
+ 'Prefer effort "high" (default) for most work; use "medium" for lighter tasks and "max" for especially hard problems.',
116
121
  args: {
117
122
  message: tool.schema.string().min(1),
118
- model: tool.schema.string().optional(),
123
+ model: tool.schema
124
+ .enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
125
+ .optional(),
126
+ effort: tool.schema
127
+ .enum(['low', 'medium', 'high', 'max'])
128
+ .default('high'),
119
129
  mode: tool.schema.enum(['plan', 'free']).default('free'),
130
+ freshSession: tool.schema.boolean().default(false),
120
131
  cwd: tool.schema.string().optional(),
121
132
  },
122
133
  async execute(args, context) {
123
134
  const cwd = args.cwd ?? context.worktree;
135
+ if (args.freshSession) {
136
+ await services.manager.clearSession(cwd);
137
+ }
124
138
  const hasActiveSession = services.manager.getStatus().sessionId !== null;
125
139
  const promptPreview = args.message.length > 100
126
140
  ? args.message.slice(0, 100) + '...'
@@ -136,7 +150,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
136
150
  });
137
151
  let turnsSoFar = 0;
138
152
  let costSoFar = 0;
139
- const result = await services.manager.sendMessage(cwd, args.message, { model: args.model, mode: args.mode, abortSignal: context.abort }, (event) => {
153
+ const result = await services.manager.sendMessage(cwd, args.message, {
154
+ model: args.model,
155
+ effort: args.effort,
156
+ mode: args.mode,
157
+ abortSignal: context.abort,
158
+ }, (event) => {
140
159
  if (event.turns !== undefined) {
141
160
  turnsSoFar = event.turns;
142
161
  }
@@ -301,6 +320,36 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
301
320
  }, null, 2);
302
321
  },
303
322
  }),
323
+ claude_manager_compact: tool({
324
+ description: 'Compact the active Claude Code session to reclaim context space. ' +
325
+ 'Sends /compact to the session, which compresses prior conversation while preserving state. ' +
326
+ 'Use before clearing when context is high but the session still has useful state. ' +
327
+ 'Fails if there is no active session.',
328
+ args: {
329
+ cwd: tool.schema.string().optional(),
330
+ },
331
+ async execute(args, context) {
332
+ const cwd = args.cwd ?? context.worktree;
333
+ annotateToolRun(context, 'Compacting session', {});
334
+ const result = await services.manager.compactSession(cwd);
335
+ const snap = services.manager.getStatus();
336
+ const contextWarning = formatContextWarning(snap);
337
+ context.metadata({
338
+ title: contextWarning
339
+ ? `Claude Code: Compacted — context at ${snap.estimatedContextPercent}%`
340
+ : `Claude Code: Compacted (${snap.totalTurns} turns, $${(snap.totalCostUsd ?? 0).toFixed(4)})`,
341
+ metadata: { sessionId: result.sessionId },
342
+ });
343
+ return JSON.stringify({
344
+ sessionId: result.sessionId,
345
+ finalText: result.finalText,
346
+ turns: result.turns,
347
+ totalCostUsd: result.totalCostUsd,
348
+ context: snap,
349
+ contextWarning,
350
+ }, null, 2);
351
+ },
352
+ }),
304
353
  claude_manager_git_diff: tool({
305
354
  description: 'Run git diff to see all current changes (staged + unstaged) relative to HEAD.',
306
355
  args: {
@@ -72,13 +72,37 @@ export const managerPromptRegistry = {
72
72
  'Check the context snapshot returned by each send:',
73
73
  '- Under 50%: proceed freely.',
74
74
  '- 50–70%: finish current step, then evaluate if a fresh session is needed.',
75
- '- Over 70%: compact or clear before sending heavy instructions.',
75
+ '- Over 70%: use claude_manager_compact to reclaim context if the session',
76
+ ' still has useful state. Only clear if compaction is insufficient.',
76
77
  '- Over 85%: clear the session immediately.',
78
+ 'Use freshSession:true on claude_manager_send when switching to an unrelated',
79
+ 'task or when the session context is contaminated. Prefer this over a manual',
80
+ 'clear+send sequence — it is atomic and self-documenting.',
81
+ '',
82
+ '## Model and effort selection',
83
+ 'Choose model and effort deliberately before each delegation:',
84
+ '- claude-opus-4-6 + high effort: default for most coding tasks.',
85
+ '- claude-sonnet-4-6 or claude-sonnet-4-5: faster/lighter work (simple renames,',
86
+ ' formatting, test scaffolding, quick investigations).',
87
+ '- effort "medium": acceptable for lighter tasks that do not require deep reasoning.',
88
+ '- effort "max": reserve for unusually hard problems (complex refactors,',
89
+ ' subtle concurrency bugs, large cross-cutting changes).',
90
+ "- Do not use Haiku for this plugin's coding-agent role.",
91
+ '',
92
+ '## Plan mode',
93
+ 'When delegating with mode:"plan", Claude Code returns a read-only',
94
+ 'implementation plan. The plan MUST be returned inline in the assistant',
95
+ 'response — do NOT write plan artifacts to disk, create files, or rely on',
96
+ 'ExitPlanMode. Treat the returned finalText as the plan. If the plan is',
97
+ 'acceptable, switch to mode:"free" and delegate the implementation steps.',
77
98
  '',
78
99
  '## Tools reference',
79
100
  'todowrite / todoread — OpenCode session todo list (track multi-step work)',
80
101
  'question — OpenCode user prompt with options (clarify trade-offs)',
81
102
  'claude_manager_send — send instruction (creates or resumes session)',
103
+ ' freshSession:true — clear session first (use for unrelated tasks)',
104
+ ' model / effort — choose deliberately (see "Model and effort selection")',
105
+ 'claude_manager_compact — compress session context (preserves session state)',
82
106
  'claude_manager_git_diff — review all uncommitted changes',
83
107
  'claude_manager_git_commit — stage all + commit',
84
108
  'claude_manager_git_reset — hard reset + clean (destructive)',
@@ -125,10 +149,12 @@ export const managerPromptRegistry = {
125
149
  modePrefixes: {
126
150
  plan: [
127
151
  '[PLAN MODE] You are in read-only planning mode. Do NOT create or edit any files.',
152
+ 'Do NOT use ExitPlanMode or write plan artifacts to disk.',
128
153
  'Use read, grep, glob, and search tools only.',
129
154
  'Analyze the codebase and produce a detailed implementation plan:',
130
155
  'files to change, functions to modify, new files to create, test strategy,',
131
156
  'and potential risks. End with a numbered step-by-step plan.',
157
+ 'Return the entire plan inline in your response text.',
132
158
  ].join(' '),
133
159
  free: '',
134
160
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",