@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
@@ -47,6 +47,8 @@ describe('ClaudeManagerPlugin', () => {
47
47
  sara: 'allow',
48
48
  Alex: 'allow',
49
49
  alex: 'allow',
50
+ BrowserQA: 'allow',
51
+ 'browser-qa': 'allow',
50
52
  'team-planner': 'allow',
51
53
  });
52
54
  });
@@ -209,4 +211,62 @@ describe('Agent ID normalization and lookup helpers', () => {
209
211
  // CTO should NOT have direct access to plan_with_team (must delegate to team-planner)
210
212
  expect(ctoPermissions['plan_with_team']).not.toBe('allow');
211
213
  });
214
+ it('browser-qa agent is registered with correct permissions', async () => {
215
+ const plugin = await ClaudeManagerPlugin({
216
+ worktree: '/tmp/project',
217
+ });
218
+ const config = {};
219
+ await plugin.config?.(config);
220
+ const agents = (config.agent ?? {});
221
+ const { AGENT_BROWSER_QA } = await import('../src/plugin/agent-hierarchy.js');
222
+ const browserQa = agents[AGENT_BROWSER_QA];
223
+ expect(browserQa).toBeDefined();
224
+ expect(browserQa.permission).toBeDefined();
225
+ // BrowserQA should have access to claude tool
226
+ expect(browserQa.permission?.['claude']).toBe('allow');
227
+ });
228
+ it('browser-qa session prompt allows Playwright skill/command', async () => {
229
+ const { managerPromptRegistry } = await import('../src/prompts/registry.js');
230
+ expect(managerPromptRegistry.browserQaSessionPrompt).toContain('Playwright skill');
231
+ expect(managerPromptRegistry.browserQaSessionPrompt).toContain('real browser');
232
+ expect(managerPromptRegistry.browserQaSessionPrompt).toContain('Allowed tools');
233
+ });
234
+ it('browser-qa agent prompt mentions PLAYWRIGHT_UNAVAILABLE sentinel', async () => {
235
+ const { managerPromptRegistry } = await import('../src/prompts/registry.js');
236
+ expect(managerPromptRegistry.browserQaAgentPrompt).toContain('PLAYWRIGHT_UNAVAILABLE');
237
+ expect(managerPromptRegistry.browserQaAgentPrompt).toContain('Playwright');
238
+ });
239
+ it('browser-qa agent is registered in ENGINEER_AGENT_IDS', async () => {
240
+ const { AGENT_BROWSER_QA, ENGINEER_AGENT_IDS } = await import('../src/plugin/agent-hierarchy.js');
241
+ // BrowserQA should be in the engineer IDs
242
+ const engineerIds = Object.values(ENGINEER_AGENT_IDS);
243
+ expect(engineerIds).toContain('browser-qa');
244
+ expect(ENGINEER_AGENT_IDS['BrowserQA']).toBe(AGENT_BROWSER_QA);
245
+ });
246
+ it('browser-qa agent config has restricted write access', async () => {
247
+ const plugin = await ClaudeManagerPlugin({
248
+ worktree: '/tmp/project',
249
+ });
250
+ const config = {};
251
+ await plugin.config?.(config);
252
+ const agents = (config.agent ?? {});
253
+ const { AGENT_BROWSER_QA } = await import('../src/plugin/agent-hierarchy.js');
254
+ const browserQa = agents[AGENT_BROWSER_QA];
255
+ expect(browserQa).toBeDefined();
256
+ // Should allow claude tool (engineers can call claude)
257
+ expect(browserQa.permission?.['claude']).toBe('allow');
258
+ // Should deny most other tools
259
+ expect(browserQa.permission?.['read']).toBeUndefined(); // Falls back to deny
260
+ expect(browserQa.permission?.['write']).toBeUndefined(); // Falls back to deny
261
+ });
262
+ it('browserqa agent uses same engineer dispatch path', async () => {
263
+ const plugin = await ClaudeManagerPlugin({
264
+ worktree: '/tmp/project',
265
+ });
266
+ const config = {};
267
+ await plugin.config?.(config);
268
+ const agents = (config.agent ?? {});
269
+ // BrowserQA should be configured as an agent
270
+ expect(agents['browser-qa']).toBeDefined();
271
+ });
212
272
  });
