@doingdev/opencode-claude-manager-plugin 0.1.35 → 0.1.43

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.
Files changed (90) hide show
  1. package/dist/claude/claude-agent-sdk-adapter.js +1 -0
  2. package/dist/manager/git-operations.d.ts +10 -1
  3. package/dist/manager/git-operations.js +18 -3
  4. package/dist/manager/persistent-manager.d.ts +19 -3
  5. package/dist/manager/persistent-manager.js +21 -9
  6. package/dist/manager/session-controller.d.ts +8 -5
  7. package/dist/manager/session-controller.js +25 -20
  8. package/dist/metadata/claude-metadata.service.d.ts +12 -0
  9. package/dist/metadata/claude-metadata.service.js +38 -0
  10. package/dist/metadata/repo-claude-config-reader.d.ts +7 -0
  11. package/dist/metadata/repo-claude-config-reader.js +154 -0
  12. package/dist/plugin/agent-hierarchy.d.ts +9 -9
  13. package/dist/plugin/agent-hierarchy.js +25 -25
  14. package/dist/plugin/claude-manager.plugin.js +83 -46
  15. package/dist/plugin/orchestrator.plugin.d.ts +2 -0
  16. package/dist/plugin/orchestrator.plugin.js +116 -0
  17. package/dist/plugin/service-factory.js +3 -8
  18. package/dist/prompts/registry.js +100 -103
  19. package/dist/providers/claude-code-wrapper.d.ts +13 -0
  20. package/dist/providers/claude-code-wrapper.js +13 -0
  21. package/dist/safety/bash-safety.d.ts +21 -0
  22. package/dist/safety/bash-safety.js +62 -0
  23. package/dist/src/claude/claude-agent-sdk-adapter.d.ts +27 -0
  24. package/dist/src/claude/claude-agent-sdk-adapter.js +517 -0
  25. package/dist/src/claude/claude-session.service.d.ts +10 -0
  26. package/dist/src/claude/claude-session.service.js +18 -0
  27. package/dist/src/claude/session-live-tailer.d.ts +51 -0
  28. package/dist/src/claude/session-live-tailer.js +269 -0
  29. package/dist/src/claude/tool-approval-manager.d.ts +27 -0
  30. package/dist/src/claude/tool-approval-manager.js +232 -0
  31. package/dist/src/index.d.ts +6 -0
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/manager/context-tracker.d.ts +33 -0
  34. package/dist/src/manager/context-tracker.js +106 -0
  35. package/dist/src/manager/git-operations.d.ts +12 -0
  36. package/dist/src/manager/git-operations.js +76 -0
  37. package/dist/src/manager/persistent-manager.d.ts +77 -0
  38. package/dist/src/manager/persistent-manager.js +170 -0
  39. package/dist/src/manager/session-controller.d.ts +44 -0
  40. package/dist/src/manager/session-controller.js +147 -0
  41. package/dist/src/plugin/agent-hierarchy.d.ts +60 -0
  42. package/dist/src/plugin/agent-hierarchy.js +157 -0
  43. package/dist/src/plugin/claude-manager.plugin.d.ts +2 -0
  44. package/dist/src/plugin/claude-manager.plugin.js +563 -0
  45. package/dist/src/plugin/service-factory.d.ts +12 -0
  46. package/dist/src/plugin/service-factory.js +38 -0
  47. package/dist/src/prompts/registry.d.ts +11 -0
  48. package/dist/src/prompts/registry.js +260 -0
  49. package/dist/src/state/file-run-state-store.d.ts +14 -0
  50. package/dist/src/state/file-run-state-store.js +85 -0
  51. package/dist/src/state/transcript-store.d.ts +15 -0
  52. package/dist/src/state/transcript-store.js +44 -0
  53. package/dist/src/types/contracts.d.ts +200 -0
  54. package/dist/src/types/contracts.js +1 -0
  55. package/dist/src/util/fs-helpers.d.ts +2 -0
  56. package/dist/src/util/fs-helpers.js +10 -0
  57. package/dist/src/util/project-context.d.ts +10 -0
  58. package/dist/src/util/project-context.js +105 -0
  59. package/dist/src/util/transcript-append.d.ts +7 -0
  60. package/dist/src/util/transcript-append.js +29 -0
  61. package/dist/test/claude-agent-sdk-adapter.test.d.ts +1 -0
  62. package/dist/test/claude-agent-sdk-adapter.test.js +459 -0
  63. package/dist/test/claude-manager.plugin.test.d.ts +1 -0
  64. package/dist/test/claude-manager.plugin.test.js +331 -0
  65. package/dist/test/context-tracker.test.d.ts +1 -0
  66. package/dist/test/context-tracker.test.js +138 -0
  67. package/dist/test/file-run-state-store.test.d.ts +1 -0
  68. package/dist/test/file-run-state-store.test.js +82 -0
  69. package/dist/test/git-operations.test.d.ts +1 -0
  70. package/dist/test/git-operations.test.js +90 -0
  71. package/dist/test/persistent-manager.test.d.ts +1 -0
  72. package/dist/test/persistent-manager.test.js +208 -0
  73. package/dist/test/project-context.test.d.ts +1 -0
  74. package/dist/test/project-context.test.js +92 -0
  75. package/dist/test/prompt-registry.test.d.ts +1 -0
  76. package/dist/test/prompt-registry.test.js +256 -0
  77. package/dist/test/session-controller.test.d.ts +1 -0
  78. package/dist/test/session-controller.test.js +149 -0
  79. package/dist/test/session-live-tailer.test.d.ts +1 -0
  80. package/dist/test/session-live-tailer.test.js +313 -0
  81. package/dist/test/tool-approval-manager.test.d.ts +1 -0
  82. package/dist/test/tool-approval-manager.test.js +264 -0
  83. package/dist/test/transcript-append.test.d.ts +1 -0
  84. package/dist/test/transcript-append.test.js +37 -0
  85. package/dist/test/transcript-store.test.d.ts +1 -0
  86. package/dist/test/transcript-store.test.js +50 -0
  87. package/dist/types/contracts.d.ts +3 -4
  88. package/dist/vitest.config.d.ts +2 -0
  89. package/dist/vitest.config.js +11 -0
  90. package/package.json +2 -2
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Agent hierarchy configuration for the CTO + Engineer Wrapper architecture.
3
3
  *
