@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.
Files changed (53) hide show
  1. package/dist/manager/team-orchestrator.d.ts +3 -2
  2. package/dist/manager/team-orchestrator.js +32 -9
  3. package/dist/plugin/agent-hierarchy.d.ts +1 -54
  4. package/dist/plugin/agent-hierarchy.js +2 -123
  5. package/dist/plugin/agents/browser-qa.d.ts +14 -0
  6. package/dist/plugin/agents/browser-qa.js +27 -0
  7. package/dist/plugin/agents/common.d.ts +37 -0
  8. package/dist/plugin/agents/common.js +59 -0
  9. package/dist/plugin/agents/cto.d.ts +9 -0
  10. package/dist/plugin/agents/cto.js +39 -0
  11. package/dist/plugin/agents/engineers.d.ts +9 -0
  12. package/dist/plugin/agents/engineers.js +11 -0
  13. package/dist/plugin/agents/index.d.ts +6 -0
  14. package/dist/plugin/agents/index.js +5 -0
  15. package/dist/plugin/agents/team-planner.d.ts +10 -0
  16. package/dist/plugin/agents/team-planner.js +23 -0
  17. package/dist/plugin/claude-manager.plugin.js +45 -23
  18. package/dist/plugin/service-factory.d.ts +4 -3
  19. package/dist/plugin/service-factory.js +4 -1
  20. package/dist/prompts/registry.js +37 -2
  21. package/dist/src/manager/team-orchestrator.d.ts +3 -2
  22. package/dist/src/manager/team-orchestrator.js +32 -9
  23. package/dist/src/plugin/agent-hierarchy.d.ts +1 -54
  24. package/dist/src/plugin/agent-hierarchy.js +2 -123
  25. package/dist/src/plugin/agents/browser-qa.d.ts +14 -0
  26. package/dist/src/plugin/agents/browser-qa.js +27 -0
  27. package/dist/src/plugin/agents/common.d.ts +37 -0
  28. package/dist/src/plugin/agents/common.js +59 -0
  29. package/dist/src/plugin/agents/cto.d.ts +9 -0
  30. package/dist/src/plugin/agents/cto.js +39 -0
  31. package/dist/src/plugin/agents/engineers.d.ts +9 -0
  32. package/dist/src/plugin/agents/engineers.js +11 -0
  33. package/dist/src/plugin/agents/index.d.ts +6 -0
  34. package/dist/src/plugin/agents/index.js +5 -0
  35. package/dist/src/plugin/agents/team-planner.d.ts +10 -0
  36. package/dist/src/plugin/agents/team-planner.js +23 -0
  37. package/dist/src/plugin/claude-manager.plugin.js +45 -23
  38. package/dist/src/plugin/service-factory.d.ts +4 -3
  39. package/dist/src/plugin/service-factory.js +4 -1
  40. package/dist/src/prompts/registry.js +37 -2
  41. package/dist/src/team/roster.d.ts +3 -2
  42. package/dist/src/team/roster.js +2 -1
  43. package/dist/src/types/contracts.d.ts +25 -1
  44. package/dist/src/types/contracts.js +2 -1
  45. package/dist/team/roster.d.ts +3 -2
  46. package/dist/team/roster.js +2 -1
  47. package/dist/test/claude-manager.plugin.test.js +60 -0
  48. package/dist/test/prompt-registry.test.js +15 -0
  49. package/dist/test/report-claude-event.test.js +44 -3
  50. package/dist/test/team-orchestrator.test.js +47 -8
  51. package/dist/types/contracts.d.ts +25 -1
  52. package/dist/types/contracts.js +2 -1
  53. package/package.json +1 -1