@@ -51,4 +51,19 @@ describe('managerPromptRegistry', () => {
51
51
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('team-planner');
52
52
  expect(managerPromptRegistry.ctoSystemPrompt).toContain('automatically selects');
53
53
  });
54
+ it('ctoSystemPrompt mentions browser-qa for delegation', () => {
55
+ expect(managerPromptRegistry.ctoSystemPrompt).toContain('browser-qa');
56
+ });
57
+ it('browserQaAgentPrompt mentions Playwright and PLAYWRIGHT_UNAVAILABLE sentinel', () => {
58
+ expect(managerPromptRegistry.browserQaAgentPrompt).toContain('Playwright');
59
+ expect(managerPromptRegistry.browserQaAgentPrompt).toContain('PLAYWRIGHT_UNAVAILABLE');
60
+ expect(managerPromptRegistry.browserQaAgentPrompt).toContain('browser QA');
61
+ });
62
+ it('browserQaSessionPrompt mentions Playwright and restricts write tools', () => {
63
+ expect(managerPromptRegistry.browserQaSessionPrompt).toContain('Playwright');
64
+ expect(managerPromptRegistry.browserQaSessionPrompt).toContain('Allowed tools');
65
+ expect(managerPromptRegistry.browserQaSessionPrompt).toContain('browser QA');
66
+ expect(managerPromptRegistry.browserQaSessionPrompt).not.toContain('implement');
67
+ expect(managerPromptRegistry.browserQaSessionPrompt).not.toContain('write code');
68
+ });
54
69
  });
@@ -12,6 +12,14 @@ import { clearPluginServices, getActiveTeamSession, getOrCreatePluginServices, }
12
12
  import { AGENT_CTO, ENGINEER_AGENT_IDS } from '../src/plugin/agent-hierarchy.js';
13
13
  import { TeamStateStore } from '../src/state/team-state-store.js';
14
14
  import { TeamOrchestrator } from '../src/manager/team-orchestrator.js';