4
- * CTO (cto) — pure orchestrator, spawns engineers, reviews diffs, commits
5
- * Engineer Plan (engineer_plan) — manages a Claude Code session for read-only investigation
6
- * Engineer Build (engineer_build) — manages a Claude Code session for implementation
7
- * Claude Code session — the underlying AI session (prompt only, no OpenCode agent)
4
+ * CTO (cto) — pure orchestrator, spawns engineers, reviews diffs, commits
5
+ * Engineer Explore (engineer_explore) — manages a Claude Code session for read-only investigation
6
+ * Engineer Implement (engineer_implement) — manages a Claude Code session for implementation
7
+ * Claude Code session — the underlying AI session (prompt only, no OpenCode agent)
8
8
  */
9
9
  // ---------------------------------------------------------------------------
10
10
  // Agent names
11
11
  // ---------------------------------------------------------------------------
12
12
  export const AGENT_CTO = 'cto';
13
- export const AGENT_ENGINEER_PLAN = 'engineer_plan';
14
- export const AGENT_ENGINEER_BUILD = 'engineer_build';
13
+ export const AGENT_ENGINEER_EXPLORE = 'engineer_explore';
14
+ export const AGENT_ENGINEER_IMPLEMENT = 'engineer_implement';
15
15
  // ---------------------------------------------------------------------------
16
16
  // Tool IDs — grouped by domain
17
17
  // ---------------------------------------------------------------------------
@@ -25,12 +25,12 @@ const ENGINEER_SHARED_TOOL_IDS = [
25
25
  ];
26
26
  /** All engineer tools — mode-locked sends + shared session tools */
27
27
  const ENGINEER_TOOL_IDS = ['explore', 'implement', ...ENGINEER_SHARED_TOOL_IDS];
28
- /** Tools for the engineer_plan wrapper (plan-mode send + shared) */
29
- const ENGINEER_PLAN_TOOL_IDS = ['explore', ...ENGINEER_SHARED_TOOL_IDS];
30
- /** Tools for the engineer_build wrapper (build-mode send + shared) */
31
- const ENGINEER_BUILD_TOOL_IDS = ['implement', ...ENGINEER_SHARED_TOOL_IDS];
28
+ /** Tools for the engineer_explore wrapper (explore-mode send + shared) */
29
+ const ENGINEER_EXPLORE_TOOL_IDS = ['explore', ...ENGINEER_SHARED_TOOL_IDS];
30
+ /** Tools for the engineer_implement wrapper (implement-mode send + shared) */
31
+ const ENGINEER_IMPLEMENT_TOOL_IDS = ['implement', ...ENGINEER_SHARED_TOOL_IDS];
32
32
  /** Git tools — owned by CTO */