@@ -0,0 +1,37 @@
1
+ export declare const AGENT_CTO = "cto";
2
+ export declare const AGENT_TEAM_PLANNER = "team-planner";
3
+ export declare const AGENT_BROWSER_QA = "browser-qa";
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
+ readonly BrowserQA: "browser-qa";
11
+ };
12
+ /** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
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", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
15
+ export declare const ENGINEER_TOOL_IDS: readonly ["claude"];
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
+ export type ToolPermission = 'allow' | 'ask' | 'deny';
18
+ export type AgentPermission = {
19
+ '*'?: ToolPermission;
20
+ read?: ToolPermission;
21
+ grep?: ToolPermission;
22
+ glob?: ToolPermission;
23
+ list?: ToolPermission;
24
+ codesearch?: ToolPermission;
25
+ webfetch?: ToolPermission;
26
+ websearch?: ToolPermission;
27
+ lsp?: ToolPermission;
28
+ todowrite?: ToolPermission;
29
+ todoread?: ToolPermission;
30
+ question?: ToolPermission;
31
+ task?: ToolPermission | Record<string, ToolPermission>;
32
+ bash?: ToolPermission | Record<string, ToolPermission>;
33
+ [tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
34
+ };
35
+ export declare const CTO_READONLY_TOOLS: Record<string, ToolPermission>;
36
+ export declare function buildEngineerPermissions(): AgentPermission;
37
+ export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
@@ -0,0 +1,59 @@
1
+ import { PLANNER_ELIGIBLE_ENGINEERS } from '../../team/roster.js';
2
+ export const AGENT_CTO = 'cto';
3
+ export const AGENT_TEAM_PLANNER = 'team-planner';
4
+ export const AGENT_BROWSER_QA = 'browser-qa';
5
+ export const ENGINEER_AGENT_IDS = {
6
+ Tom: 'tom',
7
+ John: 'john',
8
+ Maya: 'maya',
9
+ Sara: 'sara',
10
+ Alex: 'alex',
11
+ BrowserQA: 'browser-qa',
12
+ };
13
+ /** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
14
+ export const ENGINEER_AGENT_NAMES = PLANNER_ELIGIBLE_ENGINEERS;
15
+ export const CTO_ONLY_TOOL_IDS = [
16
+ 'team_status',
17
+ 'reset_engineer',
18
+ 'list_transcripts',
19
+ 'list_history',
20
+ 'git_diff',
21
+ 'git_commit',
22
+ 'git_reset',
23
+ 'git_status',
24
+ 'git_log',
25
+ 'approval_policy',
26
+ 'approval_decisions',
27
+ 'approval_update',
28
+ ];
29
+ export const ENGINEER_TOOL_IDS = ['claude'];
30
+ export const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
31
+ export const CTO_READONLY_TOOLS = {
32
+ read: 'allow',
33
+ grep: 'allow',
34
+ glob: 'allow',
35
+ list: 'allow',
36
+ codesearch: 'allow',
37
+ webfetch: 'allow',
38
+ websearch: 'allow',
39
+ lsp: 'allow',
40
+ todowrite: 'allow',
41
+ todoread: 'allow',
42
+ question: 'allow',
43
+ };
44
+ export function buildEngineerPermissions() {
45
+ const denied = {};
46
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
47
+ denied[toolId] = 'deny';
48
+ }
49
+ return {
50
+ '*': 'deny',
51
+ ...denied,
52
+ claude: 'allow',
53
+ };
54
+ }
55
+ export function denyRestrictedToolsGlobally(permissions) {
56
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
57
+ permissions[toolId] ??= 'deny';
58
+ }
59
+ }
@@ -0,0 +1,9 @@
1
+ import type { ManagerPromptRegistry } from '../../types/contracts.js';
2
+ import type { AgentPermission } from './common.js';
3
+ export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
4
+ description: string;
5
+ mode: "primary";
6
+ color: string;
7
+ permission: AgentPermission;
8
+ prompt: string;
9
+ };
@@ -0,0 +1,39 @@
1
+ import { AGENT_BROWSER_QA, AGENT_TEAM_PLANNER, ALL_RESTRICTED_TOOL_IDS, CTO_ONLY_TOOL_IDS, CTO_READONLY_TOOLS, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './common.js';
2
+ function buildCtoPermissions() {
3
+ const denied = {};
4
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
5
+ denied[toolId] = 'deny';
6
+ }
7
+ const allowed = {};
8
+ for (const toolId of CTO_ONLY_TOOL_IDS) {
9
+ allowed[toolId] = 'allow';
10
+ }
11
+ const taskPermissions = { '*': 'deny' };
12
+ for (const engineer of ENGINEER_AGENT_NAMES) {
13
+ const agentId = ENGINEER_AGENT_IDS[engineer];
14
+ // Support both uppercase (user-friendly) and lowercase (canonical) agent IDs.
15
+ // This ensures both task({ subagent_type: 'Tom' }) and task({ subagent_type: 'tom' }) work.
16
+ taskPermissions[engineer] = 'allow'; // 'Tom', 'John', etc.
17
+ taskPermissions[agentId] = 'allow'; // 'tom', 'john', etc.
18
+ }
19
+ // BrowserQA is a specialist registered separately from ENGINEER_AGENT_NAMES; add both forms.
20
+ taskPermissions['BrowserQA'] = 'allow'; // uppercase (user-friendly)
21
+ taskPermissions[AGENT_BROWSER_QA] = 'allow'; // 'browser-qa' canonical
22
+ taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
23
+ return {
24
+ '*': 'deny',
25
+ ...CTO_READONLY_TOOLS,
26
+ ...denied,
27
+ ...allowed,
28
+ task: taskPermissions,
29
+ };
30
+ }
31
+ export function buildCtoAgentConfig(prompts) {
32
+ return {
33
+ description: 'Principal engineer who orchestrates AI-powered engineers. Decomposes work, asks clarifying questions, delegates precisely, reviews diffs, and owns the outcome.',
34
+ mode: 'primary',
35
+ color: '#D97757',
36
+ permission: buildCtoPermissions(),
37
+ prompt: prompts.ctoSystemPrompt,
38
+ };
39
+ }
@@ -0,0 +1,9 @@
1
+ import type { EngineerName, ManagerPromptRegistry } from '../../types/contracts.js';
2
+ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry, engineer: EngineerName): {
3
+ description: string;
4
+ mode: "subagent";
5
+ hidden: boolean;
6
+ color: string;
7
+ permission: import("./common.js").AgentPermission;
8
+ prompt: string;
9
+ };
@@ -0,0 +1,11 @@
1
+ import { buildEngineerPermissions } from './common.js';
2
+ export function buildEngineerAgentConfig(prompts, engineer) {
3
+ return {
4
+ 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).`,
5
+ mode: 'subagent',
6
+ hidden: false,
7
+ color: '#D97757',
8
+ permission: buildEngineerPermissions(),
9
+ prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
10
+ };
11
+ }
@@ -0,0 +1,6 @@
1
+ export { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, ALL_RESTRICTED_TOOL_IDS, buildEngineerPermissions, CTO_ONLY_TOOL_IDS, CTO_READONLY_TOOLS, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, ENGINEER_TOOL_IDS, } from './common.js';
2
+ export type { AgentPermission, ToolPermission } from './common.js';
3
+ export { buildCtoAgentConfig } from './cto.js';
4
+ export { buildTeamPlannerAgentConfig } from './team-planner.js';
5
+ export { buildEngineerAgentConfig } from './engineers.js';
6
+ export { buildBrowserQaAgentConfig, buildWorkerCapabilities } from './browser-qa.js';
@@ -0,0 +1,5 @@
1
+ export { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, ALL_RESTRICTED_TOOL_IDS, buildEngineerPermissions, CTO_ONLY_TOOL_IDS, CTO_READONLY_TOOLS, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, ENGINEER_TOOL_IDS, } from './common.js';
2
+ export { buildCtoAgentConfig } from './cto.js';
3
+ export { buildTeamPlannerAgentConfig } from './team-planner.js';
4
+ export { buildEngineerAgentConfig } from './engineers.js';
5
+ export { buildBrowserQaAgentConfig, buildWorkerCapabilities } from './browser-qa.js';
@@ -0,0 +1,10 @@
1
+ import type { ManagerPromptRegistry } from '../../types/contracts.js';
2
+ import type { AgentPermission } from './common.js';
3
+ export declare function buildTeamPlannerAgentConfig(prompts: ManagerPromptRegistry): {
4
+ description: string;
5
+ mode: "subagent";
6
+ hidden: boolean;
7
+ color: string;
8
+ permission: AgentPermission;
9
+ prompt: string;
10
+ };
@@ -0,0 +1,23 @@
1
+ import { ALL_RESTRICTED_TOOL_IDS } from './common.js';
2
+ function buildTeamPlannerPermissions() {
3
+ const denied = {};
4
+ for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
5
+ denied[toolId] = 'deny';
6
+ }
7
+ return {
8
+ '*': 'deny',
9
+ plan_with_team: 'allow',
10
+ question: 'allow',
11
+ ...denied,
12
+ };
13
+ }
14
+ export function buildTeamPlannerAgentConfig(prompts) {
15
+ return {
16
+ description: 'Runs dual-engineer planning by calling plan_with_team. Automatically selects two non-overlapping available engineers if engineer names are not provided.',
17
+ mode: 'subagent',
18
+ hidden: false,
19
+ color: '#D97757',
20
+ permission: buildTeamPlannerPermissions(),
21
+ prompt: prompts.teamPlannerPrompt,
22
+ };
23
+ }
@@ -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, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
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
- const existing = getWrapperSessionMapping(worktree, input.sessionID);
54
- const persisted = existing ??
55
- (await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
56
- if (!persisted) {
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, persisted.teamId, persisted.engineer);
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 this named engineer's persistent Claude Code session. The session remembers prior turns for this engineer.",
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, engineer, event) {
521
+ function reportClaudeEvent(context, workerName, event) {
522
+ const baseMetadata = { workerName, engineer: workerName };
501
523
  if (event.type === 'error') {
502
524
  context.metadata({
503
- title: `❌ ${engineer} hit an error`,
525
+ title: `❌ ${workerName} hit an error`,
504
526
  metadata: {
505
- engineer,
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: `ℹ️ ${engineer}: ${event.text}`,
536
+ title: `ℹ️ ${workerName}: ${event.text}`,
515
537
  metadata: {
516
- engineer,
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: `⚡ ${engineer} session ready`,
546
+ title: `⚡ ${workerName} session ready`,
525
547
  metadata: {
526
- engineer,
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
- ? `⚡ ${engineer} → ${toolDescription}`
582
+ ? `⚡ ${workerName} → ${toolDescription}`
561
583
  : toolName
562
- ? `⚡ ${engineer} → ${toolName}`
563
- : `⚡ ${engineer} is using Claude Code tools`,
584
+ ? `⚡ ${workerName} → ${toolName}`
585
+ : `⚡ ${workerName} is using Claude Code tools`,
564
586
  metadata: {
565
- engineer,
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: `⚡ ${engineer} ${stateLabel}`,
600
+ title: `⚡ ${workerName} ${stateLabel}`,
579
601
  metadata: {
580
- engineer,
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
- engineer: EngineerName;
23
+ workerName: EngineerName;
23
24
  }): void;
24
25
  export declare function getWrapperSessionMapping(worktree: string, wrapperSessionId: string): {
25
26
  teamId: string;
26
- engineer: EngineerName;
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 orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.planSynthesisPrompt);
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;
@@ -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
- '- Each assignment includes: goal, acceptance criteria, relevant files/areas, constraints, and verification method.',
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 research freely.',
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.',
@@ -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;
@@ -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'];
@@ -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;
@@ -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
  }