@doingdev/opencode-claude-manager-plugin 0.1.57 → 0.1.58
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/manager/team-orchestrator.d.ts +3 -2
- package/dist/manager/team-orchestrator.js +32 -9
- package/dist/plugin/agent-hierarchy.d.ts +1 -54
- package/dist/plugin/agent-hierarchy.js +2 -123
- package/dist/plugin/agents/browser-qa.d.ts +14 -0
- package/dist/plugin/agents/browser-qa.js +27 -0
- package/dist/plugin/agents/common.d.ts +37 -0
- package/dist/plugin/agents/common.js +59 -0
- package/dist/plugin/agents/cto.d.ts +9 -0
- package/dist/plugin/agents/cto.js +39 -0
- package/dist/plugin/agents/engineers.d.ts +9 -0
- package/dist/plugin/agents/engineers.js +11 -0
- package/dist/plugin/agents/index.d.ts +6 -0
- package/dist/plugin/agents/index.js +5 -0
- package/dist/plugin/agents/team-planner.d.ts +10 -0
- package/dist/plugin/agents/team-planner.js +23 -0
- package/dist/plugin/claude-manager.plugin.js +45 -23
- package/dist/plugin/service-factory.d.ts +4 -3
- package/dist/plugin/service-factory.js +4 -1
- package/dist/prompts/registry.js +37 -2
- package/dist/src/manager/team-orchestrator.d.ts +3 -2
- package/dist/src/manager/team-orchestrator.js +32 -9
- package/dist/src/plugin/agent-hierarchy.d.ts +1 -54
- package/dist/src/plugin/agent-hierarchy.js +2 -123
- package/dist/src/plugin/agents/browser-qa.d.ts +14 -0
- package/dist/src/plugin/agents/browser-qa.js +27 -0
- package/dist/src/plugin/agents/common.d.ts +37 -0
- package/dist/src/plugin/agents/common.js +59 -0
- package/dist/src/plugin/agents/cto.d.ts +9 -0
- package/dist/src/plugin/agents/cto.js +39 -0
- package/dist/src/plugin/agents/engineers.d.ts +9 -0
- package/dist/src/plugin/agents/engineers.js +11 -0
- package/dist/src/plugin/agents/index.d.ts +6 -0
- package/dist/src/plugin/agents/index.js +5 -0
- package/dist/src/plugin/agents/team-planner.d.ts +10 -0
- package/dist/src/plugin/agents/team-planner.js +23 -0
- package/dist/src/plugin/claude-manager.plugin.js +45 -23
- package/dist/src/plugin/service-factory.d.ts +4 -3
- package/dist/src/plugin/service-factory.js +4 -1
- package/dist/src/prompts/registry.js +37 -2
- package/dist/src/team/roster.d.ts +3 -2
- package/dist/src/team/roster.js +2 -1
- package/dist/src/types/contracts.d.ts +25 -1
- package/dist/src/types/contracts.js +2 -1
- package/dist/team/roster.d.ts +3 -2
- package/dist/team/roster.js +2 -1
- package/dist/test/claude-manager.plugin.test.js +60 -0
- package/dist/test/prompt-registry.test.js +15 -0
- package/dist/test/report-claude-event.test.js +44 -3
- package/dist/test/team-orchestrator.test.js +47 -8
- package/dist/types/contracts.d.ts +25 -1
- package/dist/types/contracts.js +2 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ 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, createActionableError, getFailureGuidanceText, } from '../manager/team-orchestrator.js';
|
|
5
|
-
import { AGENT_CTO, AGENT_TEAM_PLANNER,
|
|
5
|
+
import { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, buildBrowserQaAgentConfig, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './agents/index.js';
|
|
6
6
|
import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
|
|
7
7
|
const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
|
|
8
8
|
const MODE_ENUM = ['explore', 'implement', 'verify'];
|
|
@@ -16,6 +16,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
16
16
|
denyRestrictedToolsGlobally(config.permission);
|
|
17
17
|
config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
|
|
18
18
|
config.agent[AGENT_TEAM_PLANNER] ??= buildTeamPlannerAgentConfig(managerPromptRegistry);
|
|
19
|
+
config.agent[AGENT_BROWSER_QA] ??= buildBrowserQaAgentConfig(managerPromptRegistry);
|
|
19
20
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
20
21
|
config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
|
|
21
22
|
}
|
|
@@ -41,7 +42,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
41
42
|
const teamId = persisted?.teamId ?? (await resolveTeamId(worktree, input.sessionID));
|
|
42
43
|
setWrapperSessionMapping(worktree, input.sessionID, {
|
|
43
44
|
teamId,
|
|
44
|
-
engineer,
|
|
45
|
+
workerName: engineer,
|
|
45
46
|
});
|
|
46
47
|
await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
|
|
47
48
|
}
|
|
@@ -50,26 +51,37 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
50
51
|
if (!input.sessionID) {
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!
|
|
54
|
+
// Try in-memory mapping first
|
|
55
|
+
let mapping = getWrapperSessionMapping(worktree, input.sessionID);
|
|
56
|
+
// Fall back to persisted lookup if in-memory mapping is absent
|
|
57
|
+
if (!mapping) {
|
|
58
|
+
// Check if this is an engineer wrapper session
|
|
59
|
+
const engineerMatch = await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID);
|
|
60
|
+
if (engineerMatch) {
|
|
61
|
+
mapping = {
|
|
62
|
+
teamId: engineerMatch.teamId,
|
|
63
|
+
workerName: engineerMatch.engineer,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!mapping) {
|
|
57
68
|
return;
|
|
58
69
|
}
|
|
59
|
-
const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree,
|
|
70
|
+
const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree, mapping.teamId, mapping.workerName);
|
|
60
71
|
if (wrapperContext) {
|
|
61
72
|
output.system.push(wrapperContext);
|
|
62
73
|
}
|
|
63
74
|
},
|
|
64
75
|
tool: {
|
|
65
76
|
claude: tool({
|
|
66
|
-
description: "Run work through
|
|
77
|
+
description: "Run work through a named engineer's persistent Claude Code session. Engineers include general developers (Tom, John, Maya, Sara, Alex) and specialists like browser-qa. The session remembers prior turns.",
|
|
67
78
|
args: {
|
|
68
79
|
mode: tool.schema.enum(MODE_ENUM),
|
|
69
80
|
message: tool.schema.string().min(1),
|
|
70
81
|
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
71
82
|
},
|
|
72
83
|
async execute(args, context) {
|
|
84
|
+
// Handle engineer agents (includes BrowserQA)
|
|
73
85
|
const engineer = engineerFromAgent(context.agent);
|
|
74
86
|
const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
|
|
75
87
|
const persisted = existing ??
|
|
@@ -77,7 +89,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
77
89
|
const teamId = persisted?.teamId ?? (await resolveTeamId(context.worktree, context.sessionID));
|
|
78
90
|
setWrapperSessionMapping(context.worktree, context.sessionID, {
|
|
79
91
|
teamId,
|
|
80
|
-
engineer,
|
|
92
|
+
workerName: engineer,
|
|
81
93
|
});
|
|
82
94
|
await services.orchestrator.recordWrapperSession(context.worktree, teamId, engineer, context.sessionID);
|
|
83
95
|
const result = await runEngineerAssignment({
|
|
@@ -87,6 +99,15 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
87
99
|
message: args.message,
|
|
88
100
|
model: args.model,
|
|
89
101
|
}, context);
|
|
102
|
+
const capabilities = services.workerCapabilities[engineer];
|
|
103
|
+
if (capabilities?.isRuntimeUnavailableResponse?.(result.finalText)) {
|
|
104
|
+
const lines = result.finalText.split('\n');
|
|
105
|
+
const unavailableLine = lines[0] ?? 'Playwright unavailable (reason unknown)';
|
|
106
|
+
context.metadata({
|
|
107
|
+
title: capabilities.runtimeUnavailableTitle ?? '❌ Playwright unavailable',
|
|
108
|
+
metadata: { unavailable: unavailableLine },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
90
111
|
return result.finalText;
|
|
91
112
|
},
|
|
92
113
|
}),
|
|
@@ -152,7 +173,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
152
173
|
reset_engineer: tool({
|
|
153
174
|
description: 'Reset a stuck or corrupted engineer. Clears the busy flag. Optionally clears the Claude session (starts fresh) and/or wrapper history.',
|
|
154
175
|
args: {
|
|
155
|
-
engineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
176
|
+
engineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex', 'BrowserQA']),
|
|
156
177
|
clearSession: tool.schema.boolean().optional(),
|
|
157
178
|
clearHistory: tool.schema.boolean().optional(),
|
|
158
179
|
},
|
|
@@ -497,12 +518,13 @@ function formatToolDescription(toolName, toolArgs) {
|
|
|
497
518
|
return undefined;
|
|
498
519
|
}
|
|
499
520
|
}
|
|
500
|
-
function reportClaudeEvent(context,
|
|
521
|
+
function reportClaudeEvent(context, workerName, event) {
|
|
522
|
+
const baseMetadata = { workerName, engineer: workerName };
|
|
501
523
|
if (event.type === 'error') {
|
|
502
524
|
context.metadata({
|
|
503
|
-
title: `❌ ${
|
|
525
|
+
title: `❌ ${workerName} hit an error`,
|
|
504
526
|
metadata: {
|
|
505
|
-
|
|
527
|
+
...baseMetadata,
|
|
506
528
|
sessionId: event.sessionId,
|
|
507
529
|
error: event.text.slice(0, 200),
|
|
508
530
|
},
|
|
@@ -511,9 +533,9 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
511
533
|
}
|
|
512
534
|
if (event.type === 'status') {
|
|
513
535
|
context.metadata({
|
|
514
|
-
title: `ℹ️ ${
|
|
536
|
+
title: `ℹ️ ${workerName}: ${event.text}`,
|
|
515
537
|
metadata: {
|
|
516
|
-
|
|
538
|
+
...baseMetadata,
|
|
517
539
|
status: event.text,
|
|
518
540
|
},
|
|
519
541
|
});
|
|
@@ -521,9 +543,9 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
521
543
|
}
|
|
522
544
|
if (event.type === 'init') {
|
|
523
545
|
context.metadata({
|
|
524
|
-
title: `⚡ ${
|
|
546
|
+
title: `⚡ ${workerName} session ready`,
|
|
525
547
|
metadata: {
|
|
526
|
-
|
|
548
|
+
...baseMetadata,
|
|
527
549
|
sessionId: event.sessionId,
|
|
528
550
|
},
|
|
529
551
|
});
|
|
@@ -557,12 +579,12 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
557
579
|
const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
|
|
558
580
|
context.metadata({
|
|
559
581
|
title: toolDescription
|
|
560
|
-
? `⚡ ${
|
|
582
|
+
? `⚡ ${workerName} → ${toolDescription}`
|
|
561
583
|
: toolName
|
|
562
|
-
? `⚡ ${
|
|
563
|
-
: `⚡ ${
|
|
584
|
+
? `⚡ ${workerName} → ${toolName}`
|
|
585
|
+
: `⚡ ${workerName} is using Claude Code tools`,
|
|
564
586
|
metadata: {
|
|
565
|
-
|
|
587
|
+
...baseMetadata,
|
|
566
588
|
sessionId: event.sessionId,
|
|
567
589
|
...(toolName !== undefined && { toolName }),
|
|
568
590
|
...(toolId !== undefined && { toolId }),
|
|
@@ -575,9 +597,9 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
575
597
|
const isThinking = event.text.startsWith('<thinking>');
|
|
576
598
|
const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
|
|
577
599
|
context.metadata({
|
|
578
|
-
title: `⚡ ${
|
|
600
|
+
title: `⚡ ${workerName} ${stateLabel}`,
|
|
579
601
|
metadata: {
|
|
580
|
-
|
|
602
|
+
...baseMetadata,
|
|
581
603
|
sessionId: event.sessionId,
|
|
582
604
|
preview: event.text.slice(0, 160),
|
|
583
605
|
isThinking,
|
|
@@ -3,13 +3,14 @@ import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
|
3
3
|
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
4
4
|
import { TeamOrchestrator } from '../manager/team-orchestrator.js';
|
|
5
5
|
import { TeamStateStore } from '../state/team-state-store.js';
|
|
6
|
-
import type { EngineerName } from '../types/contracts.js';
|
|
6
|
+
import type { EngineerName, WorkerCapabilities } from '../types/contracts.js';
|
|
7
7
|
interface ClaudeManagerPluginServices {
|
|
8
8
|
manager: PersistentManager;
|
|
9
9
|
sessions: ClaudeSessionService;
|
|
10
10
|
approvalManager: ToolApprovalManager;
|
|
11
11
|
teamStore: TeamStateStore;
|
|
12
12
|
orchestrator: TeamOrchestrator;
|
|
13
|
+
workerCapabilities: Partial<Record<EngineerName, WorkerCapabilities>>;
|
|
13
14
|
}
|
|
14
15
|
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
15
16
|
export declare function clearPluginServices(): void;
|
|
@@ -19,10 +20,10 @@ export declare function getPersistedActiveTeam(worktree: string): Promise<string
|
|
|
19
20
|
export declare function setPersistedActiveTeam(worktree: string, teamId: string): Promise<void>;
|
|
20
21
|
export declare function setWrapperSessionMapping(worktree: string, wrapperSessionId: string, mapping: {
|
|
21
22
|
teamId: string;
|
|
22
|
-
|
|
23
|
+
workerName: EngineerName;
|
|
23
24
|
}): void;
|
|
24
25
|
export declare function getWrapperSessionMapping(worktree: string, wrapperSessionId: string): {
|
|
25
26
|
teamId: string;
|
|
26
|
-
|
|
27
|
+
workerName: EngineerName;
|
|
27
28
|
} | null;
|
|
28
29
|
export {};
|
|
@@ -8,6 +8,7 @@ import { TeamOrchestrator } from '../manager/team-orchestrator.js';
|
|
|
8
8
|
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
9
9
|
import { TeamStateStore } from '../state/team-state-store.js';
|
|
10
10
|
import { TranscriptStore } from '../state/transcript-store.js';
|
|
11
|
+
import { buildWorkerCapabilities } from './agents/browser-qa.js';
|
|
11
12
|
const serviceRegistry = new Map();
|
|
12
13
|
const activeTeamRegistry = new Map();
|
|
13
14
|
const wrapperSessionRegistry = new Map();
|
|
@@ -24,13 +25,15 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
24
25
|
const teamStore = new TeamStateStore();
|
|
25
26
|
const transcriptStore = new TranscriptStore();
|
|
26
27
|
const manager = new PersistentManager(gitOps, transcriptStore);
|
|
27
|
-
const
|
|
28
|
+
const workerCapabilities = buildWorkerCapabilities(managerPromptRegistry);
|
|
29
|
+
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.planSynthesisPrompt, workerCapabilities);
|
|
28
30
|
const services = {
|
|
29
31
|
manager,
|
|
30
32
|
sessions: sessionService,
|
|
31
33
|
approvalManager,
|
|
32
34
|
teamStore,
|
|
33
35
|
orchestrator,
|
|
36
|
+
workerCapabilities,
|
|
34
37
|
};
|
|
35
38
|
serviceRegistry.set(worktree, services);
|
|
36
39
|
return services;
|
package/dist/prompts/registry.js
CHANGED
|
@@ -30,9 +30,10 @@ export const managerPromptRegistry = {
|
|
|
30
30
|
'## Delegate: Send precise assignments',
|
|
31
31
|
"- For single-engineer work: use `task(subagent_type: 'tom'|'john'|'maya'|'sara'|'alex', ...)` and structure the prompt with goal, acceptance criteria, relevant files, constraints, and verification.",
|
|
32
32
|
"- For dual-engineer planning: use `task(subagent_type: 'team-planner', ...)` which will lead + challenger synthesis.",
|
|
33
|
-
|
|
33
|
+
"- 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.",
|
|
34
|
+
'- Each assignment includes: goal, acceptance criteria, relevant context, constraints, and verification method.',
|
|
34
35
|
'- Reuse the same engineer when follow-up work builds on their prior context.',
|
|
35
|
-
'- Only one implementing engineer modifies the worktree at a time. Parallelize exploration and
|
|
36
|
+
'- Only one implementing engineer modifies the worktree at a time. Parallelize exploration, research, and browser verification freely.',
|
|
36
37
|
'',
|
|
37
38
|
'## Review: Inspect diffs for production safety',
|
|
38
39
|
'- After an engineer reports implementation done, review the diff with `git_diff` before declaring it complete.',
|
|
@@ -149,6 +150,40 @@ export const managerPromptRegistry = {
|
|
|
149
150
|
'- If either name is missing, `plan_with_team` will auto-select two non-overlapping engineers based on availability and context.',
|
|
150
151
|
'Do not attempt any planning or analysis yourself. Delegate entirely to `plan_with_team`.',
|
|
151
152
|
].join('\n'),
|
|
153
|
+
browserQaAgentPrompt: [
|
|
154
|
+
"You are the browser QA specialist on the CTO's team.",
|
|
155
|
+
'Your job is to run browser verification tasks through the `claude` tool.',
|
|
156
|
+
'The CTO will send tasks requesting you to test a website or web feature using the Playwright skill/command.',
|
|
157
|
+
'',
|
|
158
|
+
'How to handle verification tasks:',
|
|
159
|
+
'- Extract the verification goal and relevant context from the prompt.',
|
|
160
|
+
'- Use the `claude` tool with mode: "verify" and request Claude Code to use the Playwright skill/command.',
|
|
161
|
+
'- Instruct Claude Code: "Use the Playwright skill/command for real browser testing. If unavailable, report PLAYWRIGHT_UNAVAILABLE: <reason> and stop."',
|
|
162
|
+
'- Return the tool result directly—do not add commentary unless something unexpected occurred.',
|
|
163
|
+
'',
|
|
164
|
+
'Important:',
|
|
165
|
+
'- Never simulate or fabricate test results.',
|
|
166
|
+
'- If the Playwright tool is not available, the result will start with PLAYWRIGHT_UNAVAILABLE:.',
|
|
167
|
+
'- Your persistent Claude Code session remembers prior verification runs.',
|
|
168
|
+
].join('\n'),
|
|
169
|
+
browserQaSessionPrompt: [
|
|
170
|
+
'You are a browser QA specialist. Your job is to verify web features and user flows using the Playwright skill/command.',
|
|
171
|
+
'',
|
|
172
|
+
'For each verification task:',
|
|
173
|
+
'1. Use the Playwright skill/command to control a real browser.',
|
|
174
|
+
'2. Navigate to the specified URL, interact with the UI, and verify the expected behavior.',
|
|
175
|
+
'3. Take screenshots and collect specific error messages if verification fails.',
|
|
176
|
+
'4. Report results concisely: what was tested, pass/fail status, any errors or unexpected behavior.',
|
|
177
|
+
'',
|
|
178
|
+
'CRITICAL: If the Playwright skill or command is unavailable (not installed, command not found, skill not loaded):',
|
|
179
|
+
'- Output EXACTLY as the first line of your response:',
|
|
180
|
+
' PLAYWRIGHT_UNAVAILABLE: <specific reason>',
|
|
181
|
+
'- Do not attempt to verify by other means.',
|
|
182
|
+
'- Do not simulate or fabricate test results.',
|
|
183
|
+
'- Stop after reporting unavailability.',
|
|
184
|
+
'',
|
|
185
|
+
'Allowed tools: Playwright skill/command, safe bash, read-only tools (Read, Grep, Glob). No file editing or code modifications.',
|
|
186
|
+
].join('\n'),
|
|
152
187
|
contextWarnings: {
|
|
153
188
|
moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
|
|
154
189
|
high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
|
|
@@ -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 { EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord } from '../types/contracts.js';
|
|
5
|
+
import type { EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord, WorkerCapabilities } from '../types/contracts.js';
|
|
6
6
|
interface DispatchEngineerInput {
|
|
7
7
|
teamId: string;
|
|
8
8
|
cwd: string;
|
|
@@ -19,7 +19,8 @@ export declare class TeamOrchestrator {
|
|
|
19
19
|
private readonly transcriptStore;
|
|
20
20
|
private readonly engineerSessionPrompt;
|
|
21
21
|
private readonly planSynthesisPrompt;
|
|
22
|
-
|
|
22
|
+
private readonly workerCapabilities;
|
|
23
|
+
constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, planSynthesisPrompt: string, workerCapabilities: Partial<Record<EngineerName, WorkerCapabilities>>);
|
|
23
24
|
getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
|
|
24
25
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
25
26
|
recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
|
|
@@ -7,12 +7,14 @@ export class TeamOrchestrator {
|
|
|
7
7
|
transcriptStore;
|
|
8
8
|
engineerSessionPrompt;
|
|
9
9
|
planSynthesisPrompt;
|
|
10
|
-
|
|
10
|
+
workerCapabilities;
|
|
11
|
+
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, planSynthesisPrompt, workerCapabilities) {
|
|
11
12
|
this.sessions = sessions;
|
|
12
13
|
this.teamStore = teamStore;
|
|
13
14
|
this.transcriptStore = transcriptStore;
|
|
14
15
|
this.engineerSessionPrompt = engineerSessionPrompt;
|
|
15
16
|
this.planSynthesisPrompt = planSynthesisPrompt;
|
|
17
|
+
this.workerCapabilities = workerCapabilities;
|
|
16
18
|
}
|
|
17
19
|
async getOrCreateTeam(cwd, teamId) {
|
|
18
20
|
const existing = await this.teamStore.getTeam(cwd, teamId);
|
|
@@ -89,6 +91,13 @@ export class TeamOrchestrator {
|
|
|
89
91
|
}));
|
|
90
92
|
}
|
|
91
93
|
async dispatchEngineer(input, retryCount = 0) {
|
|
94
|
+
const workerCaps = this.workerCapabilities[input.engineer];
|
|
95
|
+
// Reject write-restricted workers in implement mode
|
|
96
|
+
if (workerCaps?.restrictWriteTools && input.mode === 'implement') {
|
|
97
|
+
throw new Error(`${input.engineer} is a browser QA specialist and does not support implement mode. ` +
|
|
98
|
+
'It can only verify and explore (test browser interactions via Playwright). ' +
|
|
99
|
+
'For code changes, use a general engineer (Tom, John, Maya, Sara, Alex).');
|
|
100
|
+
}
|
|
92
101
|
const team = await this.getOrCreateTeam(input.cwd, input.teamId);
|
|
93
102
|
const engineerState = this.getEngineerState(team, input.engineer);
|
|
94
103
|
await this.reserveEngineer(input.cwd, input.teamId, input.engineer);
|
|
@@ -107,15 +116,19 @@ export class TeamOrchestrator {
|
|
|
107
116
|
const result = await this.sessions.runTask({
|
|
108
117
|
cwd: input.cwd,
|
|
109
118
|
prompt: engineerState.claudeSessionId
|
|
110
|
-
? this.buildEngineerPrompt(input.mode, input.message)
|
|
111
|
-
: `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message)}`,
|
|
119
|
+
? this.buildEngineerPrompt(input.mode, input.message, input.engineer)
|
|
120
|
+
: `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message, input.engineer)}`,
|
|
112
121
|
resumeSessionId: engineerState.claudeSessionId ?? undefined,
|
|
113
122
|
persistSession: true,
|
|
114
123
|
includePartialMessages: true,
|
|
115
124
|
permissionMode: 'acceptEdits',
|
|
116
|
-
restrictWriteTools: input.mode === 'explore',
|
|
125
|
+
restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
|
|
117
126
|
model: input.model,
|
|
118
|
-
effort:
|
|
127
|
+
effort: (workerCaps?.restrictWriteTools ?? false)
|
|
128
|
+
? 'medium'
|
|
129
|
+
: input.mode === 'implement'
|
|
130
|
+
? 'high'
|
|
131
|
+
: 'medium',
|
|
119
132
|
settingSources: ['user', 'project', 'local'],
|
|
120
133
|
abortSignal: input.abortSignal,
|
|
121
134
|
}, input.onEvent);
|
|
@@ -321,9 +334,10 @@ export class TeamOrchestrator {
|
|
|
321
334
|
}
|
|
322
335
|
normalizeTeamRecord(team) {
|
|
323
336
|
const engineerMap = new Map(team.engineers.map((engineer) => [engineer.name, engineer]));
|
|
337
|
+
const emptyTeam = createEmptyTeamRecord(team.id, team.cwd);
|
|
324
338
|
return {
|
|
325
339
|
...team,
|
|
326
|
-
engineers:
|
|
340
|
+
engineers: emptyTeam.engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
|
|
327
341
|
};
|
|
328
342
|
}
|
|
329
343
|
getAvailableEngineers(team) {
|
|
@@ -355,9 +369,11 @@ export class TeamOrchestrator {
|
|
|
355
369
|
}
|
|
356
370
|
async selectPlanEngineers(cwd, teamId, preferredLead, preferredChallenger) {
|
|
357
371
|
const team = await this.getOrCreateTeam(cwd, teamId);
|
|
358
|
-
const
|
|
372
|
+
const allAvailable = this.getAvailableEngineers(team);
|
|
373
|
+
// Filter to only planner-eligible engineers (specialists with plannerEligible: false are excluded)
|
|
374
|
+
const available = allAvailable.filter((e) => this.workerCapabilities[e]?.plannerEligible !== false);
|
|
359
375
|
if (available.length < 2) {
|
|
360
|
-
throw new Error(`Not enough available engineers for dual planning. Need 2, found ${available.length}.`);
|
|
376
|
+
throw new Error(`Not enough available engineers for dual planning. Need 2 general engineers (specialists excluded), found ${available.length}.`);
|
|
361
377
|
}
|
|
362
378
|
const lead = preferredLead ?? available[0];
|
|
363
379
|
const foundChallenger = preferredChallenger ?? available.find((e) => e !== lead);
|
|
@@ -368,6 +384,10 @@ export class TeamOrchestrator {
|
|
|
368
384
|
return { lead, challenger };
|
|
369
385
|
}
|
|
370
386
|
buildSessionSystemPrompt(engineer, mode) {
|
|
387
|
+
const specialistPrompt = this.workerCapabilities[engineer]?.sessionPrompt;
|
|
388
|
+
if (specialistPrompt) {
|
|
389
|
+
return specialistPrompt;
|
|
390
|
+
}
|
|
371
391
|
return [
|
|
372
392
|
this.engineerSessionPrompt,
|
|
373
393
|
'',
|
|
@@ -377,7 +397,10 @@ export class TeamOrchestrator {
|
|
|
377
397
|
.join('\n')
|
|
378
398
|
.trim();
|
|
379
399
|
}
|
|
380
|
-
buildEngineerPrompt(mode, message) {
|
|
400
|
+
buildEngineerPrompt(mode, message, engineer) {
|
|
401
|
+
if (this.workerCapabilities[engineer]?.skipModeInstructions) {
|
|
402
|
+
return message;
|
|
403
|
+
}
|
|
381
404
|
return `${buildModeInstruction(mode)}\n\n${message}`;
|
|
382
405
|
}
|
|
383
406
|
}
|
|
@@ -1,54 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const AGENT_CTO = "cto";
|
|
3
|
-
export declare const AGENT_TEAM_PLANNER = "team-planner";
|
|
4
|
-
export declare const ENGINEER_AGENT_IDS: {
|
|
5
|
-
readonly Tom: "tom";
|
|
6
|
-
readonly John: "john";
|
|
7
|
-
readonly Maya: "maya";
|
|
8
|
-
readonly Sara: "sara";
|
|
9
|
-
readonly Alex: "alex";
|
|
10
|
-
};
|
|
11
|
-
export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
12
|
-
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
13
|
-
type AgentPermission = {
|
|
14
|
-
'*'?: ToolPermission;
|
|
15
|
-
read?: ToolPermission;
|
|
16
|
-
grep?: ToolPermission;
|
|
17
|
-
glob?: ToolPermission;
|
|
18
|
-
list?: ToolPermission;
|
|
19
|
-
codesearch?: ToolPermission;
|
|
20
|
-
webfetch?: ToolPermission;
|
|
21
|
-
websearch?: ToolPermission;
|
|
22
|
-
lsp?: ToolPermission;
|
|
23
|
-
todowrite?: ToolPermission;
|
|
24
|
-
todoread?: ToolPermission;
|
|
25
|
-
question?: ToolPermission;
|
|
26
|
-
task?: ToolPermission | Record<string, ToolPermission>;
|
|
27
|
-
bash?: ToolPermission | Record<string, ToolPermission>;
|
|
28
|
-
[tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
|
|
29
|
-
};
|
|
30
|
-
export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
|
|
31
|
-
description: string;
|
|
32
|
-
mode: "primary";
|
|
33
|
-
color: string;
|
|
34
|
-
permission: AgentPermission;
|
|
35
|
-
prompt: string;
|
|
36
|
-
};
|
|
37
|
-
export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry, engineer: EngineerName): {
|
|
38
|
-
description: string;
|
|
39
|
-
mode: "subagent";
|
|
40
|
-
hidden: boolean;
|
|
41
|
-
color: string;
|
|
42
|
-
permission: AgentPermission;
|
|
43
|
-
prompt: string;
|
|
44
|
-
};
|
|
45
|
-
export declare function buildTeamPlannerAgentConfig(prompts: ManagerPromptRegistry): {
|
|
46
|
-
description: string;
|
|
47
|
-
mode: "subagent";
|
|
48
|
-
hidden: boolean;
|
|
49
|
-
color: string;
|
|
50
|
-
permission: AgentPermission;
|
|
51
|
-
prompt: string;
|
|
52
|
-
};
|
|
53
|
-
export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
|
|
54
|
-
export {};
|
|
1
|
+
export * from './agents/index.js';
|
|
@@ -1,123 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export const AGENT_TEAM_PLANNER = 'team-planner';
|
|
4
|
-
export const ENGINEER_AGENT_IDS = {
|
|
5
|
-
Tom: 'tom',
|
|
6
|
-
John: 'john',
|
|
7
|
-
Maya: 'maya',
|
|
8
|
-
Sara: 'sara',
|
|
9
|
-
Alex: 'alex',
|
|
10
|
-
};
|
|
11
|
-
export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
|
|
12
|
-
const CTO_ONLY_TOOL_IDS = [
|
|
13
|
-
'team_status',
|
|
14
|
-
'reset_engineer',
|
|
15
|
-
'list_transcripts',
|
|
16
|
-
'list_history',
|
|
17
|
-
'git_diff',
|
|
18
|
-
'git_commit',
|
|
19
|
-
'git_reset',
|
|
20
|
-
'git_status',
|
|
21
|
-
'git_log',
|
|
22
|
-
'approval_policy',
|
|
23
|
-
'approval_decisions',
|
|
24
|
-
'approval_update',
|
|
25
|
-
];
|
|
26
|
-
const ENGINEER_TOOL_IDS = ['claude'];
|
|
27
|
-
const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
|
|
28
|
-
const CTO_READONLY_TOOLS = {
|
|
29
|
-
read: 'allow',
|
|
30
|
-
grep: 'allow',
|
|
31
|
-
glob: 'allow',
|
|
32
|
-
list: 'allow',
|
|
33
|
-
codesearch: 'allow',
|
|
34
|
-
webfetch: 'allow',
|
|
35
|
-
websearch: 'allow',
|
|
36
|
-
lsp: 'allow',
|
|
37
|
-
todowrite: 'allow',
|
|
38
|
-
todoread: 'allow',
|
|
39
|
-
question: 'allow',
|
|
40
|
-
};
|
|
41
|
-
function buildTeamPlannerPermissions() {
|
|
42
|
-
const denied = {};
|
|
43
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
44
|
-
denied[toolId] = 'deny';
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
'*': 'deny',
|
|
48
|
-
plan_with_team: 'allow',
|
|
49
|
-
question: 'allow',
|
|
50
|
-
...denied,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function buildCtoPermissions() {
|
|
54
|
-
const denied = {};
|
|
55
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
56
|
-
denied[toolId] = 'deny';
|
|
57
|
-
}
|
|
58
|
-
const allowed = {};
|
|
59
|
-
for (const toolId of CTO_ONLY_TOOL_IDS) {
|
|
60
|
-
allowed[toolId] = 'allow';
|
|
61
|
-
}
|
|
62
|
-
const taskPermissions = { '*': 'deny' };
|
|
63
|
-
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
64
|
-
const agentId = ENGINEER_AGENT_IDS[engineer];
|
|
65
|
-
// Support both uppercase (user-friendly) and lowercase (canonical) agent IDs.
|
|
66
|
-
// This ensures both task({ subagent_type: 'Tom' }) and task({ subagent_type: 'tom' }) work.
|
|
67
|
-
taskPermissions[engineer] = 'allow'; // 'Tom', 'John', etc.
|
|
68
|
-
taskPermissions[agentId] = 'allow'; // 'tom', 'john', etc.
|
|
69
|
-
}
|
|
70
|
-
taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
|
|
71
|
-
return {
|
|
72
|
-
'*': 'deny',
|
|
73
|
-
...CTO_READONLY_TOOLS,
|
|
74
|
-
...denied,
|
|
75
|
-
...allowed,
|
|
76
|
-
task: taskPermissions,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function buildEngineerPermissions() {
|
|
80
|
-
const denied = {};
|
|
81
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
82
|
-
denied[toolId] = 'deny';
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
'*': 'deny',
|
|
86
|
-
...denied,
|
|
87
|
-
claude: 'allow',
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
export function buildCtoAgentConfig(prompts) {
|
|
91
|
-
return {
|
|
92
|
-
description: 'Principal engineer who orchestrates AI-powered engineers. Decomposes work, asks clarifying questions, delegates precisely, reviews diffs, and owns the outcome.',
|
|
93
|
-
mode: 'primary',
|
|
94
|
-
color: '#D97757',
|
|
95
|
-
permission: buildCtoPermissions(),
|
|
96
|
-
prompt: prompts.ctoSystemPrompt,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
export function buildEngineerAgentConfig(prompts, engineer) {
|
|
100
|
-
return {
|
|
101
|
-
description: `${engineer} is a persistent engineer who works through one Claude Code session and remembers prior turns. Receives structured assignments (goal, mode, context, acceptance criteria, relevant paths, constraints, verification).`,
|
|
102
|
-
mode: 'subagent',
|
|
103
|
-
hidden: false,
|
|
104
|
-
color: '#D97757',
|
|
105
|
-
permission: buildEngineerPermissions(),
|
|
106
|
-
prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
export function buildTeamPlannerAgentConfig(prompts) {
|
|
110
|
-
return {
|
|
111
|
-
description: 'Runs dual-engineer planning by calling plan_with_team. Automatically selects two non-overlapping available engineers if engineer names are not provided.',
|
|
112
|
-
mode: 'subagent',
|
|
113
|
-
hidden: false,
|
|
114
|
-
color: '#D97757',
|
|
115
|
-
permission: buildTeamPlannerPermissions(),
|
|
116
|
-
prompt: prompts.teamPlannerPrompt,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
export function denyRestrictedToolsGlobally(permissions) {
|
|
120
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
121
|
-
permissions[toolId] ??= 'deny';
|
|
122
|
-
}
|
|
123
|
-
}
|
|
1
|
+
// Re-export barrel — all symbols now live in src/plugin/agents/.
|
|
2
|
+
export * from './agents/index.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EngineerName, ManagerPromptRegistry, WorkerCapabilities } from '../../types/contracts.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the worker capabilities map for all specialist workers.
|
|
4
|
+
* Called once at service-factory construction time to avoid re-building on each tool call.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildWorkerCapabilities(prompts: ManagerPromptRegistry): Partial<Record<EngineerName, WorkerCapabilities>>;
|
|
7
|
+
export declare function buildBrowserQaAgentConfig(prompts: ManagerPromptRegistry): {
|
|
8
|
+
description: string;
|
|
9
|
+
mode: "subagent";
|
|
10
|
+
hidden: boolean;
|
|
11
|
+
color: string;
|
|
12
|
+
permission: import("./common.js").AgentPermission;
|
|
13
|
+
prompt: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { buildEngineerPermissions } from './common.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the worker capabilities map for all specialist workers.
|
|
4
|
+
* Called once at service-factory construction time to avoid re-building on each tool call.
|
|
5
|
+
*/
|
|
6
|
+
export function buildWorkerCapabilities(prompts) {
|
|
7
|
+
return {
|
|
8
|
+
BrowserQA: {
|
|
9
|
+
sessionPrompt: prompts.browserQaSessionPrompt,
|
|
10
|
+
restrictWriteTools: true,
|
|
11
|
+
skipModeInstructions: true,
|
|
12
|
+
plannerEligible: false,
|
|
13
|
+
isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
|
|
14
|
+
runtimeUnavailableTitle: '❌ Playwright unavailable',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function buildBrowserQaAgentConfig(prompts) {
|
|
19
|
+
return {
|
|
20
|
+
description: 'Browser QA specialist who uses the Playwright skill/command to test web features and user flows. Maintains a persistent Claude Code session that remembers prior verification runs.',
|
|
21
|
+
mode: 'subagent',
|
|
22
|
+
hidden: false,
|
|
23
|
+
color: '#D97757',
|
|
24
|
+
permission: buildEngineerPermissions(), // Same permissions as engineers (claude tool only)
|
|
25
|
+
prompt: prompts.browserQaAgentPrompt,
|
|
26
|
+
};
|
|
27
|
+
}
|