33
- const GIT_TOOL_IDS = ['git_diff', 'git_commit', 'git_reset'];
33
+ const GIT_TOOL_IDS = ['git_diff', 'git_commit', 'git_reset', 'git_status', 'git_log'];
34
34
  /** Approval tools — owned by CTO */
35
35
  const APPROVAL_TOOL_IDS = ['approval_policy', 'approval_decisions', 'approval_update'];
36
36
  /** All restricted tool IDs (union of all domain groups) */
@@ -77,19 +77,19 @@ function buildCtoPermissions() {
77
77
  ...allowed,
78
78
  task: {
79
79
  '*': 'deny',
80
- [AGENT_ENGINEER_PLAN]: 'allow',
81
- [AGENT_ENGINEER_BUILD]: 'allow',
80
+ [AGENT_ENGINEER_EXPLORE]: 'allow',
81
+ [AGENT_ENGINEER_IMPLEMENT]: 'allow',
82
82
  },
83
83
  };
84
84
  }
85
- /** Engineer plan wrapper: read-only investigation + explore + shared session tools. */
86
- function buildEngineerPlanPermissions() {
85
+ /** Engineer explore wrapper: read-only investigation + explore + shared session tools. */
86
+ function buildEngineerExplorePermissions() {
87
87
  const denied = {};
88
88
  for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
89
89
  denied[toolId] = 'deny';
90
90
  }
91
91
  const allowed = {};
92
- for (const toolId of ENGINEER_PLAN_TOOL_IDS) {
92
+ for (const toolId of ENGINEER_EXPLORE_TOOL_IDS) {
93
93
  allowed[toolId] = 'allow';
94
94
  }
95
95
  return {
@@ -99,14 +99,14 @@ function buildEngineerPlanPermissions() {
99
99
  ...allowed,
100
100
  };
101
101
  }
102
- /** Engineer build wrapper: read-only investigation + implement + shared session tools. */
103
- function buildEngineerBuildPermissions() {
102
+ /** Engineer implement wrapper: read-only investigation + implement + shared session tools. */
103
+ function buildEngineerImplementPermissions() {
104
104
  const denied = {};
105
105
  for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
106
106
  denied[toolId] = 'deny';
107
107
  }
108
108
  const allowed = {};
109
- for (const toolId of ENGINEER_BUILD_TOOL_IDS) {
109
+ for (const toolId of ENGINEER_IMPLEMENT_TOOL_IDS) {
110
110
  allowed[toolId] = 'allow';
111
111
  }
112
112
  return {
@@ -128,22 +128,22 @@ export function buildCtoAgentConfig(prompts) {
128
128
  prompt: prompts.ctoSystemPrompt,
129
129
  };
130
130
  }
131
- export function buildEngineerPlanAgentConfig(prompts) {
131
+ export function buildEngineerExploreAgentConfig(prompts) {
132
132
  return {
133
133
  description: 'Thin high-judgment wrapper that frames work quickly and dispatches to Claude Code in plan mode for read-only investigation.',
134
134
  mode: 'subagent',
135
135
  color: '#D97757',
136
- permission: buildEngineerPlanPermissions(),
137
- prompt: prompts.engineerPlanPrompt,
136
+ permission: buildEngineerExplorePermissions(),
137
+ prompt: prompts.engineerExplorePrompt,
138
138
  };
139
139
  }
140
- export function buildEngineerBuildAgentConfig(prompts) {
140
+ export function buildEngineerImplementAgentConfig(prompts) {
141
141
  return {
142
142
  description: 'Thin high-judgment wrapper that frames work quickly and dispatches to Claude Code in free mode for implementation.',
143
143
  mode: 'subagent',
144
144
  color: '#D97757',
145
- permission: buildEngineerBuildPermissions(),
146
- prompt: prompts.engineerBuildPrompt,
145
+ permission: buildEngineerImplementPermissions(),
146
+ prompt: prompts.engineerImplementPrompt,
147
147
  };
148
148
  }
149
149
  // ---------------------------------------------------------------------------
@@ -1,16 +1,16 @@
1
1
  import { tool } from '@opencode-ai/plugin';
2
2
  import { composeWrapperPrompt, managerPromptRegistry } from '../prompts/registry.js';
3
3
  import { discoverProjectClaudeFiles } from '../util/project-context.js';
4
- import { AGENT_CTO, AGENT_ENGINEER_BUILD, AGENT_ENGINEER_PLAN, buildCtoAgentConfig, buildEngineerBuildAgentConfig, buildEngineerPlanAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
4
+ import { AGENT_CTO, AGENT_ENGINEER_EXPLORE, AGENT_ENGINEER_IMPLEMENT, buildCtoAgentConfig, buildEngineerExploreAgentConfig, buildEngineerImplementAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
5
5
  import { getOrCreatePluginServices } from './service-factory.js';
6
6
  export const ClaudeManagerPlugin = async ({ worktree }) => {
7
7
  const services = getOrCreatePluginServices(worktree);
8
8
  async function executeDelegate(args, context) {
9
- const cwd = args.cwd ?? context.worktree;
9
+ const wrapperServices = getOrCreatePluginServices(context.worktree);
10
10
  if (args.freshSession) {
11
- await services.manager.clearSession(cwd);
11
+ await wrapperServices.manager.clearSession();
12
12
  }
13
- const hasActiveSession = services.manager.getStatus().sessionId !== null;
13
+ const hasActiveSession = wrapperServices.manager.getStatus().sessionId !== null;
14
14
  const promptPreview = args.message.length > 100 ? args.message.slice(0, 100) + '...' : args.message;
15
15
  context.metadata({
16
16
  title: hasActiveSession
@@ -18,16 +18,17 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
18
18
  : '⚡ Claude Code: Initializing...',
19
19
  metadata: {
20
20
  status: 'running',
21
- sessionId: services.manager.getStatus().sessionId,
21
+ sessionId: wrapperServices.manager.getStatus().sessionId,
22
22
  prompt: promptPreview,
23
23
  },
24
24
  });
25
25
  let turnsSoFar;
26
26
  let costSoFar;
27
- const result = await services.manager.sendMessage(cwd, args.message, {
27
+ const result = await wrapperServices.manager.sendMessage(context.worktree, args.message, {
28
28
  model: args.model,
29
29
  effort: args.effort,
30
30
  mode: args.mode,
31
+ sessionSystemPrompt: args.sessionSystemPrompt,
31
32
  abortSignal: context.abort,
32
33
  }, (event) => {
33
34
  if (event.turns !== undefined) {
@@ -182,7 +183,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
182
183
  let toolOutputs = [];
183
184
  if (result.sessionId) {
184
185
  try {
185
- toolOutputs = await services.liveTailer.getToolOutputPreview(result.sessionId, cwd, 3);
186
+ toolOutputs = await wrapperServices.liveTailer.getToolOutputPreview(result.sessionId, context.worktree, 3);
186
187
  }
187
188
  catch {
188
189
  // Non-critical — the JSONL file may not exist yet.
@@ -210,12 +211,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
210
211
  const claudeFiles = await discoverProjectClaudeFiles(worktree);
211
212
  const derivedPrompts = {
212
213
  ...managerPromptRegistry,
213
- engineerPlanPrompt: composeWrapperPrompt(managerPromptRegistry.engineerPlanPrompt, claudeFiles),
214
- engineerBuildPrompt: composeWrapperPrompt(managerPromptRegistry.engineerBuildPrompt, claudeFiles),
214
+ engineerExplorePrompt: composeWrapperPrompt(managerPromptRegistry.engineerExplorePrompt, claudeFiles),
215
+ engineerImplementPrompt: composeWrapperPrompt(managerPromptRegistry.engineerImplementPrompt, claudeFiles),
215
216
  };
216
217
  config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
217
- config.agent[AGENT_ENGINEER_PLAN] ??= buildEngineerPlanAgentConfig(derivedPrompts);
218
- config.agent[AGENT_ENGINEER_BUILD] ??= buildEngineerBuildAgentConfig(derivedPrompts);
218
+ config.agent[AGENT_ENGINEER_EXPLORE] ??= buildEngineerExploreAgentConfig(derivedPrompts);
219
+ config.agent[AGENT_ENGINEER_IMPLEMENT] ??= buildEngineerImplementAgentConfig(derivedPrompts);
219
220
  },
220
221
  tool: {
221
222
  explore: tool({
@@ -225,14 +226,14 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
225
226
  args: {
226
227
  message: tool.schema.string().min(1),
227
228
  model: tool.schema
228
- .enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
229
+ .enum(['claude-opus-4-6', 'claude-sonnet-4-6'])
229
230
  .optional(),
230
- effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
231
+ effort: tool.schema.enum(['medium', 'high', 'max']).default('high'),
231
232
  freshSession: tool.schema.boolean().default(false),
232
- cwd: tool.schema.string().optional(),
233
+ sessionSystemPrompt: tool.schema.string().optional(),
233
234
  },
234
235
  async execute(args, context) {
235
- return executeDelegate({ ...args, mode: 'plan' }, context);
236
+ return executeDelegate({ ...args, mode: 'plan', wrapperType: 'explore' }, context);
236
237
  },
237
238
  }),
238
239
  implement: tool({
@@ -241,27 +242,27 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
241
242
  args: {
242
243
  message: tool.schema.string().min(1),
243
244
  model: tool.schema
244
- .enum(['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5'])
245
+ .enum(['claude-opus-4-6', 'claude-sonnet-4-6'])
245
246
  .optional(),
246
- effort: tool.schema.enum(['low', 'medium', 'high', 'max']).default('high'),
247
+ effort: tool.schema.enum(['medium', 'high', 'max']).default('high'),
247
248
  freshSession: tool.schema.boolean().default(false),
248
- cwd: tool.schema.string().optional(),
249
+ sessionSystemPrompt: tool.schema.string().optional(),
249
250
  },
250
251
  async execute(args, context) {
251
- return executeDelegate({ ...args, mode: 'free' }, context);
252
+ return executeDelegate({ ...args, mode: 'free', wrapperType: 'implement' }, context);
252
253
  },
253
254
  }),
254
255
  compact_context: tool({
255
256
  description: 'Compress session history to reclaim context window space. ' +
256
257
  'Preserves state while reducing token usage.',
257
258
  args: {
258
- cwd: tool.schema.string().optional(),
259
+ wrapperType: tool.schema.string().optional(),
259
260
  },
260
261
  async execute(args, context) {
261
- const cwd = args.cwd ?? context.worktree;
262
+ const wrapperServices = getOrCreatePluginServices(context.worktree);
262
263
  annotateToolRun(context, 'Compacting session', {});
263
- const result = await services.manager.compactSession(cwd);
264
- const snap = services.manager.getStatus();
264
+ const result = await wrapperServices.manager.compactSession(context.worktree);
265
+ const snap = wrapperServices.manager.getStatus();
265
266
  const contextWarning = formatContextWarning(snap);
266
267
  context.metadata({
267
268
  title: contextWarning
@@ -283,13 +284,27 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
283
284
  },
284
285
  }),
285
286
  git_diff: tool({
286
- description: 'Run git diff to see all current changes (staged + unstaged) relative to HEAD.',
287
+ description: 'Show diff of uncommitted changes. ' +
288
+ 'Use paths to filter to specific files/dirs. ' +
289
+ 'Use staged=true to see staged changes. ' +
290
+ 'Use ref to compare against a branch/tag/commit (e.g., ref="main").',
287
291
  args: {
288
- cwd: tool.schema.string().optional(),
292
+ paths: tool.schema.string().array().optional(),
293
+ staged: tool.schema.boolean().optional(),
294
+ ref: tool.schema.string().optional(),
289
295
  },
290
- async execute(_args, context) {
291
- annotateToolRun(context, 'Running git diff', {});
292
- const result = await services.manager.gitDiff();
296
+ async execute(args, context) {
297
+ annotateToolRun(context, 'Running git diff', {
298
+ paths: args.paths,
299
+ staged: args.staged,
300
+ ref: args.ref,
301
+ });
302
+ const paths = args.paths?.filter((p) => p !== undefined);
303
+ const result = await services.manager.gitDiff({
304
+ paths,
305
+ staged: args.staged,
306
+ ref: args.ref,
307
+ });
293
308
  return JSON.stringify(result, null, 2);
294
309
  },
295
310
  }),
@@ -297,7 +312,6 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
297
312
  description: 'Stage all changes and commit with the given message.',
298
313
  args: {
299
314
  message: tool.schema.string().min(1),
300
- cwd: tool.schema.string().optional(),
301
315
  },
302
316
  async execute(args, context) {
303
317
  annotateToolRun(context, 'Committing changes', {
@@ -308,39 +322,61 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
308
322
  },
309
323
  }),
310
324
  git_reset: tool({
311
- description: 'Run git reset --hard HEAD and git clean -fd to discard ALL uncommitted changes and untracked files.',
312
- args: {
313
- cwd: tool.schema.string().optional(),
314
- },
325
+ description: 'Discard all uncommitted changes: runs git reset --hard HEAD and git clean -fd.',
326
+ args: {},
315
327
  async execute(_args, context) {
316
328
  annotateToolRun(context, 'Resetting working directory', {});
317
329
  const result = await services.manager.gitReset();
318
330
  return JSON.stringify(result, null, 2);
319
331
  },
320
332
  }),
333
+ git_status: tool({
334
+ description: 'Show working tree status — lists changed files in short format. ' +
335
+ 'Returns isClean=true if nothing changed.',
336
+ args: {},
337
+ async execute(_args, context) {
338
+ annotateToolRun(context, 'Checking git status', {});
339
+ const result = await services.manager.gitStatus();
340
+ return JSON.stringify(result, null, 2);
341
+ },
342
+ }),
343
+ git_log: tool({
344
+ description: 'Show recent commits in short format. ' +
345
+ 'Default shows last 5 commits. Use count to change.',
346
+ args: {
347
+ count: tool.schema.number().optional(),
348
+ },
349
+ async execute(args, context) {
350
+ annotateToolRun(context, 'Fetching git log', { count: args.count });
351
+ const result = await services.manager.gitLog(args.count ?? 5);
352
+ return result;
353
+ },
354
+ }),
321
355
  clear_session: tool({
322
356
  description: 'Clear the active session to start fresh. ' +
323
357
  'Use when context is full or starting a new task.',
324
358
  args: {
325
- cwd: tool.schema.string().optional(),
359
+ wrapperType: tool.schema.string().optional(),
326
360
  reason: tool.schema.string().optional(),
327
361
  },
328
362
  async execute(args, context) {
363
+ const wrapperServices = getOrCreatePluginServices(context.worktree);
329
364
  annotateToolRun(context, 'Clearing session', {
330
365
  reason: args.reason,
331
366
  });
332
- const clearedId = await services.manager.clearSession(args.cwd ?? context.worktree);
367
+ const clearedId = await wrapperServices.manager.clearSession();
333
368
  return JSON.stringify({ clearedSessionId: clearedId });
334
369
  },
335
370
  }),
336
371
  session_health: tool({
337
372
  description: 'Check session health metrics: context usage %, turn count, cost, and session ID.',
338
373
  args: {
339
- cwd: tool.schema.string().optional(),
374
+ wrapperType: tool.schema.string().optional(),
340
375
  },
341
- async execute(_args, context) {
376
+ async execute(args, context) {
377
+ const wrapperServices = getOrCreatePluginServices(context.worktree);
342
378
  annotateToolRun(context, 'Checking session status', {});
343
- const status = services.manager.getStatus();
379
+ const status = wrapperServices.manager.getStatus();
344
380
  return JSON.stringify({
345
381
  ...status,
346
382
  transcriptFile: status.sessionId
@@ -353,39 +389,40 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
353
389
  list_transcripts: tool({
354
390
  description: 'List available session transcripts or inspect a specific transcript by ID.',
355
391
  args: {
356
- cwd: tool.schema.string().optional(),
392
+ wrapperType: tool.schema.string().optional(),
357
393
  sessionId: tool.schema.string().optional(),
358
394
  },
359
395
  async execute(args, context) {
396
+ const wrapperServices = getOrCreatePluginServices(context.worktree);
360
397
  annotateToolRun(context, 'Inspecting Claude session history', {});
361
- const cwd = args.cwd ?? context.worktree;
362
398
  if (args.sessionId) {
363
399
  const [sdkTranscript, localEvents] = await Promise.all([
364
- services.sessions.getTranscript(args.sessionId, cwd),
365
- services.manager.getTranscriptEvents(cwd, args.sessionId),
400
+ wrapperServices.sessions.getTranscript(args.sessionId, context.worktree),
401
+ wrapperServices.manager.getTranscriptEvents(context.worktree, args.sessionId),
366
402
  ]);
367
403
  return JSON.stringify({
368
404
  sdkTranscript,
369
405
  localEvents: localEvents.length > 0 ? localEvents : undefined,
370
406
  }, null, 2);
371
407
  }
372
- const sessions = await services.sessions.listSessions(cwd);
408
+ const sessions = await wrapperServices.sessions.listSessions(context.worktree);
373
409
  return JSON.stringify(sessions, null, 2);
374
410
  },
375
411
  }),
376
412
  list_history: tool({
377
413
  description: 'List persistent run records from the manager or inspect a specific run.',
378
414
  args: {
379
- cwd: tool.schema.string().optional(),
415
+ wrapperType: tool.schema.string().optional(),
380
416
  runId: tool.schema.string().optional(),
381
417
  },
382
418
  async execute(args, context) {
419
+ const wrapperServices = getOrCreatePluginServices(context.worktree);
383
420
  annotateToolRun(context, 'Reading manager run state', {});
384
421
  if (args.runId) {
385
- const run = await services.manager.getRun(args.cwd ?? context.worktree, args.runId);
422
+ const run = await wrapperServices.manager.getRun(context.worktree, args.runId);
386
423
  return JSON.stringify(run, null, 2);
387
424
  }
388
- const runs = await services.manager.listRuns(args.cwd ?? context.worktree);
425
+ const runs = await wrapperServices.manager.listRuns(context.worktree);
389
426
  return JSON.stringify(runs, null, 2);
390
427
  },
391
428
  }),
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+ export declare const OrchestratorPlugin: Plugin;
@@ -0,0 +1,116 @@
1
+ import { prompts } from '../prompts/registry.js';
2
+ import { evaluateBashCommand, extractBashCommand, } from '../safety/bash-safety.js';
3
+ /**
4
+ * Thin OpenCode orchestrator plugin with Claude Code specialist subagents.
5
+ *
6
+ * - Registers `claude-code` provider via a local shim over ai-sdk-provider-claude-code.
7
+ * - Creates one orchestrator agent (uses the user's default OpenCode model).
8
+ * - Creates 4 Claude Code subagents: planning + build × opus + sonnet.
9
+ * - Enforces bash safety via the permission.ask hook.
10
+ *
11
+ * NOTE: Claude Code `effort` is not configurable through OpenCode provider/model
12
+ * options at this time. The subagent prompts compensate by setting high-quality
13
+ * expectations directly.
14
+ */
15
+ // Resolve the shim path at module load time so it is stable for the lifetime
16
+ // of the process. The compiled output for this file sits at dist/plugin/ and
17
+ // the shim at dist/providers/, so we walk up one level.
18
+ const claudeCodeShimUrl = new URL('../providers/claude-code-wrapper.js', import.meta.url).href;
19
+ export const OrchestratorPlugin = async () => {
20
+ return {
21
+ config: async (config) => {
22
+ config.provider ??= {};
23
+ config.agent ??= {};
24
+ // ── Provider ──────────────────────────────────────────────────────
25
+ // Uses a file:// shim so OpenCode's factory-finder heuristic sees only
26
+ // createClaudeCode and not createAPICallError from the upstream package.
27
+ config.provider['claude-code'] ??= {
28
+ npm: claudeCodeShimUrl,
29
+ models: {
30
+ opus: {
31
+ id: 'opus',
32
+ name: 'Claude Code Opus 4.6',
33
+ },
34
+ sonnet: {
35
+ id: 'sonnet',
36
+ name: 'Claude Code Sonnet 4.6',
37
+ },
38
+ },
39
+ };
40
+ // ── Orchestrator (uses user's default model — no model set) ───────
41
+ config.agent['opencode-orchestrator'] ??= {
42
+ description: 'CTO-level orchestrator that gathers context and delegates coding to Claude Code specialists.',
43
+ mode: 'primary',
44
+ color: '#D97757',
45
+ prompt: prompts.orchestrator,
46
+ permission: {
47
+ '*': 'deny',
48
+ read: 'allow',
49
+ grep: 'allow',
50
+ glob: 'allow',
51
+ list: 'allow',
52
+ webfetch: 'allow',
53
+ question: 'allow',
54
+ todowrite: 'allow',
55
+ todoread: 'allow',
56
+ task: 'allow',
57
+ bash: 'deny',
58
+ edit: 'deny',
59
+ skill: 'deny',
60
+ },
61
+ };
62
+ // ── Planning subagents ────────────────────────────────────────────
63
+ // Claude Code tools (Bash, Read, Write, Edit, …) are executed internally
64
+ // by the claude CLI subprocess and streamed back with providerExecuted:true.
65
+ // OpenCode's own tools must not be advertised to these agents.
66
+ const claudeCodePermissions = {
67
+ '*': 'deny',
68
+ };
69
+ config.agent['claude-code-planning-opus'] ??= {
70
+ description: 'Claude Code Opus specialist for investigation, architecture, and planning.',
71
+ model: 'claude-code/opus',
72
+ mode: 'subagent',
73
+ color: 'info',
74
+ prompt: prompts.planningAgent,
75
+ permission: { ...claudeCodePermissions },
76
+ };
77
+ config.agent['claude-code-planning-sonnet'] ??= {
78
+ description: 'Claude Code Sonnet specialist for lighter investigation and planning.',
79
+ model: 'claude-code/sonnet',
80
+ mode: 'subagent',
81
+ color: 'info',
82
+ prompt: prompts.planningAgent,
83
+ permission: { ...claudeCodePermissions },
84
+ };
85
+ // ── Build subagents ───────────────────────────────────────────────
86
+ config.agent['claude-code-build-opus'] ??= {
87
+ description: 'Claude Code Opus specialist for implementation and validation.',
88
+ model: 'claude-code/opus',
89
+ mode: 'subagent',
90
+ color: 'success',
91
+ prompt: prompts.buildAgent,
92
+ permission: { ...claudeCodePermissions },
93
+ };
94
+ config.agent['claude-code-build-sonnet'] ??= {
95
+ description: 'Claude Code Sonnet specialist for lighter implementation tasks.',
96
+ model: 'claude-code/sonnet',
97
+ mode: 'subagent',
98
+ color: 'success',
99
+ prompt: prompts.buildAgent,
100
+ permission: { ...claudeCodePermissions },
101
+ };
102
+ },
103
+ // ── Bash safety via permission.ask hook ────────────────────────────
104
+ // Handles both v1 Permission ({ type, pattern }) and v2 PermissionRequest
105
+ // ({ permission, patterns }) via runtime narrowing in extractBashCommand.
106
+ 'permission.ask': async (input, output) => {
107
+ const command = extractBashCommand(input);
108
+ if (command === null)
109
+ return;
110
+ const result = evaluateBashCommand(command);
111
+ if (!result.allowed) {
112
+ output.status = 'deny';
113
+ }
114
+ },
115
+ };
116
+ };
@@ -9,23 +9,19 @@ import { GitOperations } from '../manager/git-operations.js';
9
9
  import { SessionController } from '../manager/session-controller.js';
10
10
  import { PersistentManager } from '../manager/persistent-manager.js';
11
11
  import { managerPromptRegistry } from '../prompts/registry.js';
12
- const serviceCache = new Map();
13
12
  export function getOrCreatePluginServices(worktree) {
14
- const cachedServices = serviceCache.get(worktree);
15
- if (cachedServices) {
16
- return cachedServices;
17
- }
18
13
  const approvalManager = new ToolApprovalManager();
19
14
  const sdkAdapter = new ClaudeAgentSdkAdapter(undefined, approvalManager);
20
15
  const sessionService = new ClaudeSessionService(sdkAdapter);
21
16
  const contextTracker = new ContextTracker();
22
- const sessionController = new SessionController(sdkAdapter, contextTracker, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.modePrefixes);
17
+ const sessionController = new SessionController(sdkAdapter, contextTracker, undefined, // session prompt is now constructed dynamically by the wrapper
18
+ 'default', worktree, managerPromptRegistry.modePrefixes);
23
19
  const gitOps = new GitOperations(worktree);
24
20
  const stateStore = new FileRunStateStore();
25
21
  const transcriptStore = new TranscriptStore();
26
22
  const manager = new PersistentManager(sessionController, gitOps, stateStore, contextTracker, transcriptStore);
27
23
  // Try to restore active session state (fire and forget)
28
- manager.tryRestore(worktree).catch(() => { });
24
+ manager.tryRestore().catch(() => { });
29
25
  const liveTailer = new SessionLiveTailer();
30
26
  const services = {
31
27
  manager,
@@ -33,6 +29,5 @@ export function getOrCreatePluginServices(worktree) {
33
29
  approvalManager,
34
30
  liveTailer,
35
31
  };
36
- serviceCache.set(worktree, services);
37
32
  return services;
38
33
  }