@doingdev/opencode-claude-manager-plugin 0.1.50 → 0.1.51
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/dist/claude/claude-agent-sdk-adapter.js +39 -5
- package/dist/manager/team-orchestrator.d.ts +2 -4
- package/dist/manager/team-orchestrator.js +17 -23
- package/dist/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/plugin/agent-hierarchy.js +1 -1
- package/dist/plugin/claude-manager.plugin.js +1 -3
- package/dist/plugin/service-factory.d.ts +5 -4
- package/dist/plugin/service-factory.js +5 -5
- package/dist/prompts/registry.js +14 -0
- package/dist/src/claude/claude-agent-sdk-adapter.js +39 -5
- package/dist/src/manager/team-orchestrator.d.ts +2 -4
- package/dist/src/manager/team-orchestrator.js +17 -23
- package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/src/plugin/agent-hierarchy.js +1 -1
- package/dist/src/plugin/claude-manager.plugin.js +1 -3
- package/dist/src/plugin/service-factory.d.ts +5 -4
- package/dist/src/plugin/service-factory.js +5 -5
- package/dist/src/prompts/registry.js +14 -0
- package/dist/src/types/contracts.d.ts +9 -8
- package/dist/test/claude-agent-sdk-adapter.test.js +103 -0
- package/dist/test/report-claude-event.test.js +2 -2
- package/dist/test/team-orchestrator.test.js +7 -5
- package/dist/types/contracts.d.ts +9 -8
- package/package.json +1 -1
|
@@ -151,13 +151,23 @@ export class ClaudeAgentSdkAdapter {
|
|
|
151
151
|
if (!input.resumeSessionId) {
|
|
152
152
|
delete options.resume;
|
|
153
153
|
}
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: '
|
|
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.
|
|
364
|
-
'5.
|
|
365
|
-
'6.
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
-
|
|
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
|
|
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
|
|
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
|
|
27
|
+
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt);
|
|
28
28
|
const services = {
|
|
29
29
|
manager,
|
|
30
30
|
sessions: sessionService,
|
package/dist/prompts/registry.js
CHANGED
|
@@ -9,6 +9,12 @@ export const managerPromptRegistry = {
|
|
|
9
9
|
'- Use the `question` tool to surface decisions with a concrete recommendation. Prefer one precise question over many vague ones.',
|
|
10
10
|
'- Identify what already exists in the codebase before creating anything new.',
|
|
11
11
|
'- Think about what could go wrong and address it upfront.',
|
|
12
|
+
'- 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.',
|
|
13
|
+
'',
|
|
14
|
+
'Challenge the framing:',
|
|
15
|
+
'- Before planning, ask what the user is actually trying to achieve, not just what they asked for.',
|
|
16
|
+
'- 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.',
|
|
17
|
+
'- One good reframe question saves more time than ten implementation questions.',
|
|
12
18
|
'',
|
|
13
19
|
'Plan and decompose:',
|
|
14
20
|
'- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
|
|
@@ -26,6 +32,13 @@ export const managerPromptRegistry = {
|
|
|
26
32
|
'- Give specific, actionable feedback. Not "this could be better" but "this is wrong because X, fix it by doing Y."',
|
|
27
33
|
'- Trust engineer findings but verify critical claims. Do not re-examine every file they already reviewed.',
|
|
28
34
|
'- If something fails, figure out what you missed in the assignment, not just what the engineer got wrong.',
|
|
35
|
+
'- 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.',
|
|
36
|
+
'- Auto-fix mechanical issues by sending a follow-up to the same engineer. Surface genuinely ambiguous issues to the user.',
|
|
37
|
+
'- Check scope: did the engineer build what was asked — nothing more, nothing less?',
|
|
38
|
+
'',
|
|
39
|
+
'Verify before declaring done:',
|
|
40
|
+
'- After review passes, dispatch an engineer in verify mode to run the most relevant checks (tests, lint, typecheck, build) for what changed.',
|
|
41
|
+
'- Do not declare a task complete until verification passes. If it fails, fix and re-verify.',
|
|
29
42
|
'',
|
|
30
43
|
'Constraints:',
|
|
31
44
|
'- Do not edit files or run bash directly. Engineers do the hands-on work.',
|
|
@@ -49,6 +62,7 @@ export const managerPromptRegistry = {
|
|
|
49
62
|
'Start with the smallest investigation that resolves the key uncertainty, then act.',
|
|
50
63
|
'Follow repository conventions, AGENTS.md, and any project-level instructions.',
|
|
51
64
|
'Verify your own work before reporting done. Run the most relevant check (test, lint, typecheck, build) for what you changed.',
|
|
65
|
+
'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
66
|
'Report blockers immediately with exact error output. Do not retry silently more than once.',
|
|
53
67
|
'Do not run git commit, git push, git reset, git checkout, or git stash.',
|
|
54
68
|
].join('\n'),
|
|
@@ -151,13 +151,23 @@ export class ClaudeAgentSdkAdapter {
|
|
|
151
151
|
if (!input.resumeSessionId) {
|
|
152
152
|
delete options.resume;
|
|
153
153
|
}
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: '
|
|
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.
|
|
364
|
-
'5.
|
|
365
|
-
'6.
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
-
|
|
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
|
|
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
|
|
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
|
|
27
|
+
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt);
|
|
28
28
|
const services = {
|
|
29
29
|
manager,
|
|
30
30
|
sessions: sessionService,
|
|
@@ -9,6 +9,12 @@ export const managerPromptRegistry = {
|
|
|
9
9
|
'- Use the `question` tool to surface decisions with a concrete recommendation. Prefer one precise question over many vague ones.',
|
|
10
10
|
'- Identify what already exists in the codebase before creating anything new.',
|
|
11
11
|
'- Think about what could go wrong and address it upfront.',
|
|
12
|
+
'- 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.',
|
|
13
|
+
'',
|
|
14
|
+
'Challenge the framing:',
|
|
15
|
+
'- Before planning, ask what the user is actually trying to achieve, not just what they asked for.',
|
|
16
|
+
'- 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.',
|
|
17
|
+
'- One good reframe question saves more time than ten implementation questions.',
|
|
12
18
|
'',
|
|
13
19
|
'Plan and decompose:',
|
|
14
20
|
'- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
|
|
@@ -26,6 +32,13 @@ export const managerPromptRegistry = {
|
|
|
26
32
|
'- Give specific, actionable feedback. Not "this could be better" but "this is wrong because X, fix it by doing Y."',
|
|
27
33
|
'- Trust engineer findings but verify critical claims. Do not re-examine every file they already reviewed.',
|
|
28
34
|
'- If something fails, figure out what you missed in the assignment, not just what the engineer got wrong.',
|
|
35
|
+
'- 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.',
|
|
36
|
+
'- Auto-fix mechanical issues by sending a follow-up to the same engineer. Surface genuinely ambiguous issues to the user.',
|
|
37
|
+
'- Check scope: did the engineer build what was asked — nothing more, nothing less?',
|
|
38
|
+
'',
|
|
39
|
+
'Verify before declaring done:',
|
|
40
|
+
'- After review passes, dispatch an engineer in verify mode to run the most relevant checks (tests, lint, typecheck, build) for what changed.',
|
|
41
|
+
'- Do not declare a task complete until verification passes. If it fails, fix and re-verify.',
|
|
29
42
|
'',
|
|
30
43
|
'Constraints:',
|
|
31
44
|
'- Do not edit files or run bash directly. Engineers do the hands-on work.',
|
|
@@ -49,6 +62,7 @@ export const managerPromptRegistry = {
|
|
|
49
62
|
'Start with the smallest investigation that resolves the key uncertainty, then act.',
|
|
50
63
|
'Follow repository conventions, AGENTS.md, and any project-level instructions.',
|
|
51
64
|
'Verify your own work before reporting done. Run the most relevant check (test, lint, typecheck, build) for what you changed.',
|
|
65
|
+
'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
66
|
'Report blockers immediately with exact error output. Do not retry silently more than once.',
|
|
53
67
|
'Do not run git commit, git push, git reset, git checkout, or git stash.',
|
|
54
68
|
].join('\n'),
|
|
@@ -18,14 +18,14 @@ export interface WrapperHistoryEntry {
|
|
|
18
18
|
mode?: EngineerWorkMode;
|
|
19
19
|
text: string;
|
|
20
20
|
}
|
|
21
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|