@doingdev/opencode-claude-manager-plugin 0.1.57 → 0.1.59
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 +97 -47
- package/dist/plugin/service-factory.d.ts +8 -7
- package/dist/plugin/service-factory.js +18 -19
- 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 +97 -47
- package/dist/src/plugin/service-factory.d.ts +8 -7
- package/dist/src/plugin/service-factory.js +18 -19
- package/dist/src/prompts/registry.js +37 -2
- package/dist/src/state/team-state-store.d.ts +0 -3
- package/dist/src/state/team-state-store.js +0 -22
- 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/state/team-state-store.d.ts +0 -3
- package/dist/state/team-state-store.js +0 -22
- 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/cto-active-team.test.js +176 -29
- package/dist/test/prompt-registry.test.js +15 -0
- package/dist/test/report-claude-event.test.js +60 -15
- package/dist/test/team-orchestrator.test.js +47 -8
- package/dist/test/team-state-store.test.js +0 -18
- package/dist/types/contracts.d.ts +25 -1
- package/dist/types/contracts.js +2 -1
- package/package.json +1 -1
|
@@ -2,13 +2,47 @@ 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,
|
|
6
|
-
import {
|
|
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
|
+
import { getOrCreatePluginServices, getParentSessionId, getSessionTeam, getWrapperSessionMapping, registerParentSession, registerSessionTeam, 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'];
|
|
9
|
-
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
9
|
+
export const ClaudeManagerPlugin = async ({ worktree, client }) => {
|
|
10
10
|
const services = getOrCreatePluginServices(worktree);
|
|
11
11
|
await services.approvalManager.loadPersistedPolicy();
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the team ID for a brand-new engineer wrapper session.
|
|
14
|
+
*
|
|
15
|
+
* 1. Walk the cached parentID chain (populated by session.created events).
|
|
16
|
+
* 2. On a cache miss, attempt a live client.session.get() lookup and cache
|
|
17
|
+
* whatever parentID the SDK returns.
|
|
18
|
+
* 3. Fall back to the orphan sentinel (sessionID itself) only when both
|
|
19
|
+
* cache and live lookup come up empty.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveTeamId(sessionID) {
|
|
22
|
+
let current = sessionID;
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
while (current && !seen.has(current)) {
|
|
25
|
+
seen.add(current);
|
|
26
|
+
const team = getSessionTeam(current);
|
|
27
|
+
if (team !== undefined)
|
|
28
|
+
return team;
|
|
29
|
+
// Cache miss on this node's parent: try the live SDK.
|
|
30
|
+
if (client && !getParentSessionId(current)) {
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.session.get({ path: { id: current } });
|
|
33
|
+
const parentID = result.data?.parentID;
|
|
34
|
+
if (parentID) {
|
|
35
|
+
registerParentSession(current, parentID);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Network / auth failure — let the walk continue to orphan.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
current = getParentSessionId(current);
|
|
43
|
+
}
|
|
44
|
+
return sessionID;
|
|
45
|
+
}
|
|
12
46
|
return {
|
|
13
47
|
config: async (config) => {
|
|
14
48
|
config.agent ??= {};
|
|
@@ -16,21 +50,16 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
16
50
|
denyRestrictedToolsGlobally(config.permission);
|
|
17
51
|
config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
|
|
18
52
|
config.agent[AGENT_TEAM_PLANNER] ??= buildTeamPlannerAgentConfig(managerPromptRegistry);
|
|
53
|
+
config.agent[AGENT_BROWSER_QA] ??= buildBrowserQaAgentConfig(managerPromptRegistry);
|
|
19
54
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
20
55
|
config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
|
|
21
56
|
}
|
|
22
57
|
},
|
|
23
58
|
'chat.message': async (input) => {
|
|
24
59
|
if (input.agent === AGENT_CTO) {
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
const activeTeamId = persistedTeamId ?? input.sessionID;
|
|
29
|
-
setActiveTeamSession(worktree, activeTeamId);
|
|
30
|
-
if (!persistedTeamId) {
|
|
31
|
-
// First CTO session for this worktree — persist this session as active team.
|
|
32
|
-
await setPersistedActiveTeam(worktree, activeTeamId);
|
|
33
|
-
}
|
|
60
|
+
// Each CTO session ID is its own team. The session ID is the durable
|
|
61
|
+
// identity: no worktree-global active-team state.
|
|
62
|
+
registerSessionTeam(input.sessionID, input.sessionID);
|
|
34
63
|
return;
|
|
35
64
|
}
|
|
36
65
|
if (input.agent && isEngineerAgent(input.agent)) {
|
|
@@ -38,46 +67,65 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
38
67
|
const existing = getWrapperSessionMapping(worktree, input.sessionID);
|
|
39
68
|
const persisted = existing ??
|
|
40
69
|
(await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
|
|
41
|
-
const teamId = persisted?.teamId ?? (await resolveTeamId(
|
|
70
|
+
const teamId = persisted?.teamId ?? (await resolveTeamId(input.sessionID));
|
|
42
71
|
setWrapperSessionMapping(worktree, input.sessionID, {
|
|
43
72
|
teamId,
|
|
44
|
-
engineer,
|
|
73
|
+
workerName: engineer,
|
|
45
74
|
});
|
|
46
75
|
await services.orchestrator.recordWrapperSession(worktree, teamId, engineer, input.sessionID);
|
|
47
76
|
}
|
|
48
77
|
},
|
|
78
|
+
event: async ({ event: sdkEvent }) => {
|
|
79
|
+
if (sdkEvent.type === 'session.created') {
|
|
80
|
+
const session = sdkEvent.properties.info;
|
|
81
|
+
if (session.parentID) {
|
|
82
|
+
registerParentSession(session.id, session.parentID);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
49
86
|
'experimental.chat.system.transform': async (input, output) => {
|
|
50
87
|
if (!input.sessionID) {
|
|
51
88
|
return;
|
|
52
89
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!
|
|
90
|
+
// Try in-memory mapping first
|
|
91
|
+
let mapping = getWrapperSessionMapping(worktree, input.sessionID);
|
|
92
|
+
// Fall back to persisted lookup if in-memory mapping is absent
|
|
93
|
+
if (!mapping) {
|
|
94
|
+
// Check if this is an engineer wrapper session
|
|
95
|
+
const engineerMatch = await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID);
|
|
96
|
+
if (engineerMatch) {
|
|
97
|
+
mapping = {
|
|
98
|
+
teamId: engineerMatch.teamId,
|
|
99
|
+
workerName: engineerMatch.engineer,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!mapping) {
|
|
57
104
|
return;
|
|
58
105
|
}
|
|
59
|
-
const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree,
|
|
106
|
+
const wrapperContext = await services.orchestrator.getWrapperSystemContext(worktree, mapping.teamId, mapping.workerName);
|
|
60
107
|
if (wrapperContext) {
|
|
61
108
|
output.system.push(wrapperContext);
|
|
62
109
|
}
|
|
63
110
|
},
|
|
64
111
|
tool: {
|
|
65
112
|
claude: tool({
|
|
66
|
-
description: "Run work through
|
|
113
|
+
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
114
|
args: {
|
|
68
115
|
mode: tool.schema.enum(MODE_ENUM),
|
|
69
116
|
message: tool.schema.string().min(1),
|
|
70
117
|
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
71
118
|
},
|
|
72
119
|
async execute(args, context) {
|
|
120
|
+
// Handle engineer agents (includes BrowserQA)
|
|
73
121
|
const engineer = engineerFromAgent(context.agent);
|
|
74
122
|
const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
|
|
75
123
|
const persisted = existing ??
|
|
76
124
|
(await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
|
|
77
|
-
const teamId = persisted?.teamId ?? (await resolveTeamId(context.
|
|
125
|
+
const teamId = persisted?.teamId ?? (await resolveTeamId(context.sessionID));
|
|
78
126
|
setWrapperSessionMapping(context.worktree, context.sessionID, {
|
|
79
127
|
teamId,
|
|
80
|
-
engineer,
|
|
128
|
+
workerName: engineer,
|
|
81
129
|
});
|
|
82
130
|
await services.orchestrator.recordWrapperSession(context.worktree, teamId, engineer, context.sessionID);
|
|
83
131
|
const result = await runEngineerAssignment({
|
|
@@ -87,6 +135,15 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
87
135
|
message: args.message,
|
|
88
136
|
model: args.model,
|
|
89
137
|
}, context);
|
|
138
|
+
const capabilities = services.workerCapabilities[engineer];
|
|
139
|
+
if (capabilities?.isRuntimeUnavailableResponse?.(result.finalText)) {
|
|
140
|
+
const lines = result.finalText.split('\n');
|
|
141
|
+
const unavailableLine = lines[0] ?? 'Playwright unavailable (reason unknown)';
|
|
142
|
+
context.metadata({
|
|
143
|
+
title: capabilities.runtimeUnavailableTitle ?? '❌ Playwright unavailable',
|
|
144
|
+
metadata: { unavailable: unavailableLine },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
90
147
|
return result.finalText;
|
|
91
148
|
},
|
|
92
149
|
}),
|
|
@@ -96,7 +153,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
96
153
|
teamId: tool.schema.string().optional(),
|
|
97
154
|
},
|
|
98
155
|
async execute(args, context) {
|
|
99
|
-
const teamId = args.teamId ??
|
|
156
|
+
const teamId = args.teamId ?? context.sessionID;
|
|
100
157
|
annotateToolRun(context, 'Reading team status', {
|
|
101
158
|
teamId,
|
|
102
159
|
});
|
|
@@ -113,7 +170,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
113
170
|
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
114
171
|
},
|
|
115
172
|
async execute(args, context) {
|
|
116
|
-
const teamId =
|
|
173
|
+
const teamId = context.sessionID;
|
|
117
174
|
// Pre-determine engineers for event labeling (using orchestrator selection logic)
|
|
118
175
|
const { lead, challenger } = await services.orchestrator.selectPlanEngineers(context.worktree, teamId, args.leadEngineer, args.challengerEngineer);
|
|
119
176
|
annotateToolRun(context, 'Running dual-engineer plan synthesis', {
|
|
@@ -152,12 +209,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
152
209
|
reset_engineer: tool({
|
|
153
210
|
description: 'Reset a stuck or corrupted engineer. Clears the busy flag. Optionally clears the Claude session (starts fresh) and/or wrapper history.',
|
|
154
211
|
args: {
|
|
155
|
-
engineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
212
|
+
engineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex', 'BrowserQA']),
|
|
156
213
|
clearSession: tool.schema.boolean().optional(),
|
|
157
214
|
clearHistory: tool.schema.boolean().optional(),
|
|
158
215
|
},
|
|
159
216
|
async execute(args, context) {
|
|
160
|
-
const teamId =
|
|
217
|
+
const teamId = context.sessionID;
|
|
161
218
|
annotateToolRun(context, `Resetting ${args.engineer}`, {
|
|
162
219
|
teamId,
|
|
163
220
|
clearSession: args.clearSession,
|
|
@@ -435,14 +492,6 @@ export function isEngineerAgent(agentId) {
|
|
|
435
492
|
const normalized = normalizeAgentId(agentId);
|
|
436
493
|
return Object.values(ENGINEER_AGENT_IDS).some((id) => id === normalized);
|
|
437
494
|
}
|
|
438
|
-
/**
|
|
439
|
-
* Resolves the team ID for an engineer session.
|
|
440
|
-
* Reads the persisted active team first (survives process restarts), then
|
|
441
|
-
* falls back to the in-memory registry, then to the raw session ID as a last resort.
|
|
442
|
-
*/
|
|
443
|
-
async function resolveTeamId(worktree, sessionID) {
|
|
444
|
-
return (await getPersistedActiveTeam(worktree)) ?? getActiveTeamSession(worktree) ?? sessionID;
|
|
445
|
-
}
|
|
446
495
|
function formatToolDescription(toolName, toolArgs) {
|
|
447
496
|
if (!toolArgs || typeof toolArgs !== 'object')
|
|
448
497
|
return undefined;
|
|
@@ -497,12 +546,13 @@ function formatToolDescription(toolName, toolArgs) {
|
|
|
497
546
|
return undefined;
|
|
498
547
|
}
|
|
499
548
|
}
|
|
500
|
-
function reportClaudeEvent(context,
|
|
549
|
+
function reportClaudeEvent(context, workerName, event) {
|
|
550
|
+
const baseMetadata = { workerName, engineer: workerName };
|
|
501
551
|
if (event.type === 'error') {
|
|
502
552
|
context.metadata({
|
|
503
|
-
title: `❌ ${
|
|
553
|
+
title: `❌ ${workerName} hit an error`,
|
|
504
554
|
metadata: {
|
|
505
|
-
|
|
555
|
+
...baseMetadata,
|
|
506
556
|
sessionId: event.sessionId,
|
|
507
557
|
error: event.text.slice(0, 200),
|
|
508
558
|
},
|
|
@@ -511,9 +561,9 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
511
561
|
}
|
|
512
562
|
if (event.type === 'status') {
|
|
513
563
|
context.metadata({
|
|
514
|
-
title: `ℹ️ ${
|
|
564
|
+
title: `ℹ️ ${workerName}: ${event.text}`,
|
|
515
565
|
metadata: {
|
|
516
|
-
|
|
566
|
+
...baseMetadata,
|
|
517
567
|
status: event.text,
|
|
518
568
|
},
|
|
519
569
|
});
|
|
@@ -521,9 +571,9 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
521
571
|
}
|
|
522
572
|
if (event.type === 'init') {
|
|
523
573
|
context.metadata({
|
|
524
|
-
title: `⚡ ${
|
|
574
|
+
title: `⚡ ${workerName} session ready`,
|
|
525
575
|
metadata: {
|
|
526
|
-
|
|
576
|
+
...baseMetadata,
|
|
527
577
|
sessionId: event.sessionId,
|
|
528
578
|
},
|
|
529
579
|
});
|
|
@@ -557,12 +607,12 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
557
607
|
const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
|
|
558
608
|
context.metadata({
|
|
559
609
|
title: toolDescription
|
|
560
|
-
? `⚡ ${
|
|
610
|
+
? `⚡ ${workerName} → ${toolDescription}`
|
|
561
611
|
: toolName
|
|
562
|
-
? `⚡ ${
|
|
563
|
-
: `⚡ ${
|
|
612
|
+
? `⚡ ${workerName} → ${toolName}`
|
|
613
|
+
: `⚡ ${workerName} is using Claude Code tools`,
|
|
564
614
|
metadata: {
|
|
565
|
-
|
|
615
|
+
...baseMetadata,
|
|
566
616
|
sessionId: event.sessionId,
|
|
567
617
|
...(toolName !== undefined && { toolName }),
|
|
568
618
|
...(toolId !== undefined && { toolId }),
|
|
@@ -575,9 +625,9 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
575
625
|
const isThinking = event.text.startsWith('<thinking>');
|
|
576
626
|
const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
|
|
577
627
|
context.metadata({
|
|
578
|
-
title: `⚡ ${
|
|
628
|
+
title: `⚡ ${workerName} ${stateLabel}`,
|
|
579
629
|
metadata: {
|
|
580
|
-
|
|
630
|
+
...baseMetadata,
|
|
581
631
|
sessionId: event.sessionId,
|
|
582
632
|
preview: event.text.slice(0, 160),
|
|
583
633
|
isThinking,
|
|
@@ -3,26 +3,27 @@ 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;
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function
|
|
18
|
-
export declare function
|
|
19
|
-
export declare function
|
|
17
|
+
export declare function registerParentSession(childId: string, parentId: string): void;
|
|
18
|
+
export declare function getParentSessionId(childId: string): string | undefined;
|
|
19
|
+
export declare function registerSessionTeam(sessionId: string, teamId: string): void;
|
|
20
|
+
export declare function getSessionTeam(sessionId: string): string | undefined;
|
|
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,9 +8,13 @@ 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
|
-
const activeTeamRegistry = new Map();
|
|
13
13
|
const wrapperSessionRegistry = new Map();
|
|
14
|
+
/** childSessionId → parentSessionId — populated from session.created events */
|
|
15
|
+
const parentSessionRegistry = new Map();
|
|
16
|
+
/** ctoSessionId → teamId — populated when a CTO chat.message fires */
|
|
17
|
+
const sessionTeamRegistry = new Map();
|
|
14
18
|
export function getOrCreatePluginServices(worktree) {
|
|
15
19
|
const existing = serviceRegistry.get(worktree);
|
|
16
20
|
if (existing) {
|
|
@@ -24,41 +28,36 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
24
28
|
const teamStore = new TeamStateStore();
|
|
25
29
|
const transcriptStore = new TranscriptStore();
|
|
26
30
|
const manager = new PersistentManager(gitOps, transcriptStore);
|
|
27
|
-
const
|
|
31
|
+
const workerCapabilities = buildWorkerCapabilities(managerPromptRegistry);
|
|
32
|
+
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.planSynthesisPrompt, workerCapabilities);
|
|
28
33
|
const services = {
|
|
29
34
|
manager,
|
|
30
35
|
sessions: sessionService,
|
|
31
36
|
approvalManager,
|
|
32
37
|
teamStore,
|
|
33
38
|
orchestrator,
|
|
39
|
+
workerCapabilities,
|
|
34
40
|
};
|
|
35
41
|
serviceRegistry.set(worktree, services);
|
|
36
42
|
return services;
|
|
37
43
|
}
|
|
38
44
|
export function clearPluginServices() {
|
|
39
45
|
serviceRegistry.clear();
|
|
40
|
-
activeTeamRegistry.clear();
|
|
41
46
|
wrapperSessionRegistry.clear();
|
|
47
|
+
parentSessionRegistry.clear();
|
|
48
|
+
sessionTeamRegistry.clear();
|
|
42
49
|
}
|
|
43
|
-
export function
|
|
44
|
-
|
|
50
|
+
export function registerParentSession(childId, parentId) {
|
|
51
|
+
parentSessionRegistry.set(childId, parentId);
|
|
45
52
|
}
|
|
46
|
-
export function
|
|
47
|
-
return
|
|
53
|
+
export function getParentSessionId(childId) {
|
|
54
|
+
return parentSessionRegistry.get(childId);
|
|
48
55
|
}
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
if (!services) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
return services.teamStore.getActiveTeam(worktree);
|
|
56
|
+
export function registerSessionTeam(sessionId, teamId) {
|
|
57
|
+
sessionTeamRegistry.set(sessionId, teamId);
|
|
55
58
|
}
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
if (!services) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
await services.teamStore.setActiveTeam(worktree, teamId);
|
|
59
|
+
export function getSessionTeam(sessionId) {
|
|
60
|
+
return sessionTeamRegistry.get(sessionId);
|
|
62
61
|
}
|
|
63
62
|
export function setWrapperSessionMapping(worktree, wrapperSessionId, mapping) {
|
|
64
63
|
wrapperSessionRegistry.set(`${worktree}:${wrapperSessionId}`, mapping);
|
|
@@ -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.',
|
|
@@ -7,10 +7,7 @@ export declare class TeamStateStore {
|
|
|
7
7
|
getTeam(cwd: string, teamId: string): Promise<TeamRecord | null>;
|
|
8
8
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
9
9
|
updateTeam(cwd: string, teamId: string, update: (team: TeamRecord) => TeamRecord): Promise<TeamRecord>;
|
|
10
|
-
getActiveTeam(cwd: string): Promise<string | null>;
|
|
11
|
-
setActiveTeam(cwd: string, teamId: string): Promise<void>;
|
|
12
10
|
private getTeamKey;
|
|
13
|
-
private getActiveTeamPath;
|
|
14
11
|
private getTeamsDirectory;
|
|
15
12
|
private getTeamPath;
|
|
16
13
|
private enqueueWrite;
|
|
@@ -62,31 +62,9 @@ export class TeamStateStore {
|
|
|
62
62
|
return updated;
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
async getActiveTeam(cwd) {
|
|
66
|
-
const filePath = this.getActiveTeamPath(cwd);
|
|
67
|
-
try {
|
|
68
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
69
|
-
const parsed = JSON.parse(content);
|
|
70
|
-
return parsed.teamId ?? null;
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
if (isFileNotFoundError(error)) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async setActiveTeam(cwd, teamId) {
|
|
80
|
-
const filePath = this.getActiveTeamPath(cwd);
|
|
81
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
82
|
-
await writeJsonAtomically(filePath, { teamId });
|
|
83
|
-
}
|
|
84
65
|
getTeamKey(cwd, teamId) {
|
|
85
66
|
return `${cwd}:${teamId}`;
|
|
86
67
|
}
|
|
87
|
-
getActiveTeamPath(cwd) {
|
|
88
|
-
return path.join(cwd, this.baseDirectoryName, 'active-team.json');
|
|
89
|
-
}
|
|
90
68
|
getTeamsDirectory(cwd) {
|
|
91
69
|
return path.join(cwd, this.baseDirectoryName, 'teams');
|
|
92
70
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type EngineerName, type TeamEngineerRecord, type TeamRecord } from '../types/contracts.js';
|
|
2
|
-
export declare const TEAM_ENGINEERS: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
1
|
+
import { PLANNER_ELIGIBLE_ENGINEERS, type EngineerName, type TeamEngineerRecord, type TeamRecord } from '../types/contracts.js';
|
|
2
|
+
export declare const TEAM_ENGINEERS: readonly ["Tom", "John", "Maya", "Sara", "Alex", "BrowserQA"];
|
|
3
|
+
export { PLANNER_ELIGIBLE_ENGINEERS };
|
|
3
4
|
export declare function isEngineerName(value: string): value is EngineerName;
|
|
4
5
|
export declare function createEmptyTeamRecord(teamId: string, cwd: string): TeamRecord;
|
|
5
6
|
export declare function createEmptyEngineerRecord(name: EngineerName): TeamEngineerRecord;
|
package/dist/src/team/roster.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { DEFAULT_ENGINEER_NAMES, } from '../types/contracts.js';
|
|
1
|
+
import { DEFAULT_ENGINEER_NAMES, PLANNER_ELIGIBLE_ENGINEERS, } from '../types/contracts.js';
|
|
2
2
|
export const TEAM_ENGINEERS = DEFAULT_ENGINEER_NAMES;
|
|
3
|
+
export { PLANNER_ELIGIBLE_ENGINEERS };
|
|
3
4
|
export function isEngineerName(value) {
|
|
4
5
|
return TEAM_ENGINEERS.includes(value);
|
|
5
6
|
}
|
|
@@ -6,6 +6,10 @@ export interface ManagerPromptRegistry {
|
|
|
6
6
|
planSynthesisPrompt: string;
|
|
7
7
|
/** Visible subagent prompt for teamPlanner — thin bridge that calls plan_with_team. */
|
|
8
8
|
teamPlannerPrompt: string;
|
|
9
|
+
/** Visible subagent prompt for browserQa — thin bridge that calls claude tool for browser verification. */
|
|
10
|
+
browserQaAgentPrompt: string;
|
|
11
|
+
/** Prompt prepended to browser verification task prompts in Claude Code sessions. */
|
|
12
|
+
browserQaSessionPrompt: string;
|
|
9
13
|
contextWarnings: {
|
|
10
14
|
moderate: string;
|
|
11
15
|
high: string;
|
|
@@ -13,7 +17,8 @@ export interface ManagerPromptRegistry {
|
|
|
13
17
|
};
|
|
14
18
|
}
|
|
15
19
|
export type SessionMode = 'plan' | 'free';
|
|
16
|
-
export declare const DEFAULT_ENGINEER_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
20
|
+
export declare const DEFAULT_ENGINEER_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex", "BrowserQA"];
|
|
21
|
+
export declare const PLANNER_ELIGIBLE_ENGINEERS: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
17
22
|
export type EngineerName = (typeof DEFAULT_ENGINEER_NAMES)[number];
|
|
18
23
|
export type EngineerWorkMode = 'explore' | 'implement' | 'verify';
|
|
19
24
|
export interface WrapperHistoryEntry {
|
|
@@ -164,6 +169,25 @@ export interface SynthesizedPlanResult {
|
|
|
164
169
|
recommendedQuestion: string | null;
|
|
165
170
|
recommendedAnswer: string | null;
|
|
166
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Per-worker capability config for behavior that differs from the standard engineer path.
|
|
174
|
+
* Keyed by EngineerName in a Partial<Record<EngineerName, WorkerCapabilities>> map;
|
|
175
|
+
* absent entries use default behavior.
|
|
176
|
+
*/
|
|
177
|
+
export interface WorkerCapabilities {
|
|
178
|
+
/** Override the session system prompt for this worker. Absent = use standard engineer prompt. */
|
|
179
|
+
sessionPrompt?: string;
|
|
180
|
+
/** Always restrict write tools regardless of mode. Default: false. */
|
|
181
|
+
restrictWriteTools?: boolean;
|
|
182
|
+
/** Skip mode instructions in the task prompt. Default: false. */
|
|
183
|
+
skipModeInstructions?: boolean;
|
|
184
|
+
/** Allow this worker in plan_with_team. Absent or true = eligible. False = excluded. */
|
|
185
|
+
plannerEligible?: boolean;
|
|
186
|
+
/** Returns true if the final output indicates the required runtime is unavailable. */
|
|
187
|
+
isRuntimeUnavailableResponse?: (finalText: string) => boolean;
|
|
188
|
+
/** Metadata title for the runtime-unavailable event. */
|
|
189
|
+
runtimeUnavailableTitle?: string;
|
|
190
|
+
}
|
|
167
191
|
export interface GitDiffResult {
|
|
168
192
|
hasDiff: boolean;
|
|
169
193
|
diffText: string;
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export const DEFAULT_ENGINEER_NAMES = ['Tom', 'John', 'Maya', 'Sara', 'Alex'];
|
|
1
|
+
export const DEFAULT_ENGINEER_NAMES = ['Tom', 'John', 'Maya', 'Sara', 'Alex', 'BrowserQA'];
|
|
2
|
+
export const PLANNER_ELIGIBLE_ENGINEERS = ['Tom', 'John', 'Maya', 'Sara', 'Alex'];
|
|
@@ -7,10 +7,7 @@ export declare class TeamStateStore {
|
|
|
7
7
|
getTeam(cwd: string, teamId: string): Promise<TeamRecord | null>;
|
|
8
8
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
9
9
|
updateTeam(cwd: string, teamId: string, update: (team: TeamRecord) => TeamRecord): Promise<TeamRecord>;
|
|
10
|
-
getActiveTeam(cwd: string): Promise<string | null>;
|
|
11
|
-
setActiveTeam(cwd: string, teamId: string): Promise<void>;
|
|
12
10
|
private getTeamKey;
|
|
13
|
-
private getActiveTeamPath;
|
|
14
11
|
private getTeamsDirectory;
|
|
15
12
|
private getTeamPath;
|
|
16
13
|
private enqueueWrite;
|
|
@@ -62,31 +62,9 @@ export class TeamStateStore {
|
|
|
62
62
|
return updated;
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
async getActiveTeam(cwd) {
|
|
66
|
-
const filePath = this.getActiveTeamPath(cwd);
|
|
67
|
-
try {
|
|
68
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
69
|
-
const parsed = JSON.parse(content);
|
|
70
|
-
return parsed.teamId ?? null;
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
if (isFileNotFoundError(error)) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async setActiveTeam(cwd, teamId) {
|
|
80
|
-
const filePath = this.getActiveTeamPath(cwd);
|
|
81
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
82
|
-
await writeJsonAtomically(filePath, { teamId });
|
|
83
|
-
}
|
|
84
65
|
getTeamKey(cwd, teamId) {
|
|
85
66
|
return `${cwd}:${teamId}`;
|
|
86
67
|
}
|
|
87
|
-
getActiveTeamPath(cwd) {
|
|
88
|
-
return path.join(cwd, this.baseDirectoryName, 'active-team.json');
|
|
89
|
-
}
|
|
90
68
|
getTeamsDirectory(cwd) {
|
|
91
69
|
return path.join(cwd, this.baseDirectoryName, 'teams');
|
|
92
70
|
}
|
package/dist/team/roster.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type EngineerName, type TeamEngineerRecord, type TeamRecord } from '../types/contracts.js';
|
|
2
|
-
export declare const TEAM_ENGINEERS: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
1
|
+
import { PLANNER_ELIGIBLE_ENGINEERS, type EngineerName, type TeamEngineerRecord, type TeamRecord } from '../types/contracts.js';
|
|
2
|
+
export declare const TEAM_ENGINEERS: readonly ["Tom", "John", "Maya", "Sara", "Alex", "BrowserQA"];
|
|
3
|
+
export { PLANNER_ELIGIBLE_ENGINEERS };
|
|
3
4
|
export declare function isEngineerName(value: string): value is EngineerName;
|
|
4
5
|
export declare function createEmptyTeamRecord(teamId: string, cwd: string): TeamRecord;
|
|
5
6
|
export declare function createEmptyEngineerRecord(name: EngineerName): TeamEngineerRecord;
|
package/dist/team/roster.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { DEFAULT_ENGINEER_NAMES, } from '../types/contracts.js';
|
|
1
|
+
import { DEFAULT_ENGINEER_NAMES, PLANNER_ELIGIBLE_ENGINEERS, } from '../types/contracts.js';
|
|
2
2
|
export const TEAM_ENGINEERS = DEFAULT_ENGINEER_NAMES;
|
|
3
|
+
export { PLANNER_ELIGIBLE_ENGINEERS };
|
|
3
4
|
export function isEngineerName(value) {
|
|
4
5
|
return TEAM_ENGINEERS.includes(value);
|
|
5
6
|
}
|