15
+ const BROWSER_QA_TEST_CAPS = {
16
+ sessionPrompt: 'Browser QA prompt',
17
+ restrictWriteTools: true,
18
+ skipModeInstructions: true,
19
+ plannerEligible: false,
20
+ isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
21
+ runtimeUnavailableTitle: '❌ Playwright unavailable',
22
+ };
15
23
  function makeContext(worktree, agentId, sessionID) {
16
24
  const metadata = vi.fn();
17
25
  const ctx = {
@@ -100,7 +108,7 @@ describe('reportClaudeEvent — via plugin onEvent chain', () => {
100
108
  expect(call.metadata.toolId).toBe('call-abc');
101
109
  expect(call.metadata.toolArgs).toEqual({ file_path: '/foo.ts' });
102
110
  expect(call.metadata.sessionId).toBe('ses-1');
103
- expect(call.metadata.engineer).toBe('Tom');
111
+ expect(call.metadata.workerName).toBe('Tom');
104
112
  });
105
113
  it('double-decodes a JSON-string input (tool input serialised twice)', async () => {
106
114
  // The SDK adapter may serialize `input` as a JSON string inside the outer JSON on
@@ -193,7 +201,7 @@ describe('second invocation continuity', () => {
193
201
  // ── Phase 1: first task via orchestrator (no real SDK needed) ──────────
194
202
  const store = new TeamStateStore();
195
203
  await store.setActiveTeam(tempRoot, 'cto-1');
196
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt');
204
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
197
205
  await orchestrator.recordWrapperSession(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1');
198
206
  await orchestrator.recordWrapperExchange(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1', 'explore', 'Investigate the auth flow', 'Found two race conditions in the token refresh path.');
199
207
  // ── Phase 2: process restart ───────────────────────────────────────────
@@ -219,7 +227,7 @@ describe('second invocation continuity', () => {
219
227
  // ── Phase 1: pre-seed Tom with a claudeSessionId ───────────────────────
220
228
  const store = new TeamStateStore();
221
229
  await store.setActiveTeam(tempRoot, 'cto-1');
222
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt');
230
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
223
231
  await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
224
232
  await store.updateTeam(tempRoot, 'cto-1', (team) => ({
225
233
  ...team,
@@ -256,4 +264,37 @@ describe('second invocation continuity', () => {
256
264
  });
257
265
  expect(runTask.mock.calls[0]?.[0].systemPrompt).toBeUndefined(); // no system prompt when resuming
258
266
  });
267
+ it('BrowserQA PLAYWRIGHT_UNAVAILABLE returns plainly and emits warning metadata', async () => {
268
+ const plugin = await ClaudeManagerPlugin({ worktree: tempRoot });
269
+ const chatMessage = plugin['chat.message'];
270
+ // Register BrowserQA session
271
+ await chatMessage({ agent: ENGINEER_AGENT_IDS.BrowserQA, sessionID: 'wrapper-browserqa-1' });
272
+ const services = getOrCreatePluginServices(tempRoot);
273
+ // Mock dispatchEngineer to return PLAYWRIGHT_UNAVAILABLE sentinel
274
+ const unavailableMessage = 'PLAYWRIGHT_UNAVAILABLE: Playwright is not available in this environment. Consider installing it via npm.';
275
+ vi.spyOn(services.orchestrator, 'dispatchEngineer').mockResolvedValueOnce(makeDispatchResult({
276
+ engineer: 'BrowserQA',
277
+ finalText: unavailableMessage,
278
+ }));
279
+ vi.spyOn(services.orchestrator, 'recordWrapperExchange').mockResolvedValue(undefined);
280
+ const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.BrowserQA, 'wrapper-browserqa-1');
281
+ const result = await executeClaude(plugin, ctx, {
282
+ mode: 'verify',
283
+ message: 'Check if browser is available',
284
+ });
285
+ // Result should contain the PLAYWRIGHT_UNAVAILABLE text plainly (not wrapped in error)
286
+ expect(result).toContain('PLAYWRIGHT_UNAVAILABLE');
287
+ expect(result).toContain('Playwright is not available');
288
+ // Verify warning metadata was emitted
289
+ expect(metadata).toHaveBeenCalled();
290
+ const calls = metadata.mock.calls;
291
+ const warningCall = calls.find((call) => {
292
+ const arg = call[0];
293
+ const title = arg?.title ?? '';
294
+ return (title.includes('Playwright') ||
295
+ title.includes('unavailable') ||
296
+ title.toLowerCase().includes('playwright unavailable'));
297
+ });
298
+ expect(warningCall).toBeDefined();
299
+ });
259
300
  });
@@ -4,6 +4,15 @@ import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { TeamOrchestrator } from '../src/manager/team-orchestrator.js';
6
6
  import { TeamStateStore } from '../src/state/team-state-store.js';
7
+ // Full BrowserQA capabilities used in all test orchestrator instances
8
+ const BROWSER_QA_TEST_CAPS = {
9
+ sessionPrompt: 'Browser QA prompt',
10
+ restrictWriteTools: true,
11
+ skipModeInstructions: true,
12
+ plannerEligible: false,
13
+ isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
14
+ runtimeUnavailableTitle: '❌ Playwright unavailable',
15
+ };
7
16
  describe('TeamOrchestrator', () => {
8
17
  let tempRoot;
9
18
  afterEach(async () => {
@@ -35,7 +44,7 @@ describe('TeamOrchestrator', () => {
35
44
  outputTokens: 300,
36
45
  contextWindowSize: 200_000,
37
46
  });
38
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
47
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
39
48
  const first = await orchestrator.dispatchEngineer({
40
49
  teamId: 'team-1',
41
50
  cwd: tempRoot,
@@ -79,7 +88,7 @@ describe('TeamOrchestrator', () => {
79
88
  it('rejects work when the same engineer is already busy', async () => {
80
89
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
81
90
  const store = new TeamStateStore('.state');
82
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
91
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
83
92
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
84
93
  await store.saveTeam({
85
94
  ...team,
@@ -117,7 +126,7 @@ describe('TeamOrchestrator', () => {
117
126
  events: [],
118
127
  finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
119
128
  });
120
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
129
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
121
130
  const result = await orchestrator.planWithTeam({
122
131
  teamId: 'team-1',
123
132
  cwd: tempRoot,
@@ -166,7 +175,7 @@ describe('TeamOrchestrator', () => {
166
175
  };
167
176
  }
168
177
  });
169
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
178
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
170
179
  const onLeadEvent = vi.fn();
171
180
  const onChallengerEvent = vi.fn();
172
181
  const onSynthesisEvent = vi.fn();
@@ -186,7 +195,7 @@ describe('TeamOrchestrator', () => {
186
195
  });
187
196
  it('persists wrapper session memory for an engineer', async () => {
188
197
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
189
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
198
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
190
199
  await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
191
200
  await orchestrator.recordWrapperExchange(tempRoot, 'team-1', 'Tom', 'wrapper-tom', 'explore', 'Investigate the auth flow and compare approaches', 'The auth flow uses one shared validator and the cookie refresh path is the main risk.');
192
201
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
@@ -240,7 +249,7 @@ describe('TeamOrchestrator', () => {
240
249
  };
241
250
  }
242
251
  });
243
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
252
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
244
253
  const result = await orchestrator.planWithTeam({
245
254
  teamId: 'team-1',
246
255
  cwd: tempRoot,
@@ -253,7 +262,7 @@ describe('TeamOrchestrator', () => {
253
262
  });
254
263
  it('throws error when fewer than 2 viable engineers exist for planning', async () => {
255
264
  tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
256
- const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
265
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
257
266
  // Mark all engineers as busy
258
267
  const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
259
268
  for (const engineer of team.engineers) {
@@ -292,7 +301,7 @@ describe('TeamOrchestrator', () => {
292
301
  contextWindowSize: 200_000,
293
302
  };
294
303
  });
295
- const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
304
+ const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
296
305
  const allEvents = [];
297
306
  const result = await orchestrator.dispatchEngineer({
298
307
  teamId: 'team-1',
@@ -314,4 +323,34 @@ describe('TeamOrchestrator', () => {
314
323
  // Verify success result
315
324
  expect(result.finalText).toBe('Success after retry');
316
325
  });
326
+ it('rejects BrowserQA with implement mode', async () => {
327
+ tempRoot = await mkdtemp(join(tmpdir(), 'browserqa-implement-'));
328
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
329
+ const error = await orchestrator
330
+ .dispatchEngineer({
331
+ teamId: 'team-1',
332
+ cwd: tempRoot,
333
+ engineer: 'BrowserQA',
334
+ mode: 'implement',
335
+ message: 'Write a feature',
336
+ })
337
+ .catch((e) => e);
338
+ expect(error).toBeInstanceOf(Error);
339
+ expect(error.message).toContain('BrowserQA is a browser QA specialist');
340
+ expect(error.message).toContain('does not support implement mode');
341
+ });
342
+ it('selectPlanEngineers excludes BrowserQA from planner selection', async () => {
343
+ tempRoot = await mkdtemp(join(tmpdir(), 'planner-exclude-browserqa-'));
344
+ const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
345
+ // Create a team with all engineers
346
+ await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
347
+ // Select plan engineers
348
+ const selection = await orchestrator.selectPlanEngineers(tempRoot, 'team-1');
349
+ // Both lead and challenger should be from general engineers only
350
+ const generalEngineers = ['Tom', 'John', 'Maya', 'Sara', 'Alex'];
351
+ expect(generalEngineers).toContain(selection.lead);
352
+ expect(generalEngineers).toContain(selection.challenger);
353
+ expect(selection.lead).not.toBe('BrowserQA');
354
+ expect(selection.challenger).not.toBe('BrowserQA');
355
+ });
317
356
  });
@@ -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'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",