@doingdev/opencode-claude-manager-plugin 0.1.60 → 0.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/claude/claude-agent-sdk-adapter.d.ts +3 -1
- package/dist/claude/claude-agent-sdk-adapter.js +57 -6
- package/dist/manager/team-orchestrator.js +11 -4
- package/dist/plugin/agents/browser-qa.js +4 -0
- package/dist/plugin/agents/common.d.ts +2 -2
- package/dist/plugin/agents/common.js +0 -2
- package/dist/plugin/agents/team-planner.js +1 -1
- package/dist/plugin/claude-manager.plugin.js +14 -43
- package/dist/plugin/service-factory.d.ts +1 -0
- package/dist/plugin/service-factory.js +3 -1
- package/dist/prompts/registry.js +30 -30
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +3 -1
- package/dist/src/claude/claude-agent-sdk-adapter.js +57 -6
- package/dist/src/manager/team-orchestrator.js +11 -4
- package/dist/src/plugin/agents/browser-qa.js +4 -0
- package/dist/src/plugin/agents/common.d.ts +2 -2
- package/dist/src/plugin/agents/common.js +0 -2
- package/dist/src/plugin/agents/team-planner.js +1 -1
- package/dist/src/plugin/claude-manager.plugin.js +14 -43
- package/dist/src/plugin/service-factory.d.ts +1 -0
- package/dist/src/plugin/service-factory.js +3 -1
- package/dist/src/prompts/registry.js +30 -30
- package/dist/src/types/contracts.d.ts +10 -3
- package/dist/src/util/fs-helpers.d.ts +6 -0
- package/dist/src/util/fs-helpers.js +11 -0
- package/dist/test/claude-agent-sdk-adapter.test.js +157 -1
- package/dist/test/claude-manager.plugin.test.js +23 -150
- package/dist/test/fs-helpers.test.d.ts +1 -0
- package/dist/test/fs-helpers.test.js +56 -0
- package/dist/test/prompt-registry.test.js +24 -28
- package/dist/test/team-orchestrator.test.js +71 -0
- package/dist/types/contracts.d.ts +10 -3
- package/dist/util/fs-helpers.d.ts +6 -0
- package/dist/util/fs-helpers.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,7 @@ The plugin registers a CTO + named engineer team through the OpenCode plugin `co
|
|
|
89
89
|
|
|
90
90
|
- **`cto`** (primary agent) — owns the outcome, finds missing requirements, spawns named engineers with the Task tool, compares plans, reviews diffs, and manages git.
|
|
91
91
|
- **`tom`**, **`john`**, **`maya`**, **`sara`**, **`alex`** (subagents) — thin named engineer wrappers. Each uses the `claude` tool and keeps one persistent Claude Code session.
|
|
92
|
+
- **`team-planner`** (subagent) — thin planning wrapper that runs `plan_with_team` so the UI shows live planning activity.
|
|
92
93
|
- **Claude Code sessions** — the underlying execution layer. One session per engineer inside the active team.
|
|
93
94
|
|
|
94
95
|
These are added to OpenCode config at runtime by the plugin, so they do not require separate manual `opencode.json` entries.
|
|
@@ -98,7 +99,7 @@ These are added to OpenCode config at runtime by the plugin, so they do not requ
|
|
|
98
99
|
Typical flow inside OpenCode:
|
|
99
100
|
|
|
100
101
|
1. Ask the `cto` agent for the work.
|
|
101
|
-
2. Let `cto` spawn one or more named engineers with the Task tool.
|
|
102
|
+
2. Let `cto` investigate lightly, then spawn one or more named engineers with the Task tool.
|
|
102
103
|
3. Review changes with `git_diff`, then commit or reset.
|
|
103
104
|
4. Inspect saved Claude history with `list_transcripts` or saved team state with `list_history` / `team_status`.
|
|
104
105
|
|
|
@@ -108,10 +109,10 @@ Example tasks:
|
|
|
108
109
|
Ask CTO to send Tom to implement the new validation logic in src/auth.ts, then review with git_diff.
|
|
109
110
|
```
|
|
110
111
|
|
|
111
|
-
For a larger feature where you want
|
|
112
|
+
For a larger feature where you want investigation first and then a stronger combined plan:
|
|
112
113
|
|
|
113
114
|
```text
|
|
114
|
-
Ask CTO to
|
|
115
|
+
Ask CTO to inspect the billing feature scope, then use team-planner to run two independent investigations and synthesize the best implementation plan.
|
|
115
116
|
```
|
|
116
117
|
|
|
117
118
|
## Local Development
|
|
@@ -17,10 +17,12 @@ interface ClaudeAgentSdkFacade {
|
|
|
17
17
|
export declare class ClaudeAgentSdkAdapter {
|
|
18
18
|
private readonly sdkFacade;
|
|
19
19
|
private readonly approvalManager?;
|
|
20
|
-
|
|
20
|
+
private readonly debugLogPath?;
|
|
21
|
+
constructor(sdkFacade?: ClaudeAgentSdkFacade, approvalManager?: ToolApprovalManager | undefined, debugLogPath?: string | undefined);
|
|
21
22
|
runSession(input: RunClaudeSessionInput, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
22
23
|
listSavedSessions(cwd?: string): Promise<ClaudeSessionSummary[]>;
|
|
23
24
|
getTranscript(sessionId: string, cwd?: string): Promise<ClaudeSessionTranscriptMessage[]>;
|
|
24
25
|
private buildOptions;
|
|
26
|
+
private logDeniedTool;
|
|
25
27
|
}
|
|
26
28
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSessionMessages, listSessions, query, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { appendDebugLog } from '../util/fs-helpers.js';
|
|
2
3
|
import { appendTranscriptEvents } from '../util/transcript-append.js';
|
|
3
4
|
const defaultFacade = {
|
|
4
5
|
query,
|
|
@@ -24,9 +25,11 @@ function mergeSkillIntoAllowedTools(allowedTools, disallowedTools) {
|
|
|
24
25
|
export class ClaudeAgentSdkAdapter {
|
|
25
26
|
sdkFacade;
|
|
26
27
|
approvalManager;
|
|
27
|
-
|
|
28
|
+
debugLogPath;
|
|
29
|
+
constructor(sdkFacade = defaultFacade, approvalManager, debugLogPath) {
|
|
28
30
|
this.sdkFacade = sdkFacade;
|
|
29
31
|
this.approvalManager = approvalManager;
|
|
32
|
+
this.debugLogPath = debugLogPath;
|
|
30
33
|
}
|
|
31
34
|
async runSession(input, onEvent) {
|
|
32
35
|
const options = this.buildOptions(input);
|
|
@@ -156,22 +159,42 @@ export class ClaudeAgentSdkAdapter {
|
|
|
156
159
|
const manager = this.approvalManager;
|
|
157
160
|
options.canUseTool = async (toolName, toolInput, opts) => {
|
|
158
161
|
if (restrictWrites && isWriteTool(toolName, toolInput)) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
162
|
+
const message = 'Write operations are restricted in explore mode. Ask the CTO to re-dispatch in implement mode for edits.';
|
|
163
|
+
await this.logDeniedTool(toolName, toolInput, message, 'restrictWriteTools');
|
|
164
|
+
return { behavior: 'deny', message };
|
|
163
165
|
}
|
|
164
166
|
if (manager) {
|
|
165
|
-
|
|
167
|
+
const decision = manager.evaluate(toolName, toolInput, {
|
|
166
168
|
title: opts.title,
|
|
167
169
|
agentID: opts.agentID,
|
|
168
170
|
});
|
|
171
|
+
if (decision.behavior === 'deny') {
|
|
172
|
+
await this.logDeniedTool(toolName, toolInput, decision.message, 'approvalPolicy');
|
|
173
|
+
}
|
|
174
|
+
return decision;
|
|
169
175
|
}
|
|
170
176
|
return { behavior: 'allow' };
|
|
171
177
|
};
|
|
172
178
|
}
|
|
173
179
|
return options;
|
|
174
180
|
}
|
|
181
|
+
async logDeniedTool(toolName, toolInput, message, reason) {
|
|
182
|
+
if (!this.debugLogPath) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
await appendDebugLog(this.debugLogPath, {
|
|
187
|
+
type: 'tool_denied',
|
|
188
|
+
toolName,
|
|
189
|
+
inputPreview: sanitizeInputForLog(toolName, toolInput),
|
|
190
|
+
reason,
|
|
191
|
+
message,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Log write failures must not block tool evaluation.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
175
198
|
}
|
|
176
199
|
function normalizeSdkMessages(message, includePartials) {
|
|
177
200
|
const sessionId = 'session_id' in message ? message.session_id : undefined;
|
|
@@ -471,6 +494,34 @@ function isWriteTool(toolName, toolInput) {
|
|
|
471
494
|
}
|
|
472
495
|
return false;
|
|
473
496
|
}
|
|
497
|
+
const FILE_TOOL_NAMES = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit', 'Read']);
|
|
498
|
+
/**
|
|
499
|
+
* Returns a compact, safe preview of tool input for debug logging.
|
|
500
|
+
* Intentionally omits file content and other large/sensitive payloads.
|
|
501
|
+
*/
|
|
502
|
+
function sanitizeInputForLog(toolName, toolInput) {
|
|
503
|
+
if (FILE_TOOL_NAMES.has(toolName)) {
|
|
504
|
+
const filePath = typeof toolInput.file_path === 'string' ? toolInput.file_path : '(unknown)';
|
|
505
|
+
return `file_path=${filePath}`;
|
|
506
|
+
}
|
|
507
|
+
if (toolName === 'Bash' || toolName === 'bash') {
|
|
508
|
+
const command = typeof toolInput.command === 'string' ? toolInput.command : '(unknown)';
|
|
509
|
+
return `command=${command.slice(0, 150)}`;
|
|
510
|
+
}
|
|
511
|
+
if (toolName === 'Grep' || toolName === 'grep') {
|
|
512
|
+
const parts = [];
|
|
513
|
+
if (typeof toolInput.pattern === 'string')
|
|
514
|
+
parts.push(`pattern=${toolInput.pattern}`);
|
|
515
|
+
if (typeof toolInput.path === 'string')
|
|
516
|
+
parts.push(`path=${toolInput.path}`);
|
|
517
|
+
return parts.join(' ') || '(unknown)';
|
|
518
|
+
}
|
|
519
|
+
if (toolName === 'Glob' || toolName === 'glob') {
|
|
520
|
+
const pattern = typeof toolInput.pattern === 'string' ? toolInput.pattern : '(unknown)';
|
|
521
|
+
return `pattern=${pattern}`;
|
|
522
|
+
}
|
|
523
|
+
return '(details omitted)';
|
|
524
|
+
}
|
|
474
525
|
function extractUsageFromResult(message) {
|
|
475
526
|
if (message.type !== 'result') {
|
|
476
527
|
return {};
|
|
@@ -122,6 +122,7 @@ export class TeamOrchestrator {
|
|
|
122
122
|
persistSession: true,
|
|
123
123
|
includePartialMessages: true,
|
|
124
124
|
permissionMode: 'acceptEdits',
|
|
125
|
+
allowedTools: workerCaps?.sessionAllowedTools,
|
|
125
126
|
restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
|
|
126
127
|
model: input.model,
|
|
127
128
|
effort: (workerCaps?.restrictWriteTools ?? false)
|
|
@@ -219,6 +220,9 @@ export class TeamOrchestrator {
|
|
|
219
220
|
else if (message.includes('context') || message.includes('token limit')) {
|
|
220
221
|
failureKind = 'contextExhausted';
|
|
221
222
|
}
|
|
223
|
+
else if (message.includes('does not support implement mode')) {
|
|
224
|
+
failureKind = 'modeNotSupported';
|
|
225
|
+
}
|
|
222
226
|
else if (message.includes('denied') || message.includes('not allowed')) {
|
|
223
227
|
failureKind = 'toolDenied';
|
|
224
228
|
}
|
|
@@ -426,7 +430,7 @@ export class TeamOrchestrator {
|
|
|
426
430
|
const now = new Date().toISOString();
|
|
427
431
|
await this.teamStore.updateTeam(cwd, teamId, (team) => {
|
|
428
432
|
if (!team.activePlan) {
|
|
429
|
-
throw new Error(`Cannot update slice: team "${teamId}" has no active plan.
|
|
433
|
+
throw new Error(`Cannot update slice: team "${teamId}" has no active plan. Persist an active plan before updating slices.`);
|
|
430
434
|
}
|
|
431
435
|
const sliceExists = team.activePlan.slices.some((s) => s.index === sliceIndex);
|
|
432
436
|
if (!sliceExists) {
|
|
@@ -483,9 +487,10 @@ function buildModeInstruction(mode) {
|
|
|
483
487
|
switch (mode) {
|
|
484
488
|
case 'explore':
|
|
485
489
|
return [
|
|
486
|
-
'
|
|
487
|
-
'Read, search, and reason about the codebase.',
|
|
488
|
-
'
|
|
490
|
+
'Exploration mode.',
|
|
491
|
+
'Read, search, and reason about the codebase without editing files.',
|
|
492
|
+
'The caller should specify the desired output for this exploration task, such as root cause, findings, affected files, options, risk review, or a concrete plan.',
|
|
493
|
+
'If the caller does not specify the output shape, return concise findings, relevant file paths, open questions, and the recommended next step.',
|
|
489
494
|
'Do not create or edit files.',
|
|
490
495
|
].join(' ');
|
|
491
496
|
case 'implement':
|
|
@@ -578,6 +583,8 @@ export function getFailureGuidanceText(failureKind) {
|
|
|
578
583
|
return 'This engineer is currently working on another assignment. Wait for them to finish, choose a different engineer, or try again shortly.';
|
|
579
584
|
case 'toolDenied':
|
|
580
585
|
return 'A tool permission was denied during the assignment. Check the approval policy and tool permissions, then retry.';
|
|
586
|
+
case 'modeNotSupported':
|
|
587
|
+
return 'This engineer does not support the requested work mode. BrowserQA only supports explore and verify modes — use a general engineer (Tom, John, Maya, Sara, Alex) for implement tasks.';
|
|
581
588
|
case 'aborted':
|
|
582
589
|
return 'The assignment was cancelled by the user or an abort signal was triggered. Review the request and try again.';
|
|
583
590
|
case 'sdkError':
|
|
@@ -12,6 +12,10 @@ export function buildWorkerCapabilities(prompts) {
|
|
|
12
12
|
plannerEligible: false,
|
|
13
13
|
isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
|
|
14
14
|
runtimeUnavailableTitle: '❌ Playwright unavailable',
|
|
15
|
+
// Pre-approve the Playwriter toolchain at the SDK level so headless sessions
|
|
16
|
+
// never stall waiting for interactive confirmation. Write tools remain blocked
|
|
17
|
+
// by restrictWriteTools and the canUseTool write-filter.
|
|
18
|
+
sessionAllowedTools: ['Skill', 'Bash', 'Read', 'Grep', 'Glob', 'LS', 'ListDirectory'],
|
|
15
19
|
},
|
|
16
20
|
};
|
|
17
21
|
}
|
|
@@ -11,9 +11,9 @@ export declare const ENGINEER_AGENT_IDS: {
|
|
|
11
11
|
};
|
|
12
12
|
/** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
|
|
13
13
|
export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
14
|
-
export declare const CTO_ONLY_TOOL_IDS: readonly ["team_status", "reset_engineer", "
|
|
14
|
+
export declare const CTO_ONLY_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
|
|
15
15
|
export declare const ENGINEER_TOOL_IDS: readonly ["claude"];
|
|
16
|
-
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "reset_engineer", "
|
|
16
|
+
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
|
|
17
17
|
export type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
18
18
|
export type AgentPermission = {
|
|
19
19
|
'*'?: ToolPermission;
|
|
@@ -13,7 +13,7 @@ function buildTeamPlannerPermissions() {
|
|
|
13
13
|
}
|
|
14
14
|
export function buildTeamPlannerAgentConfig(prompts) {
|
|
15
15
|
return {
|
|
16
|
-
description: '
|
|
16
|
+
description: 'Thin planning wrapper that calls plan_with_team for dual-engineer synthesis with live UI activity.',
|
|
17
17
|
mode: 'subagent',
|
|
18
18
|
hidden: false,
|
|
19
19
|
color: '#D97757',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin';
|
|
2
2
|
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
3
|
+
import { appendDebugLog } from '../util/fs-helpers.js';
|
|
3
4
|
import { isEngineerName } from '../team/roster.js';
|
|
4
5
|
import { TeamOrchestrator, createActionableError, getFailureGuidanceText, } from '../manager/team-orchestrator.js';
|
|
5
6
|
import { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, buildBrowserQaAgentConfig, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './agents/index.js';
|
|
@@ -206,49 +207,6 @@ export const ClaudeManagerPlugin = async ({ worktree, client }) => {
|
|
|
206
207
|
}, null, 2);
|
|
207
208
|
},
|
|
208
209
|
}),
|
|
209
|
-
confirm_plan: tool({
|
|
210
|
-
description: 'Persist plan confirmation and optional slice metadata after the user confirms a plan. For large tasks, provide a slice list to enable per-slice progress tracking. Set preAuthorized to true only when the user has explicitly said to proceed through all slices without further confirmation.',
|
|
211
|
-
args: {
|
|
212
|
-
summary: tool.schema.string().min(1),
|
|
213
|
-
taskSize: tool.schema.enum(['trivial', 'simple', 'large']),
|
|
214
|
-
slices: tool.schema.string().array().optional(),
|
|
215
|
-
preAuthorized: tool.schema.boolean().optional(),
|
|
216
|
-
},
|
|
217
|
-
async execute(args, context) {
|
|
218
|
-
const teamId = context.sessionID;
|
|
219
|
-
annotateToolRun(context, 'Persisting confirmed plan', {
|
|
220
|
-
teamId,
|
|
221
|
-
taskSize: args.taskSize,
|
|
222
|
-
sliceCount: args.slices?.length ?? 0,
|
|
223
|
-
});
|
|
224
|
-
const activePlan = await services.orchestrator.setActivePlan(context.worktree, teamId, {
|
|
225
|
-
summary: args.summary,
|
|
226
|
-
taskSize: args.taskSize,
|
|
227
|
-
slices: args.slices ?? [],
|
|
228
|
-
preAuthorized: args.preAuthorized ?? false,
|
|
229
|
-
});
|
|
230
|
-
return JSON.stringify(activePlan, null, 2);
|
|
231
|
-
},
|
|
232
|
-
}),
|
|
233
|
-
advance_slice: tool({
|
|
234
|
-
description: 'Mark a plan slice as done (or skipped) and advance to the next one. Use this after each slice completes to track large-task progress.',
|
|
235
|
-
args: {
|
|
236
|
-
sliceIndex: tool.schema.number(),
|
|
237
|
-
status: tool.schema.enum(['done', 'skipped']).optional(),
|
|
238
|
-
},
|
|
239
|
-
async execute(args, context) {
|
|
240
|
-
const teamId = context.sessionID;
|
|
241
|
-
const status = args.status ?? 'done';
|
|
242
|
-
annotateToolRun(context, `Advancing slice ${args.sliceIndex} → ${status}`, {
|
|
243
|
-
teamId,
|
|
244
|
-
sliceIndex: args.sliceIndex,
|
|
245
|
-
status,
|
|
246
|
-
});
|
|
247
|
-
await services.orchestrator.updateActivePlanSlice(context.worktree, teamId, args.sliceIndex, status);
|
|
248
|
-
const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
|
|
249
|
-
return JSON.stringify({ activePlan: team.activePlan ?? null }, null, 2);
|
|
250
|
-
},
|
|
251
|
-
}),
|
|
252
210
|
reset_engineer: tool({
|
|
253
211
|
description: 'Reset a stuck or corrupted engineer. Clears the busy flag. Optionally clears the Claude session (starts fresh) and/or wrapper history.',
|
|
254
212
|
args: {
|
|
@@ -499,6 +457,19 @@ async function runEngineerAssignment(input, context) {
|
|
|
499
457
|
guidance,
|
|
500
458
|
},
|
|
501
459
|
});
|
|
460
|
+
try {
|
|
461
|
+
await appendDebugLog(services.debugLogPath, {
|
|
462
|
+
type: 'engineer_failure',
|
|
463
|
+
engineer: failure.engineer,
|
|
464
|
+
teamId: failure.teamId,
|
|
465
|
+
mode: failure.mode,
|
|
466
|
+
failureKind: failure.failureKind,
|
|
467
|
+
message: failure.message.slice(0, 300),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Log write failures must not mask the original error.
|
|
472
|
+
}
|
|
502
473
|
throw createActionableError(failure, error);
|
|
503
474
|
}
|
|
504
475
|
await services.orchestrator.recordWrapperExchange(context.worktree, input.teamId, input.engineer, context.sessionID, input.mode, input.message, result.finalText);
|
|
@@ -11,6 +11,7 @@ interface ClaudeManagerPluginServices {
|
|
|
11
11
|
teamStore: TeamStateStore;
|
|
12
12
|
orchestrator: TeamOrchestrator;
|
|
13
13
|
workerCapabilities: Partial<Record<EngineerName, WorkerCapabilities>>;
|
|
14
|
+
debugLogPath: string;
|
|
14
15
|
}
|
|
15
16
|
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
16
17
|
export declare function clearPluginServices(): void;
|
|
@@ -21,8 +21,9 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
21
21
|
return existing;
|
|
22
22
|
}
|
|
23
23
|
const approvalPolicyPath = path.join(worktree, '.claude-manager', 'approval-policy.json');
|
|
24
|
+
const debugLogPath = path.join(worktree, '.claude-manager', 'debug.log');
|
|
24
25
|
const approvalManager = new ToolApprovalManager(undefined, undefined, approvalPolicyPath);
|
|
25
|
-
const sdkAdapter = new ClaudeAgentSdkAdapter(undefined, approvalManager);
|
|
26
|
+
const sdkAdapter = new ClaudeAgentSdkAdapter(undefined, approvalManager, debugLogPath);
|
|
26
27
|
const sessionService = new ClaudeSessionService(sdkAdapter);
|
|
27
28
|
const gitOps = new GitOperations(worktree);
|
|
28
29
|
const teamStore = new TeamStateStore();
|
|
@@ -37,6 +38,7 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
37
38
|
teamStore,
|
|
38
39
|
orchestrator,
|
|
39
40
|
workerCapabilities,
|
|
41
|
+
debugLogPath,
|
|
40
42
|
};
|
|
41
43
|
serviceRegistry.set(worktree, services);
|
|
42
44
|
return services;
|
package/dist/prompts/registry.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export const managerPromptRegistry = {
|
|
2
2
|
ctoSystemPrompt: [
|
|
3
3
|
'You are a principal engineer orchestrating a team of AI-powered engineers.',
|
|
4
|
-
'Your role is to
|
|
4
|
+
'Your role is to investigate first, delegate precisely, review diffs for production risks, and verify outcomes.',
|
|
5
5
|
'You do not write code. All edits go through engineers. You multiply output by coordinating parallel work and catching issues others miss.',
|
|
6
6
|
'',
|
|
7
|
-
'# Operating Loop: Orient →
|
|
7
|
+
'# Operating Loop: Orient → Investigate → Decide → Delegate → Review → Verify → Close',
|
|
8
|
+
'Treat this loop as adaptive, not rigid. You may revisit earlier steps, skip unnecessary steps, or improvise when the work demands it—as long as you stay explicit about why.',
|
|
8
9
|
'',
|
|
9
10
|
'## Orient: Understand the request',
|
|
10
11
|
'- Extract what you can from the user message, codebase (read/grep/glob/codesearch), prior engineer results, and `websearch`/`webfetch` when relevant.',
|
|
@@ -13,38 +14,33 @@ export const managerPromptRegistry = {
|
|
|
13
14
|
'- If requirements are vague or architecture is unclear, use `question` tool with 2–3 concrete options, your recommendation, and what breaks if user picks differently.',
|
|
14
15
|
'- Only ask when the decision will materially change scope, architecture, risk, or how you verify—and you cannot resolve it from context.',
|
|
15
16
|
'',
|
|
16
|
-
'##
|
|
17
|
+
'## Investigate: Reduce uncertainty before choosing a path',
|
|
18
|
+
'- Start with the smallest useful investigation. For a bug, get to root cause. For a feature, inspect the existing surface area before inventing a plan.',
|
|
19
|
+
'- You may investigate yourself with read-only tools or delegate exploration to one engineer when that is faster or gives better continuity.',
|
|
20
|
+
'- When delegating exploration, explicitly say what artifact you want back: root cause, findings, affected files, options, risk review, file map, or a concrete plan.',
|
|
21
|
+
'- Do not default exploration to planning. Use planning only when the task is genuinely complex, ambiguous, cross-cutting, or risky.',
|
|
22
|
+
'',
|
|
23
|
+
'## Decide: Choose the lightest process that fits the work',
|
|
17
24
|
'- Is this a bug fix, feature, refactor, or something else?',
|
|
18
25
|
'- Task size: classify as trivial (single-line fix, unambiguous, no side effects), simple (one focused task, clear scope, 1–2 files), or large (multiple steps, cross-cutting changes, requires vertical slicing).',
|
|
19
26
|
'- What could go wrong? Is it reversible or irreversible? Can it fail in prod?',
|
|
20
27
|
'- Does it require careful rollout, data migration, observability, or backwards compatibility handling?',
|
|
21
28
|
'- Are there decisions the user has not explicitly made (architecture, scope, deployment strategy)?',
|
|
22
|
-
'',
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
' - Team-planner automatically selects two non-overlapping engineers by availability and context; you may optionally specify lead and challenger.',
|
|
27
|
-
' - Challenger engineer identifies missing decisions, risks, and scope gaps before implementation.',
|
|
28
|
-
'- For large tasks: break into vertical slices before delegating. Each slice must deliver end-to-end, user-testable value independently (e.g., "user can register and receive a confirmation email", "user can view billing history"). Horizontal layers (e.g., "just types", "just tests") are not vertical slices. Document slices when calling `confirm_plan`.',
|
|
29
|
-
'- Break work into independent pieces that can run in parallel. Two engineers exploring then synthesizing beats one engineer doing everything sequentially.',
|
|
30
|
-
'- Before delegating, state your success criteria, not just the task. What done looks like. How you will verify it.',
|
|
31
|
-
'',
|
|
32
|
-
'## Confirm: Get user buy-in before implementing',
|
|
33
|
-
'- After planning but before dispatching any engineer in implement mode, present the plan to the user with the `question` tool.',
|
|
34
|
-
'- State what will be built or changed, which files or systems are affected, what success looks like, and any risks or open decisions.',
|
|
35
|
-
'- If team-planner synthesis surfaced a recommendedQuestion, include it here as part of the confirmation question.',
|
|
36
|
-
'- Do not proceed to implementation until the user confirms the plan.',
|
|
37
|
-
'- After the user confirms, call `confirm_plan` with a summary, taskSize, and (for large tasks) the slice list. Set preAuthorized: true only if the user explicitly says to proceed through all slices without further confirmation.',
|
|
38
|
-
'- For large tasks not preAuthorized: confirm each slice with the user before dispatching it.',
|
|
39
|
-
'- Skip `question` only when: the user has explicitly said "proceed" or "just do it", the change is a trivial fix with no ambiguity, or the task is purely exploratory (no edits).',
|
|
40
|
-
'- If the user refines or rejects the plan, revise it and re-confirm before implementing.',
|
|
29
|
+
'- For trivial or simple work with clear scope: delegate directly to one engineer.',
|
|
30
|
+
'- For bugs or unclear requests: investigate first, then decide whether implementation is now straightforward.',
|
|
31
|
+
"- For complex or cross-cutting work: use `task(subagent_type: 'team-planner', ...)` so the wrapper can sharpen the request and run `plan_with_team` with live UI activity.",
|
|
32
|
+
'- Ask the user to confirm only when the decision materially changes scope, risk, rollout, or architecture. Do not force confirmation for every non-trivial task.',
|
|
41
33
|
'',
|
|
42
34
|
'## Delegate: Send precise assignments',
|
|
43
|
-
"- For single-engineer work: use `task(subagent_type: 'tom'|'john'|'maya'|'sara'|'alex', ...)` and structure the prompt with goal, acceptance criteria, relevant
|
|
44
|
-
"- For
|
|
35
|
+
"- For single-engineer work: use `task(subagent_type: 'tom'|'john'|'maya'|'sara'|'alex', ...)` and structure the prompt with goal, mode, expected deliverable, acceptance criteria, relevant context, constraints, and verification.",
|
|
36
|
+
"- For complex planning work: use `task(subagent_type: 'team-planner', ...)`. The wrapper preserves live activity in the UI while it inspects context lightly and runs `plan_with_team`.",
|
|
45
37
|
"- For browser/UI verification: use `task(subagent_type: 'browser-qa', ...)` with a clear verification goal. BrowserQA uses the Playwright skill to verify in a real browser and can run safe bash when needed.",
|
|
46
|
-
'-
|
|
47
|
-
'-
|
|
38
|
+
'- For large tasks: break work into genuine vertical slices before implementation. Each slice must deliver end-to-end, user-testable value independently (e.g., "user can register and receive a confirmation email", "user can view billing history"). Horizontal layers (e.g., "just types", "just tests") are not vertical slices.',
|
|
39
|
+
'- Break work into independent pieces that can run in parallel. Two engineers exploring and then synthesizing is often better than one engineer guessing alone.',
|
|
40
|
+
'- Before delegating, state success criteria and expected output shape, not just the task. Say what done looks like and how you will verify it.',
|
|
41
|
+
'- If planning surfaced a recommendedQuestion or the work is risky enough to need confirmation, use the `question` tool before implementation. Otherwise, delegate directly.',
|
|
42
|
+
'',
|
|
43
|
+
'- Each assignment includes: goal, mode, expected deliverable, acceptance criteria, relevant context, constraints, and verification method.',
|
|
48
44
|
'- Reuse the same engineer when follow-up work builds on their prior context.',
|
|
49
45
|
'- Only one implementing engineer modifies the worktree at a time. Parallelize exploration, research, and browser verification freely.',
|
|
50
46
|
'- Context warnings (moderate/high/critical) are informational only. Do NOT reset an engineer session in response to a context warning. Sessions auto-reset only on an actual contextExhausted error.',
|
|
@@ -79,6 +75,7 @@ export const managerPromptRegistry = {
|
|
|
79
75
|
'',
|
|
80
76
|
'- Questions: Use the `question` tool when a decision will materially affect scope, architecture, or how you verify the outcome. Name the decision, offer 2–3 concrete options, state your recommendation, and say what breaks if the user picks differently. One high-leverage question at a time.',
|
|
81
77
|
'- Reframing: Before planning, ask what the user is actually trying to achieve, not just what they asked for. If the request sounds like a feature, ask what job-to-be-done it serves.',
|
|
78
|
+
'- Exploration outputs: when you send an engineer in explore mode, specify the expected output explicitly. Examples: root cause, findings, affected files, options, risk review, or implementation plan.',
|
|
82
79
|
'- Engineer selection: When assigning to a single engineer, prefer lower context pressure and less-recently-used engineers. Reuse if follow-up work builds on prior context.',
|
|
83
80
|
'- Context warnings: At moderate/high/critical context levels the system surfaces a warning. These are advisory — do not force session reset. Reserve reset for actual contextExhausted errors only.',
|
|
84
81
|
'- Failure handling:',
|
|
@@ -107,6 +104,7 @@ export const managerPromptRegistry = {
|
|
|
107
104
|
'',
|
|
108
105
|
'Your wrapper context from prior turns is reloaded automatically. Use it to avoid repeating work or re-explaining context that Claude Code already knows.',
|
|
109
106
|
"Return the tool result directly. Add your own commentary only when something was unexpected or needs the CTO's attention.",
|
|
107
|
+
'Explore mode is caller-directed. Follow the requested output shape instead of defaulting to a plan. If the assignment does not specify the output, return findings, relevant files, open questions, and the recommended next step.',
|
|
110
108
|
'If you discover during implementation that the agreed approach is not viable (unexpected constraints, wrong files, missing context), stop immediately and surface the deviation to the CTO before proceeding with a different approach. Do not silently implement something different from what was confirmed.',
|
|
111
109
|
].join('\n'),
|
|
112
110
|
engineerSessionPrompt: [
|
|
@@ -117,6 +115,7 @@ export const managerPromptRegistry = {
|
|
|
117
115
|
'When investigating bugs:',
|
|
118
116
|
'- Always explore the root cause before implementing a fix. Do not assume; verify.',
|
|
119
117
|
'- If three fix attempts fail, question the architecture, not the hypothesis.',
|
|
118
|
+
'- In explore mode, return the artifact the caller asked for. Do not default to a plan unless the caller explicitly asks for one.',
|
|
120
119
|
'',
|
|
121
120
|
'When writing code:',
|
|
122
121
|
'- Consider rollout/migration/observability implications: Will this require staged rollout, data migration, new metrics, or log/trace points?',
|
|
@@ -158,14 +157,15 @@ export const managerPromptRegistry = {
|
|
|
158
157
|
'<answer or NONE>',
|
|
159
158
|
].join('\n'),
|
|
160
159
|
teamPlannerPrompt: [
|
|
161
|
-
'You are the team
|
|
160
|
+
'You are the team-planner wrapper. Your job is to help the CTO get a stronger plan for complex work while preserving live activity in the UI.',
|
|
162
161
|
'`plan_with_team` dispatches two engineers in parallel (lead + challenger) then synthesizes their plans.',
|
|
163
162
|
'',
|
|
164
|
-
'Call `plan_with_team`
|
|
163
|
+
'Call `plan_with_team` with the task and any engineer names provided.',
|
|
165
164
|
'- If lead and challenger engineer names are both specified, use them.',
|
|
166
165
|
'- If either name is missing, `plan_with_team` will auto-select two non-overlapping engineers based on availability and context.',
|
|
167
|
-
'Do not
|
|
168
|
-
'
|
|
166
|
+
'- Keep the wrapper thin. Do not do your own repo investigation or solo planning.',
|
|
167
|
+
'- If the request is blocked by a missing decision, ask one focused question with a recommendation instead of guessing.',
|
|
168
|
+
'After `plan_with_team` returns, pass the full result back to the CTO unchanged.',
|
|
169
169
|
].join('\n'),
|
|
170
170
|
browserQaAgentPrompt: [
|
|
171
171
|
"You are the browser QA specialist on the CTO's team.",
|
|
@@ -17,10 +17,12 @@ interface ClaudeAgentSdkFacade {
|
|
|
17
17
|
export declare class ClaudeAgentSdkAdapter {
|
|
18
18
|
private readonly sdkFacade;
|
|
19
19
|
private readonly approvalManager?;
|
|
20
|
-
|
|
20
|
+
private readonly debugLogPath?;
|
|
21
|
+
constructor(sdkFacade?: ClaudeAgentSdkFacade, approvalManager?: ToolApprovalManager | undefined, debugLogPath?: string | undefined);
|
|
21
22
|
runSession(input: RunClaudeSessionInput, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
22
23
|
listSavedSessions(cwd?: string): Promise<ClaudeSessionSummary[]>;
|
|
23
24
|
getTranscript(sessionId: string, cwd?: string): Promise<ClaudeSessionTranscriptMessage[]>;
|
|
24
25
|
private buildOptions;
|
|
26
|
+
private logDeniedTool;
|
|
25
27
|
}
|
|
26
28
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSessionMessages, listSessions, query, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { appendDebugLog } from '../util/fs-helpers.js';
|
|
2
3
|
import { appendTranscriptEvents } from '../util/transcript-append.js';
|
|
3
4
|
const defaultFacade = {
|
|
4
5
|
query,
|
|
@@ -24,9 +25,11 @@ function mergeSkillIntoAllowedTools(allowedTools, disallowedTools) {
|
|
|
24
25
|
export class ClaudeAgentSdkAdapter {
|
|
25
26
|
sdkFacade;
|
|
26
27
|
approvalManager;
|
|
27
|
-
|
|
28
|
+
debugLogPath;
|
|
29
|
+
constructor(sdkFacade = defaultFacade, approvalManager, debugLogPath) {
|
|
28
30
|
this.sdkFacade = sdkFacade;
|
|
29
31
|
this.approvalManager = approvalManager;
|
|
32
|
+
this.debugLogPath = debugLogPath;
|
|
30
33
|
}
|
|
31
34
|
async runSession(input, onEvent) {
|
|
32
35
|
const options = this.buildOptions(input);
|
|
@@ -156,22 +159,42 @@ export class ClaudeAgentSdkAdapter {
|
|
|
156
159
|
const manager = this.approvalManager;
|
|
157
160
|
options.canUseTool = async (toolName, toolInput, opts) => {
|
|
158
161
|
if (restrictWrites && isWriteTool(toolName, toolInput)) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
162
|
+
const message = 'Write operations are restricted in explore mode. Ask the CTO to re-dispatch in implement mode for edits.';
|
|
163
|
+
await this.logDeniedTool(toolName, toolInput, message, 'restrictWriteTools');
|
|
164
|
+
return { behavior: 'deny', message };
|
|
163
165
|
}
|
|
164
166
|
if (manager) {
|
|
165
|
-
|
|
167
|
+
const decision = manager.evaluate(toolName, toolInput, {
|
|
166
168
|
title: opts.title,
|
|
167
169
|
agentID: opts.agentID,
|
|
168
170
|
});
|
|
171
|
+
if (decision.behavior === 'deny') {
|
|
172
|
+
await this.logDeniedTool(toolName, toolInput, decision.message, 'approvalPolicy');
|
|
173
|
+
}
|
|
174
|
+
return decision;
|
|
169
175
|
}
|
|
170
176
|
return { behavior: 'allow' };
|
|
171
177
|
};
|
|
172
178
|
}
|
|
173
179
|
return options;
|
|
174
180
|
}
|
|
181
|
+
async logDeniedTool(toolName, toolInput, message, reason) {
|
|
182
|
+
if (!this.debugLogPath) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
await appendDebugLog(this.debugLogPath, {
|
|
187
|
+
type: 'tool_denied',
|
|
188
|
+
toolName,
|
|
189
|
+
inputPreview: sanitizeInputForLog(toolName, toolInput),
|
|
190
|
+
reason,
|
|
191
|
+
message,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Log write failures must not block tool evaluation.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
175
198
|
}
|
|
176
199
|
function normalizeSdkMessages(message, includePartials) {
|
|
177
200
|
const sessionId = 'session_id' in message ? message.session_id : undefined;
|
|
@@ -471,6 +494,34 @@ function isWriteTool(toolName, toolInput) {
|
|
|
471
494
|
}
|
|
472
495
|
return false;
|
|
473
496
|
}
|
|
497
|
+
const FILE_TOOL_NAMES = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit', 'Read']);
|
|
498
|
+
/**
|
|
499
|
+
* Returns a compact, safe preview of tool input for debug logging.
|
|
500
|
+
* Intentionally omits file content and other large/sensitive payloads.
|
|
501
|
+
*/
|
|
502
|
+
function sanitizeInputForLog(toolName, toolInput) {
|
|
503
|
+
if (FILE_TOOL_NAMES.has(toolName)) {
|
|
504
|
+
const filePath = typeof toolInput.file_path === 'string' ? toolInput.file_path : '(unknown)';
|
|
505
|
+
return `file_path=${filePath}`;
|
|
506
|
+
}
|
|
507
|
+
if (toolName === 'Bash' || toolName === 'bash') {
|
|
508
|
+
const command = typeof toolInput.command === 'string' ? toolInput.command : '(unknown)';
|
|
509
|
+
return `command=${command.slice(0, 150)}`;
|
|
510
|
+
}
|
|
511
|
+
if (toolName === 'Grep' || toolName === 'grep') {
|
|
512
|
+
const parts = [];
|
|
513
|
+
if (typeof toolInput.pattern === 'string')
|
|
514
|
+
parts.push(`pattern=${toolInput.pattern}`);
|
|
515
|
+
if (typeof toolInput.path === 'string')
|
|
516
|
+
parts.push(`path=${toolInput.path}`);
|
|
517
|
+
return parts.join(' ') || '(unknown)';
|
|
518
|
+
}
|
|
519
|
+
if (toolName === 'Glob' || toolName === 'glob') {
|
|
520
|
+
const pattern = typeof toolInput.pattern === 'string' ? toolInput.pattern : '(unknown)';
|
|
521
|
+
return `pattern=${pattern}`;
|
|
522
|
+
}
|
|
523
|
+
return '(details omitted)';
|
|
524
|
+
}
|
|
474
525
|
function extractUsageFromResult(message) {
|
|
475
526
|
if (message.type !== 'result') {
|
|
476
527
|
return {};
|