@doingdev/opencode-claude-manager-plugin 0.1.50 → 0.1.52

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.
@@ -151,13 +151,23 @@ export class ClaudeAgentSdkAdapter {
151
151
  if (!input.resumeSessionId) {
152
152
  delete options.resume;
153
153
  }
154
- if (this.approvalManager) {
154
+ const restrictWrites = input.restrictWriteTools === true;
155
+ if (this.approvalManager || restrictWrites) {
155
156
  const manager = this.approvalManager;
156
157
  options.canUseTool = async (toolName, toolInput, opts) => {
157
- return manager.evaluate(toolName, toolInput, {
158
- title: opts.title,
159
- agentID: opts.agentID,
160
- });
158
+ if (restrictWrites && isWriteTool(toolName, toolInput)) {
159
+ return {
160
+ behavior: 'deny',
161
+ message: 'Write operations are restricted in explore mode. Ask the CTO to re-dispatch in implement mode for edits.',
162
+ };
163
+ }
164
+ if (manager) {
165
+ return manager.evaluate(toolName, toolInput, {
166
+ title: opts.title,
167
+ agentID: opts.agentID,
168
+ });
169
+ }
170
+ return { behavior: 'allow' };
161
171
  };
162
172
  }
163
173
  return options;
@@ -437,6 +447,30 @@ function extractText(payload) {
437
447
  }
438
448
  return JSON.stringify(payload);
439
449
  }
450
+ const WRITE_TOOL_NAMES = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
451
+ const BASH_WRITE_PATTERNS = [
452
+ /\b(sed|awk)\b.*-i/,
453
+ /\btee\b/,
454
+ />>/,
455
+ /\becho\b.*>/,
456
+ /\bcat\b.*>/,
457
+ /\bmv\b/,
458
+ /\brm\b/,
459
+ /\bmkdir\b/,
460
+ /\btouch\b/,
461
+ /\bcp\b/,
462
+ /\bgit\s+(add|commit|push|reset|checkout|merge|rebase|stash)\b/,
463
+ ];
464
+ function isWriteTool(toolName, toolInput) {
465
+ if (WRITE_TOOL_NAMES.has(toolName)) {
466
+ return true;
467
+ }
468
+ if (toolName === 'Bash' || toolName === 'bash') {
469
+ const command = typeof toolInput.command === 'string' ? toolInput.command : '';
470
+ return BASH_WRITE_PATTERNS.some((pattern) => pattern.test(command));
471
+ }
472
+ return false;
473
+ }
440
474
  function extractUsageFromResult(message) {
441
475
  if (message.type !== 'result') {
442
476
  return {};
@@ -2,7 +2,7 @@ import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapt
2
2
  import type { ClaudeSessionService } from '../claude/claude-session.service.js';
3
3
  import type { TeamStateStore } from '../state/team-state-store.js';
4
4
  import type { TranscriptStore } from '../state/transcript-store.js';
5
- import type { DiscoveredClaudeFile, EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord } from '../types/contracts.js';
5
+ import type { EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord } from '../types/contracts.js';
6
6
  interface DispatchEngineerInput {
7
7
  teamId: string;
8
8
  cwd: string;
@@ -18,8 +18,7 @@ export declare class TeamOrchestrator {
18
18
  private readonly teamStore;
19
19
  private readonly transcriptStore;
20
20
  private readonly engineerSessionPrompt;
21
- private readonly projectClaudeFiles;
22
- constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, projectClaudeFiles: DiscoveredClaudeFile[]);
21
+ constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string);
23
22
  getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
24
23
  listTeams(cwd: string): Promise<TeamRecord[]>;
25
24
  recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
@@ -52,6 +51,5 @@ export declare class TeamOrchestrator {
52
51
  private normalizeTeamRecord;
53
52
  private buildSessionSystemPrompt;
54
53
  private buildEngineerPrompt;
55
- private mapWorkModeToSessionMode;
56
54
  }
57
55
  export {};
@@ -6,13 +6,11 @@ export class TeamOrchestrator {
6
6
  teamStore;
7
7
  transcriptStore;
8
8
  engineerSessionPrompt;
9
- projectClaudeFiles;
10
- constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, projectClaudeFiles) {
9
+ constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt) {
11
10
  this.sessions = sessions;
12
11
  this.teamStore = teamStore;
13
12
  this.transcriptStore = transcriptStore;
14
13
  this.engineerSessionPrompt = engineerSessionPrompt;
15
- this.projectClaudeFiles = projectClaudeFiles;
16
14
  }
17
15
  async getOrCreateTeam(cwd, teamId) {
18
16
  const existing = await this.teamStore.getTeam(cwd, teamId);
@@ -113,10 +111,11 @@ export class TeamOrchestrator {
113
111
  resumeSessionId: engineerState.claudeSessionId ?? undefined,
114
112
  persistSession: true,
115
113
  includePartialMessages: true,
116
- permissionMode: this.mapWorkModeToSessionMode(input.mode) === 'plan' ? 'plan' : 'acceptEdits',
114
+ permissionMode: 'acceptEdits',
115
+ restrictWriteTools: input.mode === 'explore',
117
116
  model: input.model,
118
117
  effort: input.mode === 'implement' ? 'high' : 'medium',
119
- settingSources: ['user'],
118
+ settingSources: ['user', 'project', 'local'],
120
119
  abortSignal: input.abortSignal,
121
120
  }, input.onEvent);
122
121
  tracker.recordResult({
@@ -225,10 +224,11 @@ export class TeamOrchestrator {
225
224
  systemPrompt: buildSynthesisSystemPrompt(),
226
225
  persistSession: false,
227
226
  includePartialMessages: false,
228
- permissionMode: 'plan',
227
+ permissionMode: 'acceptEdits',
228
+ restrictWriteTools: true,
229
229
  model: input.model,
230
230
  effort: 'high',
231
- settingSources: ['user'],
231
+ settingSources: ['user', 'project', 'local'],
232
232
  abortSignal: input.abortSignal,
233
233
  });
234
234
  const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
@@ -293,17 +293,11 @@ export class TeamOrchestrator {
293
293
  };
294
294
  }
295
295
  buildSessionSystemPrompt(engineer, mode) {
296
- const claudeFileSection = this.projectClaudeFiles.length
297
- ? `\n\nProject Claude Files:\n${this.projectClaudeFiles
298
- .map((file) => `## ${file.relativePath}\n${file.content}`)
299
- .join('\n\n')}`
300
- : '';
301
296
  return [
302
297
  this.engineerSessionPrompt,
303
298
  '',
304
299
  `Assigned engineer: ${engineer}.`,
305
300
  `Current work mode: ${mode}.`,
306
- claudeFileSection,
307
301
  ]
308
302
  .join('\n')
309
303
  .trim();
@@ -311,9 +305,6 @@ export class TeamOrchestrator {
311
305
  buildEngineerPrompt(mode, message) {
312
306
  return `${buildModeInstruction(mode)}\n\n${message}`;
313
307
  }
314
- mapWorkModeToSessionMode(mode) {
315
- return mode === 'explore' ? 'plan' : 'free';
316
- }
317
308
  }
318
309
  function buildModeInstruction(mode) {
319
310
  switch (mode) {
@@ -328,11 +319,13 @@ function buildModeInstruction(mode) {
328
319
  return [
329
320
  'Implementation mode.',
330
321
  'Make the changes, run the most relevant verification (tests, lint, typecheck), and report what changed and what you verified.',
322
+ 'Before reporting done, review your own diff for issues that pass tests but break in production.',
331
323
  ].join(' ');
332
324
  case 'verify':
333
325
  return [
334
326
  'Verification mode.',
335
- 'Run targeted checks in order of relevance.',
327
+ 'Run targeted checks in order of relevance: tests, lint, typecheck, build.',
328
+ 'Check that changed code paths have test coverage.',
336
329
  'Report pass/fail with evidence.',
337
330
  'Escalate failures with exact output.',
338
331
  ].join(' ');
@@ -351,18 +344,19 @@ function appendWrapperHistoryEntries(existing, nextEntries) {
351
344
  }
352
345
  function buildPlanDraftRequest(perspective, request) {
353
346
  const posture = perspective === 'lead'
354
- ? 'You are the lead planner. Propose the most direct workable plan with concrete file paths and clear next steps.'
355
- : 'You are the challenger. Stress-test assumptions, surface missing decisions, and propose a stronger alternative when the lead plan is weak.';
347
+ ? 'You are the lead planner. Propose the most direct workable plan with concrete file paths and clear next steps. Think about failure modes and edge cases — what can break at each boundary?'
348
+ : 'You are the challenger. Stress-test assumptions, surface missing decisions, and propose a stronger alternative when the lead plan is weak. Think about failure modes and edge cases — what can break at each boundary?';
356
349
  return [
357
350
  posture,
358
351
  '',
359
352
  'Return exactly these sections:',
360
353
  '1. Objective',
361
- '2. Proposed approach',
354
+ '2. Proposed approach (include system boundaries and data flow)',
362
355
  '3. Files or systems likely involved',
363
- '4. Risks and open questions',
364
- '5. Verification',
365
- '6. Step-by-step plan',
356
+ '4. Failure modes and edge cases (what happens when things go wrong?)',
357
+ '5. Risks and open questions',
358
+ '6. Verification (how to prove it works, including what tests to add)',
359
+ '7. Step-by-step plan',
366
360
  '',
367
361
  `User request: ${request}`,
368
362
  ].join('\n');
@@ -8,7 +8,6 @@ export declare const ENGINEER_AGENT_IDS: {
8
8
  readonly Alex: "alex";
9
9
  };
10
10
  export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
11
- export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "plan_with_team", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
12
11
  type ToolPermission = 'allow' | 'ask' | 'deny';
13
12
  type AgentPermission = {
14
13
  '*'?: ToolPermission;
@@ -24,7 +24,7 @@ const CTO_ONLY_TOOL_IDS = [
24
24
  'approval_update',
25
25
  ];
26
26
  const ENGINEER_TOOL_IDS = ['claude'];
27
- export const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
27
+ const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
28
28
  const CTO_READONLY_TOOLS = {
29
29
  read: 'allow',
30
30
  grep: 'allow',
@@ -2,14 +2,12 @@ import { tool } from '@opencode-ai/plugin';
2
2
  import { managerPromptRegistry } from '../prompts/registry.js';
3
3
  import { isEngineerName } from '../team/roster.js';
4
4
  import { TeamOrchestrator } from '../manager/team-orchestrator.js';
5
- import { discoverProjectClaudeFiles } from '../util/project-context.js';
6
5
  import { AGENT_CTO, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
7
6
  import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
8
7
  const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
9
8
  const MODE_ENUM = ['explore', 'implement', 'verify'];
10
9
  export const ClaudeManagerPlugin = async ({ worktree }) => {
11
- const claudeFiles = await discoverProjectClaudeFiles(worktree);
12
- const services = getOrCreatePluginServices(worktree, claudeFiles);
10
+ const services = getOrCreatePluginServices(worktree);
13
11
  await services.approvalManager.loadPersistedPolicy();
14
12
  return {
15
13
  config: async (config) => {
@@ -1,17 +1,17 @@
1
1
  import { ClaudeSessionService } from '../claude/claude-session.service.js';
2
2
  import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
3
- import { TeamStateStore } from '../state/team-state-store.js';
4
3
  import { PersistentManager } from '../manager/persistent-manager.js';
5
4
  import { TeamOrchestrator } from '../manager/team-orchestrator.js';
6
- import type { DiscoveredClaudeFile, EngineerName } from '../types/contracts.js';
7
- export interface ClaudeManagerPluginServices {
5
+ import { TeamStateStore } from '../state/team-state-store.js';
6
+ import type { EngineerName } from '../types/contracts.js';
7
+ interface ClaudeManagerPluginServices {
8
8
  manager: PersistentManager;
9
9
  sessions: ClaudeSessionService;
10
10
  approvalManager: ToolApprovalManager;
11
11
  teamStore: TeamStateStore;
12
12
  orchestrator: TeamOrchestrator;
13
13
  }
14
- export declare function getOrCreatePluginServices(worktree: string, projectClaudeFiles?: DiscoveredClaudeFile[]): ClaudeManagerPluginServices;
14
+ export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
15
15
  export declare function clearPluginServices(): void;
16
16
  export declare function setActiveTeamSession(worktree: string, teamId: string): void;
17
17
  export declare function getActiveTeamSession(worktree: string): string | null;
@@ -25,3 +25,4 @@ export declare function getWrapperSessionMapping(worktree: string, wrapperSessio
25
25
  teamId: string;
26
26
  engineer: EngineerName;
27
27
  } | null;
28
+ export {};
@@ -2,16 +2,16 @@ import path from 'node:path';
2
2
  import { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
3
3
  import { ClaudeSessionService } from '../claude/claude-session.service.js';
4
4
  import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
5
- import { TeamStateStore } from '../state/team-state-store.js';
6
- import { TranscriptStore } from '../state/transcript-store.js';
7
5
  import { GitOperations } from '../manager/git-operations.js';
8
6
  import { PersistentManager } from '../manager/persistent-manager.js';
9
- import { managerPromptRegistry } from '../prompts/registry.js';
10
7
  import { TeamOrchestrator } from '../manager/team-orchestrator.js';
8
+ import { managerPromptRegistry } from '../prompts/registry.js';
9
+ import { TeamStateStore } from '../state/team-state-store.js';
10
+ import { TranscriptStore } from '../state/transcript-store.js';
11
11
  const serviceRegistry = new Map();
12
12
  const activeTeamRegistry = new Map();
13
13
  const wrapperSessionRegistry = new Map();
14
- export function getOrCreatePluginServices(worktree, projectClaudeFiles = []) {
14
+ export function getOrCreatePluginServices(worktree) {
15
15
  const existing = serviceRegistry.get(worktree);
16
16
  if (existing) {
17
17
  return existing;
@@ -24,7 +24,7 @@ export function getOrCreatePluginServices(worktree, projectClaudeFiles = []) {
24
24
  const teamStore = new TeamStateStore();
25
25
  const transcriptStore = new TranscriptStore();
26
26
  const manager = new PersistentManager(gitOps, transcriptStore);
27
- const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, projectClaudeFiles);
27
+ const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt);
28
28
  const services = {
29
29
  manager,
30
30
  sessions: sessionService,
@@ -5,10 +5,25 @@ export const managerPromptRegistry = {
5
5
  'Every prompt you send to an engineer costs time and tokens. Make each one count.',
6
6
  '',
7
7
  'Understand first:',
8
- '- Ask questions. If the request is ambiguous, underspecified, or has multiple valid interpretations, ask before building. Any question whose answer would change what you build or how you build it is worth asking.',
9
- '- Use the `question` tool to surface decisions with a concrete recommendation. Prefer one precise question over many vague ones.',
8
+ '- Before asking the user anything, extract what you can from the user message, codebase (read/grep/glob/codesearch), prior engineer results, and `websearch`/`webfetch` when relevant.',
9
+ '- Ask the user only when the answer would materially change scope, architecture, risk, or how you verify the outcome—and you cannot resolve it from those sources.',
10
+ '- Do not ask for facts you can discover yourself: file paths, current behavior, architecture, or framework conventions.',
11
+ '- Before using `question`, silently check: is it in the user message? answerable from code or transcripts? from web? If still blocked, is this a real decision or only uncertainty tolerance?',
10
12
  '- Identify what already exists in the codebase before creating anything new.',
11
13
  '- Think about what could go wrong and address it upfront.',
14
+ '- When a bug is reported, always explore the root cause before implementing a fix. No fix without investigation. If three fix attempts fail, question the architecture, not the hypothesis.',
15
+ '',
16
+ 'Questions (high bar):',
17
+ '- Good questions resolve irreversible choices, product tradeoffs, or ambiguous success criteria that the codebase cannot answer.',
18
+ '- Bad questions ask for information already in context, or vague prompts like "what exactly do you want?" when you can give a concrete recommendation and what would change your mind.',
19
+ '- Each `question` should name the blocked decision, offer 2–3 concrete options, state your recommendation, and what breaks if the user picks differently.',
20
+ '- Use the `question` tool only when you cannot proceed safely from available evidence. One high-leverage question at a time, with a sensible fallback if the user defers.',
21
+ '',
22
+ 'Challenge the framing:',
23
+ '- Not a mandatory opener: if the request is concrete, derive context first; reframe only when it would change what you build.',
24
+ '- Before planning, ask what the user is actually trying to achieve, not just what they asked for.',
25
+ '- If the request sounds like a feature ("add photo upload"), ask what job-to-be-done it serves. The real feature might be larger or different.',
26
+ '- One good reframe question saves more time than ten implementation questions.',
12
27
  '',
13
28
  'Plan and decompose:',
14
29
  '- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
@@ -26,11 +41,19 @@ export const managerPromptRegistry = {
26
41
  '- Give specific, actionable feedback. Not "this could be better" but "this is wrong because X, fix it by doing Y."',
27
42
  '- Trust engineer findings but verify critical claims. Do not re-examine every file they already reviewed.',
28
43
  '- If something fails, figure out what you missed in the assignment, not just what the engineer got wrong.',
44
+ '- After an engineer reports implementation done, review the diff looking for issues that pass tests but break in production: race conditions, N+1 queries, missing error handling, trust boundary violations, stale reads, forgotten enum cases.',
45
+ '- Auto-fix mechanical issues by sending a follow-up to the same engineer. Surface genuinely ambiguous issues to the user.',
46
+ '- Check scope: did the engineer build what was asked — nothing more, nothing less?',
47
+ '',
48
+ 'Verify before declaring done:',
49
+ '- After review passes, dispatch an engineer in verify mode to run the most relevant checks (tests, lint, typecheck, build) for what changed.',
50
+ '- Do not declare a task complete until verification passes. If it fails, fix and re-verify.',
29
51
  '',
30
52
  'Constraints:',
31
53
  '- Do not edit files or run bash directly. Engineers do the hands-on work.',
32
54
  '- Do not read files or grep when an engineer can answer the question faster.',
33
55
  '- Communicate proactively. If the plan changes or you discover something unexpected, tell the user.',
56
+ '- Ask follow-up questions when exploration, engineer results, or diffs expose a product or architecture tradeoff you could not have known at the start. Prefer that timing over opening with speculative clarifiers.',
34
57
  ].join('\n'),
35
58
  engineerAgentPrompt: [
36
59
  "You are a named engineer on the CTO's team.",
@@ -49,6 +72,7 @@ export const managerPromptRegistry = {
49
72
  'Start with the smallest investigation that resolves the key uncertainty, then act.',
50
73
  'Follow repository conventions, AGENTS.md, and any project-level instructions.',
51
74
  'Verify your own work before reporting done. Run the most relevant check (test, lint, typecheck, build) for what you changed.',
75
+ 'Review your own diff before reporting done. Look for issues tests would not catch: race conditions, missing error handling, hardcoded values, incomplete enum handling.',
52
76
  'Report blockers immediately with exact error output. Do not retry silently more than once.',
53
77
  'Do not run git commit, git push, git reset, git checkout, or git stash.',
54
78
  ].join('\n'),
@@ -151,13 +151,23 @@ export class ClaudeAgentSdkAdapter {
151
151
  if (!input.resumeSessionId) {
152
152
  delete options.resume;
153
153
  }
154
- if (this.approvalManager) {
154
+ const restrictWrites = input.restrictWriteTools === true;
155
+ if (this.approvalManager || restrictWrites) {
155
156
  const manager = this.approvalManager;
156
157
  options.canUseTool = async (toolName, toolInput, opts) => {
157
- return manager.evaluate(toolName, toolInput, {
158
- title: opts.title,
159
- agentID: opts.agentID,
160
- });
158
+ if (restrictWrites && isWriteTool(toolName, toolInput)) {
159
+ return {
160
+ behavior: 'deny',
161
+ message: 'Write operations are restricted in explore mode. Ask the CTO to re-dispatch in implement mode for edits.',
162
+ };
163
+ }
164
+ if (manager) {
165
+ return manager.evaluate(toolName, toolInput, {
166
+ title: opts.title,
167
+ agentID: opts.agentID,
168
+ });
169
+ }
170
+ return { behavior: 'allow' };
161
171
  };
162
172
  }
163
173
  return options;
@@ -437,6 +447,30 @@ function extractText(payload) {
437
447
  }
438
448
  return JSON.stringify(payload);
439
449
  }
450
+ const WRITE_TOOL_NAMES = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
451
+ const BASH_WRITE_PATTERNS = [
452
+ /\b(sed|awk)\b.*-i/,
453
+ /\btee\b/,
454
+ />>/,
455
+ /\becho\b.*>/,
456
+ /\bcat\b.*>/,
457
+ /\bmv\b/,
458
+ /\brm\b/,
459
+ /\bmkdir\b/,
460
+ /\btouch\b/,
461
+ /\bcp\b/,
462
+ /\bgit\s+(add|commit|push|reset|checkout|merge|rebase|stash)\b/,
463
+ ];
464
+ function isWriteTool(toolName, toolInput) {
465
+ if (WRITE_TOOL_NAMES.has(toolName)) {
466
+ return true;
467
+ }
468
+ if (toolName === 'Bash' || toolName === 'bash') {
469
+ const command = typeof toolInput.command === 'string' ? toolInput.command : '';
470
+ return BASH_WRITE_PATTERNS.some((pattern) => pattern.test(command));
471
+ }
472
+ return false;
473
+ }
440
474
  function extractUsageFromResult(message) {
441
475
  if (message.type !== 'result') {
442
476
  return {};
@@ -2,7 +2,7 @@ import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapt
2
2
  import type { ClaudeSessionService } from '../claude/claude-session.service.js';
3
3
  import type { TeamStateStore } from '../state/team-state-store.js';
4
4
  import type { TranscriptStore } from '../state/transcript-store.js';
5
- import type { DiscoveredClaudeFile, EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord } from '../types/contracts.js';
5
+ import type { EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord } from '../types/contracts.js';
6
6
  interface DispatchEngineerInput {
7
7
  teamId: string;
8
8
  cwd: string;
@@ -18,8 +18,7 @@ export declare class TeamOrchestrator {
18
18
  private readonly teamStore;
19
19
  private readonly transcriptStore;
20
20
  private readonly engineerSessionPrompt;
21
- private readonly projectClaudeFiles;
22
- constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, projectClaudeFiles: DiscoveredClaudeFile[]);
21
+ constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string);
23
22
  getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
24
23
  listTeams(cwd: string): Promise<TeamRecord[]>;
25
24
  recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
@@ -52,6 +51,5 @@ export declare class TeamOrchestrator {
52
51
  private normalizeTeamRecord;
53
52
  private buildSessionSystemPrompt;
54
53
  private buildEngineerPrompt;
55
- private mapWorkModeToSessionMode;
56
54
  }
57
55
  export {};
@@ -6,13 +6,11 @@ export class TeamOrchestrator {
6
6
  teamStore;
7
7
  transcriptStore;
8
8
  engineerSessionPrompt;
9
- projectClaudeFiles;
10
- constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, projectClaudeFiles) {
9
+ constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt) {
11
10
  this.sessions = sessions;
12
11
  this.teamStore = teamStore;
13
12
  this.transcriptStore = transcriptStore;
14
13
  this.engineerSessionPrompt = engineerSessionPrompt;
15
- this.projectClaudeFiles = projectClaudeFiles;
16
14
  }
17
15
  async getOrCreateTeam(cwd, teamId) {
18
16
  const existing = await this.teamStore.getTeam(cwd, teamId);
@@ -113,10 +111,11 @@ export class TeamOrchestrator {
113
111
  resumeSessionId: engineerState.claudeSessionId ?? undefined,
114
112
  persistSession: true,
115
113
  includePartialMessages: true,
116
- permissionMode: this.mapWorkModeToSessionMode(input.mode) === 'plan' ? 'plan' : 'acceptEdits',
114
+ permissionMode: 'acceptEdits',
115
+ restrictWriteTools: input.mode === 'explore',
117
116
  model: input.model,
118
117
  effort: input.mode === 'implement' ? 'high' : 'medium',
119
- settingSources: ['user'],
118
+ settingSources: ['user', 'project', 'local'],
120
119
  abortSignal: input.abortSignal,
121
120
  }, input.onEvent);
122
121
  tracker.recordResult({
@@ -225,10 +224,11 @@ export class TeamOrchestrator {
225
224
  systemPrompt: buildSynthesisSystemPrompt(),
226
225
  persistSession: false,
227
226
  includePartialMessages: false,
228
- permissionMode: 'plan',
227
+ permissionMode: 'acceptEdits',
228
+ restrictWriteTools: true,
229
229
  model: input.model,
230
230
  effort: 'high',
231
- settingSources: ['user'],
231
+ settingSources: ['user', 'project', 'local'],
232
232
  abortSignal: input.abortSignal,
233
233
  });
234
234
  const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
@@ -293,17 +293,11 @@ export class TeamOrchestrator {
293
293
  };
294
294
  }
295
295
  buildSessionSystemPrompt(engineer, mode) {
296
- const claudeFileSection = this.projectClaudeFiles.length
297
- ? `\n\nProject Claude Files:\n${this.projectClaudeFiles
298
- .map((file) => `## ${file.relativePath}\n${file.content}`)
299
- .join('\n\n')}`
300
- : '';
301
296
  return [
302
297
  this.engineerSessionPrompt,
303
298
  '',
304
299
  `Assigned engineer: ${engineer}.`,
305
300
  `Current work mode: ${mode}.`,
306
- claudeFileSection,
307
301
  ]
308
302
  .join('\n')
309
303
  .trim();
@@ -311,9 +305,6 @@ export class TeamOrchestrator {
311
305
  buildEngineerPrompt(mode, message) {
312
306
  return `${buildModeInstruction(mode)}\n\n${message}`;
313
307
  }
314
- mapWorkModeToSessionMode(mode) {
315
- return mode === 'explore' ? 'plan' : 'free';
316
- }
317
308
  }
318
309
  function buildModeInstruction(mode) {
319
310
  switch (mode) {
@@ -328,11 +319,13 @@ function buildModeInstruction(mode) {
328
319
  return [
329
320
  'Implementation mode.',
330
321
  'Make the changes, run the most relevant verification (tests, lint, typecheck), and report what changed and what you verified.',
322
+ 'Before reporting done, review your own diff for issues that pass tests but break in production.',
331
323
  ].join(' ');
332
324
  case 'verify':
333
325
  return [
334
326
  'Verification mode.',
335
- 'Run targeted checks in order of relevance.',
327
+ 'Run targeted checks in order of relevance: tests, lint, typecheck, build.',
328
+ 'Check that changed code paths have test coverage.',
336
329
  'Report pass/fail with evidence.',
337
330
  'Escalate failures with exact output.',
338
331
  ].join(' ');
@@ -351,18 +344,19 @@ function appendWrapperHistoryEntries(existing, nextEntries) {
351
344
  }
352
345
  function buildPlanDraftRequest(perspective, request) {
353
346
  const posture = perspective === 'lead'
354
- ? 'You are the lead planner. Propose the most direct workable plan with concrete file paths and clear next steps.'
355
- : 'You are the challenger. Stress-test assumptions, surface missing decisions, and propose a stronger alternative when the lead plan is weak.';
347
+ ? 'You are the lead planner. Propose the most direct workable plan with concrete file paths and clear next steps. Think about failure modes and edge cases — what can break at each boundary?'
348
+ : 'You are the challenger. Stress-test assumptions, surface missing decisions, and propose a stronger alternative when the lead plan is weak. Think about failure modes and edge cases — what can break at each boundary?';
356
349
  return [
357
350
  posture,
358
351
  '',
359
352
  'Return exactly these sections:',
360
353
  '1. Objective',
361
- '2. Proposed approach',
354
+ '2. Proposed approach (include system boundaries and data flow)',
362
355
  '3. Files or systems likely involved',
363
- '4. Risks and open questions',
364
- '5. Verification',
365
- '6. Step-by-step plan',
356
+ '4. Failure modes and edge cases (what happens when things go wrong?)',
357
+ '5. Risks and open questions',
358
+ '6. Verification (how to prove it works, including what tests to add)',
359
+ '7. Step-by-step plan',
366
360
  '',
367
361
  `User request: ${request}`,
368
362
  ].join('\n');
@@ -8,7 +8,6 @@ export declare const ENGINEER_AGENT_IDS: {
8
8
  readonly Alex: "alex";
9
9
  };
10
10
  export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
11
- export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "plan_with_team", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
12
11
  type ToolPermission = 'allow' | 'ask' | 'deny';
13
12
  type AgentPermission = {
14
13
  '*'?: ToolPermission;
@@ -24,7 +24,7 @@ const CTO_ONLY_TOOL_IDS = [
24
24
  'approval_update',
25
25
  ];
26
26
  const ENGINEER_TOOL_IDS = ['claude'];
27
- export const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
27
+ const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
28
28
  const CTO_READONLY_TOOLS = {
29
29
  read: 'allow',
30
30
  grep: 'allow',
@@ -2,14 +2,12 @@ import { tool } from '@opencode-ai/plugin';
2
2
  import { managerPromptRegistry } from '../prompts/registry.js';
3
3
  import { isEngineerName } from '../team/roster.js';
4
4
  import { TeamOrchestrator } from '../manager/team-orchestrator.js';
5
- import { discoverProjectClaudeFiles } from '../util/project-context.js';
6
5
  import { AGENT_CTO, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
7
6
  import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
8
7
  const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
9
8
  const MODE_ENUM = ['explore', 'implement', 'verify'];
10
9
  export const ClaudeManagerPlugin = async ({ worktree }) => {
11
- const claudeFiles = await discoverProjectClaudeFiles(worktree);
12
- const services = getOrCreatePluginServices(worktree, claudeFiles);
10
+ const services = getOrCreatePluginServices(worktree);
13
11
  await services.approvalManager.loadPersistedPolicy();
14
12
  return {
15
13
  config: async (config) => {
@@ -1,17 +1,17 @@
1
1
  import { ClaudeSessionService } from '../claude/claude-session.service.js';
2
2
  import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
3
- import { TeamStateStore } from '../state/team-state-store.js';
4
3
  import { PersistentManager } from '../manager/persistent-manager.js';
5
4
  import { TeamOrchestrator } from '../manager/team-orchestrator.js';
6
- import type { DiscoveredClaudeFile, EngineerName } from '../types/contracts.js';
7
- export interface ClaudeManagerPluginServices {
5
+ import { TeamStateStore } from '../state/team-state-store.js';
6
+ import type { EngineerName } from '../types/contracts.js';
7
+ interface ClaudeManagerPluginServices {
8
8
  manager: PersistentManager;
9
9
  sessions: ClaudeSessionService;
10
10
  approvalManager: ToolApprovalManager;
11
11
  teamStore: TeamStateStore;
12
12
  orchestrator: TeamOrchestrator;
13
13
  }
14
- export declare function getOrCreatePluginServices(worktree: string, projectClaudeFiles?: DiscoveredClaudeFile[]): ClaudeManagerPluginServices;
14
+ export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
15
15
  export declare function clearPluginServices(): void;
16
16
  export declare function setActiveTeamSession(worktree: string, teamId: string): void;
17
17
  export declare function getActiveTeamSession(worktree: string): string | null;
@@ -25,3 +25,4 @@ export declare function getWrapperSessionMapping(worktree: string, wrapperSessio
25
25
  teamId: string;
26
26
  engineer: EngineerName;
27
27
  } | null;
28
+ export {};
@@ -2,16 +2,16 @@ import path from 'node:path';
2
2
  import { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
3
3
  import { ClaudeSessionService } from '../claude/claude-session.service.js';
4
4
  import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
5
- import { TeamStateStore } from '../state/team-state-store.js';
6
- import { TranscriptStore } from '../state/transcript-store.js';
7
5
  import { GitOperations } from '../manager/git-operations.js';
8
6
  import { PersistentManager } from '../manager/persistent-manager.js';
9
- import { managerPromptRegistry } from '../prompts/registry.js';
10
7
  import { TeamOrchestrator } from '../manager/team-orchestrator.js';
8
+ import { managerPromptRegistry } from '../prompts/registry.js';
9
+ import { TeamStateStore } from '../state/team-state-store.js';
10
+ import { TranscriptStore } from '../state/transcript-store.js';
11
11
  const serviceRegistry = new Map();
12
12
  const activeTeamRegistry = new Map();
13
13
  const wrapperSessionRegistry = new Map();
14
- export function getOrCreatePluginServices(worktree, projectClaudeFiles = []) {
14
+ export function getOrCreatePluginServices(worktree) {
15
15
  const existing = serviceRegistry.get(worktree);
16
16
  if (existing) {
17
17
  return existing;
@@ -24,7 +24,7 @@ export function getOrCreatePluginServices(worktree, projectClaudeFiles = []) {
24
24
  const teamStore = new TeamStateStore();
25
25
  const transcriptStore = new TranscriptStore();
26
26
  const manager = new PersistentManager(gitOps, transcriptStore);
27
- const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, projectClaudeFiles);
27
+ const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt);
28
28
  const services = {
29
29
  manager,
30
30
  sessions: sessionService,
@@ -5,10 +5,25 @@ export const managerPromptRegistry = {
5
5
  'Every prompt you send to an engineer costs time and tokens. Make each one count.',
6
6
  '',
7
7
  'Understand first:',
8
- '- Ask questions. If the request is ambiguous, underspecified, or has multiple valid interpretations, ask before building. Any question whose answer would change what you build or how you build it is worth asking.',
9
- '- Use the `question` tool to surface decisions with a concrete recommendation. Prefer one precise question over many vague ones.',
8
+ '- Before asking the user anything, extract what you can from the user message, codebase (read/grep/glob/codesearch), prior engineer results, and `websearch`/`webfetch` when relevant.',
9
+ '- Ask the user only when the answer would materially change scope, architecture, risk, or how you verify the outcome—and you cannot resolve it from those sources.',
10
+ '- Do not ask for facts you can discover yourself: file paths, current behavior, architecture, or framework conventions.',
11
+ '- Before using `question`, silently check: is it in the user message? answerable from code or transcripts? from web? If still blocked, is this a real decision or only uncertainty tolerance?',
10
12
  '- Identify what already exists in the codebase before creating anything new.',
11
13
  '- Think about what could go wrong and address it upfront.',
14
+ '- When a bug is reported, always explore the root cause before implementing a fix. No fix without investigation. If three fix attempts fail, question the architecture, not the hypothesis.',
15
+ '',
16
+ 'Questions (high bar):',
17
+ '- Good questions resolve irreversible choices, product tradeoffs, or ambiguous success criteria that the codebase cannot answer.',
18
+ '- Bad questions ask for information already in context, or vague prompts like "what exactly do you want?" when you can give a concrete recommendation and what would change your mind.',
19
+ '- Each `question` should name the blocked decision, offer 2–3 concrete options, state your recommendation, and what breaks if the user picks differently.',
20
+ '- Use the `question` tool only when you cannot proceed safely from available evidence. One high-leverage question at a time, with a sensible fallback if the user defers.',
21
+ '',
22
+ 'Challenge the framing:',
23
+ '- Not a mandatory opener: if the request is concrete, derive context first; reframe only when it would change what you build.',
24
+ '- Before planning, ask what the user is actually trying to achieve, not just what they asked for.',
25
+ '- If the request sounds like a feature ("add photo upload"), ask what job-to-be-done it serves. The real feature might be larger or different.',
26
+ '- One good reframe question saves more time than ten implementation questions.',
12
27
  '',
13
28
  'Plan and decompose:',
14
29
  '- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
@@ -26,11 +41,19 @@ export const managerPromptRegistry = {
26
41
  '- Give specific, actionable feedback. Not "this could be better" but "this is wrong because X, fix it by doing Y."',
27
42
  '- Trust engineer findings but verify critical claims. Do not re-examine every file they already reviewed.',
28
43
  '- If something fails, figure out what you missed in the assignment, not just what the engineer got wrong.',
44
+ '- After an engineer reports implementation done, review the diff looking for issues that pass tests but break in production: race conditions, N+1 queries, missing error handling, trust boundary violations, stale reads, forgotten enum cases.',
45
+ '- Auto-fix mechanical issues by sending a follow-up to the same engineer. Surface genuinely ambiguous issues to the user.',
46
+ '- Check scope: did the engineer build what was asked — nothing more, nothing less?',
47
+ '',
48
+ 'Verify before declaring done:',
49
+ '- After review passes, dispatch an engineer in verify mode to run the most relevant checks (tests, lint, typecheck, build) for what changed.',
50
+ '- Do not declare a task complete until verification passes. If it fails, fix and re-verify.',
29
51
  '',
30
52
  'Constraints:',
31
53
  '- Do not edit files or run bash directly. Engineers do the hands-on work.',
32
54
  '- Do not read files or grep when an engineer can answer the question faster.',
33
55
  '- Communicate proactively. If the plan changes or you discover something unexpected, tell the user.',
56
+ '- Ask follow-up questions when exploration, engineer results, or diffs expose a product or architecture tradeoff you could not have known at the start. Prefer that timing over opening with speculative clarifiers.',
34
57
  ].join('\n'),
35
58
  engineerAgentPrompt: [
36
59
  "You are a named engineer on the CTO's team.",
@@ -49,6 +72,7 @@ export const managerPromptRegistry = {
49
72
  'Start with the smallest investigation that resolves the key uncertainty, then act.',
50
73
  'Follow repository conventions, AGENTS.md, and any project-level instructions.',
51
74
  'Verify your own work before reporting done. Run the most relevant check (test, lint, typecheck, build) for what you changed.',
75
+ 'Review your own diff before reporting done. Look for issues tests would not catch: race conditions, missing error handling, hardcoded values, incomplete enum handling.',
52
76
  'Report blockers immediately with exact error output. Do not retry silently more than once.',
53
77
  'Do not run git commit, git push, git reset, git checkout, or git stash.',
54
78
  ].join('\n'),
@@ -18,14 +18,14 @@ export interface WrapperHistoryEntry {
18
18
  mode?: EngineerWorkMode;
19
19
  text: string;
20
20
  }
21
- export interface ClaudeCommandMetadata {
21
+ interface ClaudeCommandMetadata {
22
22
  name: string;
23
23
  description: string;
24
24
  argumentHint?: string;
25
25
  source: 'sdk' | 'skill' | 'command';
26
26
  path?: string;
27
27
  }
28
- export interface ClaudeAgentMetadata {
28
+ interface ClaudeAgentMetadata {
29
29
  name: string;
30
30
  description: string;
31
31
  model?: string;
@@ -48,6 +48,12 @@ export interface RunClaudeSessionInput {
48
48
  effort?: 'low' | 'medium' | 'high' | 'max';
49
49
  mode?: SessionMode;
50
50
  permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk';
51
+ /**
52
+ * When true, the canUseTool callback denies write tools (Edit, Write, MultiEdit,
53
+ * NotebookEdit) and destructive bash commands. Used instead of SDK plan mode so
54
+ * the agent can still exit plan mode if needed.
55
+ */
56
+ restrictWriteTools?: boolean;
51
57
  /** Merged with `Skill` by the SDK adapter unless `Skill` appears in `disallowedTools`. */
52
58
  allowedTools?: string[];
53
59
  disallowedTools?: string[];
@@ -168,12 +174,6 @@ export interface GitOperationResult {
168
174
  output: string;
169
175
  error?: string;
170
176
  }
171
- export interface DiscoveredClaudeFile {
172
- /** Relative path from the project root (forward slashes, deterministic order). */
173
- relativePath: string;
174
- /** Trimmed UTF-8 text content. */
175
- content: string;
176
- }
177
177
  export interface ToolApprovalRule {
178
178
  id: string;
179
179
  description?: string;
@@ -200,3 +200,4 @@ export interface ToolApprovalDecision {
200
200
  denyMessage?: string;
201
201
  agentId?: string;
202
202
  }
203
+ export {};
@@ -418,6 +418,109 @@ describe('ClaudeAgentSdkAdapter', () => {
418
418
  expect(result.outputTokens).toBe(200);
419
419
  expect(result.contextWindowSize).toBe(180_000);
420
420
  });
421
+ it('denies write tools when restrictWriteTools is true', async () => {
422
+ let capturedCanUseTool;
423
+ const adapter = new ClaudeAgentSdkAdapter({
424
+ query: (params) => {
425
+ capturedCanUseTool = params.options?.canUseTool;
426
+ return createFakeQuery([
427
+ {
428
+ type: 'result',
429
+ subtype: 'success',
430
+ session_id: 'ses_rw',
431
+ is_error: false,
432
+ result: 'ok',
433
+ num_turns: 1,
434
+ total_cost_usd: 0,
435
+ },
436
+ ]);
437
+ },
438
+ listSessions: async () => [],
439
+ getSessionMessages: async () => [],
440
+ });
441
+ await adapter.runSession({
442
+ cwd: '/tmp/project',
443
+ prompt: 'Investigate',
444
+ restrictWriteTools: true,
445
+ });
446
+ expect(capturedCanUseTool).toBeDefined();
447
+ const editResult = await capturedCanUseTool('Edit', { file_path: 'x.ts' }, {});
448
+ expect(editResult.behavior).toBe('deny');
449
+ const writeResult = await capturedCanUseTool('Write', { file_path: 'y.ts' }, {});
450
+ expect(writeResult.behavior).toBe('deny');
451
+ const multiEditResult = await capturedCanUseTool('MultiEdit', { file_path: 'z.ts' }, {});
452
+ expect(multiEditResult.behavior).toBe('deny');
453
+ const readResult = await capturedCanUseTool('Read', { file_path: 'a.ts' }, {});
454
+ expect(readResult.behavior).toBe('allow');
455
+ const grepResult = await capturedCanUseTool('Grep', { pattern: 'foo', path: '.' }, {});
456
+ expect(grepResult.behavior).toBe('allow');
457
+ });
458
+ it('denies destructive bash commands when restrictWriteTools is true', async () => {
459
+ let capturedCanUseTool;
460
+ const adapter = new ClaudeAgentSdkAdapter({
461
+ query: (params) => {
462
+ capturedCanUseTool = params.options?.canUseTool;
463
+ return createFakeQuery([
464
+ {
465
+ type: 'result',
466
+ subtype: 'success',
467
+ session_id: 'ses_bash',
468
+ is_error: false,
469
+ result: 'ok',
470
+ num_turns: 1,
471
+ total_cost_usd: 0,
472
+ },
473
+ ]);
474
+ },
475
+ listSessions: async () => [],
476
+ getSessionMessages: async () => [],
477
+ });
478
+ await adapter.runSession({
479
+ cwd: '/tmp/project',
480
+ prompt: 'Check',
481
+ restrictWriteTools: true,
482
+ });
483
+ expect(capturedCanUseTool).toBeDefined();
484
+ const sedResult = await capturedCanUseTool('Bash', { command: "sed -i 's/old/new/' file.ts" }, {});
485
+ expect(sedResult.behavior).toBe('deny');
486
+ const echoResult = await capturedCanUseTool('Bash', { command: 'echo "data" > out.txt' }, {});
487
+ expect(echoResult.behavior).toBe('deny');
488
+ const gitCommitResult = await capturedCanUseTool('Bash', { command: 'git commit -m "fix"' }, {});
489
+ expect(gitCommitResult.behavior).toBe('deny');
490
+ const lsResult = await capturedCanUseTool('Bash', { command: 'ls -la src/' }, {});
491
+ expect(lsResult.behavior).toBe('allow');
492
+ const catResult = await capturedCanUseTool('Bash', { command: 'cat src/index.ts' }, {});
493
+ expect(catResult.behavior).toBe('allow');
494
+ const gitLogResult = await capturedCanUseTool('Bash', { command: 'git log --oneline -10' }, {});
495
+ expect(gitLogResult.behavior).toBe('allow');
496
+ });
497
+ it('allows write tools when restrictWriteTools is false', async () => {
498
+ let capturedCanUseTool;
499
+ const adapter = new ClaudeAgentSdkAdapter({
500
+ query: (params) => {
501
+ capturedCanUseTool = params.options?.canUseTool;
502
+ return createFakeQuery([
503
+ {
504
+ type: 'result',
505
+ subtype: 'success',
506
+ session_id: 'ses_free',
507
+ is_error: false,
508
+ result: 'ok',
509
+ num_turns: 1,
510
+ total_cost_usd: 0,
511
+ },
512
+ ]);
513
+ },
514
+ listSessions: async () => [],
515
+ getSessionMessages: async () => [],
516
+ });
517
+ await adapter.runSession({
518
+ cwd: '/tmp/project',
519
+ prompt: 'Implement',
520
+ restrictWriteTools: false,
521
+ });
522
+ expect(capturedCanUseTool).toBeUndefined();
523
+ });
421
524
  it('passes through explicit permissionMode', async () => {
422
525
  let capturedPermissionMode;
423
526
  const adapter = new ClaudeAgentSdkAdapter({
@@ -180,7 +180,7 @@ describe('second invocation continuity', () => {
180
180
  // ── Phase 1: first task via orchestrator (no real SDK needed) ──────────
181
181
  const store = new TeamStateStore();
182
182
  await store.setActiveTeam(tempRoot, 'cto-1');
183
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', []);
183
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt');
184
184
  await orchestrator.recordWrapperSession(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1');
185
185
  await orchestrator.recordWrapperExchange(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1', 'explore', 'Investigate the auth flow', 'Found two race conditions in the token refresh path.');
186
186
  // ── Phase 2: process restart ───────────────────────────────────────────
@@ -206,7 +206,7 @@ describe('second invocation continuity', () => {
206
206
  // ── Phase 1: pre-seed Tom with a claudeSessionId ───────────────────────
207
207
  const store = new TeamStateStore();
208
208
  await store.setActiveTeam(tempRoot, 'cto-1');
209
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', []);
209
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt');
210
210
  await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
211
211
  await store.updateTeam(tempRoot, 'cto-1', (team) => ({
212
212
  ...team,
@@ -35,7 +35,7 @@ describe('TeamOrchestrator', () => {
35
35
  outputTokens: 300,
36
36
  contextWindowSize: 200_000,
37
37
  });
38
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', []);
38
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
39
39
  const first = await orchestrator.dispatchEngineer({
40
40
  teamId: 'team-1',
41
41
  cwd: tempRoot,
@@ -55,12 +55,14 @@ describe('TeamOrchestrator', () => {
55
55
  expect(runTask.mock.calls[0]?.[0]).toMatchObject({
56
56
  systemPrompt: expect.stringContaining('Assigned engineer: Tom.'),
57
57
  resumeSessionId: undefined,
58
- permissionMode: 'plan',
58
+ permissionMode: 'acceptEdits',
59
+ restrictWriteTools: true,
59
60
  });
60
61
  expect(runTask.mock.calls[1]?.[0]).toMatchObject({
61
62
  systemPrompt: undefined,
62
63
  resumeSessionId: 'ses_tom',
63
64
  permissionMode: 'acceptEdits',
65
+ restrictWriteTools: false,
64
66
  });
65
67
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
66
68
  expect(team.engineers.find((engineer) => engineer.name === 'Tom')).toMatchObject({
@@ -72,7 +74,7 @@ describe('TeamOrchestrator', () => {
72
74
  it('rejects work when the same engineer is already busy', async () => {
73
75
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
74
76
  const store = new TeamStateStore('.state');
75
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', []);
77
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
76
78
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
77
79
  await store.saveTeam({
78
80
  ...team,
@@ -110,7 +112,7 @@ describe('TeamOrchestrator', () => {
110
112
  events: [],
111
113
  finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
112
114
  });
113
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', []);
115
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
114
116
  const result = await orchestrator.planWithTeam({
115
117
  teamId: 'team-1',
116
118
  cwd: tempRoot,
@@ -126,7 +128,7 @@ describe('TeamOrchestrator', () => {
126
128
  });
127
129
  it('persists wrapper session memory for an engineer', async () => {
128
130
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
129
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', []);
131
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
130
132
  await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
131
133
  await orchestrator.recordWrapperExchange(tempRoot, 'team-1', 'Tom', 'wrapper-tom', 'explore', 'Investigate the auth flow and compare approaches', 'The auth flow uses one shared validator and the cookie refresh path is the main risk.');
132
134
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
@@ -18,14 +18,14 @@ export interface WrapperHistoryEntry {
18
18
  mode?: EngineerWorkMode;
19
19
  text: string;
20
20
  }
21
- export interface ClaudeCommandMetadata {
21
+ interface ClaudeCommandMetadata {
22
22
  name: string;
23
23
  description: string;
24
24
  argumentHint?: string;
25
25
  source: 'sdk' | 'skill' | 'command';
26
26
  path?: string;
27
27
  }
28
- export interface ClaudeAgentMetadata {
28
+ interface ClaudeAgentMetadata {
29
29
  name: string;
30
30
  description: string;
31
31
  model?: string;
@@ -48,6 +48,12 @@ export interface RunClaudeSessionInput {
48
48
  effort?: 'low' | 'medium' | 'high' | 'max';
49
49
  mode?: SessionMode;
50
50
  permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk';
51
+ /**
52
+ * When true, the canUseTool callback denies write tools (Edit, Write, MultiEdit,
53
+ * NotebookEdit) and destructive bash commands. Used instead of SDK plan mode so
54
+ * the agent can still exit plan mode if needed.
55
+ */
56
+ restrictWriteTools?: boolean;
51
57
  /** Merged with `Skill` by the SDK adapter unless `Skill` appears in `disallowedTools`. */
52
58
  allowedTools?: string[];
53
59
  disallowedTools?: string[];
@@ -168,12 +174,6 @@ export interface GitOperationResult {
168
174
  output: string;
169
175
  error?: string;
170
176
  }
171
- export interface DiscoveredClaudeFile {
172
- /** Relative path from the project root (forward slashes, deterministic order). */
173
- relativePath: string;
174
- /** Trimmed UTF-8 text content. */
175
- content: string;
176
- }
177
177
  export interface ToolApprovalRule {
178
178
  id: string;
179
179
  description?: string;
@@ -200,3 +200,4 @@ export interface ToolApprovalDecision {
200
200
  denyMessage?: string;
201
201
  agentId?: string;
202
202
  }
203
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",