@doingdev/opencode-claude-manager-plugin 0.1.56 → 0.1.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/manager/team-orchestrator.d.ts +13 -5
- package/dist/manager/team-orchestrator.js +134 -15
- 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 +68 -32
- package/dist/plugin/service-factory.d.ts +4 -3
- package/dist/plugin/service-factory.js +4 -1
- package/dist/prompts/registry.js +142 -57
- package/dist/src/manager/team-orchestrator.d.ts +13 -5
- package/dist/src/manager/team-orchestrator.js +134 -15
- 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 +68 -32
- package/dist/src/plugin/service-factory.d.ts +4 -3
- package/dist/src/plugin/service-factory.js +4 -1
- package/dist/src/prompts/registry.js +142 -57
- package/dist/src/team/roster.d.ts +3 -2
- package/dist/src/team/roster.js +2 -1
- package/dist/src/types/contracts.d.ts +26 -2
- package/dist/src/types/contracts.js +2 -1
- package/dist/team/roster.d.ts +3 -2
- package/dist/team/roster.js +2 -1
- package/dist/test/claude-manager.plugin.test.js +70 -0
- package/dist/test/prompt-registry.test.js +31 -6
- package/dist/test/report-claude-event.test.js +57 -3
- package/dist/test/team-orchestrator.test.js +155 -5
- package/dist/types/contracts.d.ts +26 -2
- package/dist/types/contracts.js +2 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -2,10 +2,14 @@ export interface ManagerPromptRegistry {
|
|
|
2
2
|
ctoSystemPrompt: string;
|
|
3
3
|
engineerAgentPrompt: string;
|
|
4
4
|
engineerSessionPrompt: string;
|
|
5
|
-
/** Prompt
|
|
5
|
+
/** Prompt prepended to the user prompt of the synthesis runTask call inside plan_with_team. */
|
|
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/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
|
}
|
|
@@ -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
|
});
|
|
@@ -199,4 +201,72 @@ describe('Agent ID normalization and lookup helpers', () => {
|
|
|
199
201
|
expect(isEngineerAgent('cto')).toBe(false);
|
|
200
202
|
expect(isEngineerAgent('team-planner')).toBe(false);
|
|
201
203
|
});
|
|
204
|
+
it('CTO agent config does not have direct assign_engineer access (delegates to named engineers)', async () => {
|
|
205
|
+
const { buildCtoAgentConfig } = await import('../src/plugin/agent-hierarchy.js');
|
|
206
|
+
const { managerPromptRegistry } = await import('../src/prompts/registry.js');
|
|
207
|
+
const ctoConfig = buildCtoAgentConfig(managerPromptRegistry);
|
|
208
|
+
const ctoPermissions = ctoConfig.permission;
|
|
209
|
+
// CTO should NOT have direct access to assign_engineer (uses task() to named engineers instead)
|
|
210
|
+
expect(ctoPermissions['assign_engineer']).not.toBe('allow');
|
|
211
|
+
// CTO should NOT have direct access to plan_with_team (must delegate to team-planner)
|
|
212
|
+
expect(ctoPermissions['plan_with_team']).not.toBe('allow');
|
|
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
|
+
});
|
|
202
272
|
});
|
|
@@ -3,11 +3,13 @@ import { managerPromptRegistry } from '../src/prompts/registry.js';
|
|
|
3
3
|
describe('managerPromptRegistry', () => {
|
|
4
4
|
it('gives the CTO explicit orchestration guidance', () => {
|
|
5
5
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('You are a principal engineer orchestrating a team of AI-powered engineers');
|
|
6
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('
|
|
7
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('
|
|
6
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Operating Loop');
|
|
7
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('named engineer');
|
|
8
8
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('team-planner');
|
|
9
9
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('question');
|
|
10
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('
|
|
10
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Review: Inspect diffs for production safety');
|
|
11
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('race condition');
|
|
12
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('contextExhausted');
|
|
11
13
|
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('clear_session');
|
|
12
14
|
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('freshSession');
|
|
13
15
|
});
|
|
@@ -21,8 +23,10 @@ describe('managerPromptRegistry', () => {
|
|
|
21
23
|
it('keeps the engineer session prompt direct and repo-aware', () => {
|
|
22
24
|
expect(managerPromptRegistry.engineerSessionPrompt).toContain('expert software engineer');
|
|
23
25
|
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Start with the smallest investigation that resolves the key uncertainty');
|
|
24
|
-
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Verify your
|
|
26
|
+
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Verify your work before reporting done');
|
|
25
27
|
expect(managerPromptRegistry.engineerSessionPrompt).toContain('Do not run git commit');
|
|
28
|
+
expect(managerPromptRegistry.engineerSessionPrompt).toContain('rollout');
|
|
29
|
+
expect(managerPromptRegistry.engineerSessionPrompt).toContain('backwards compatibility');
|
|
26
30
|
});
|
|
27
31
|
it('keeps context warnings available for engineer sessions', () => {
|
|
28
32
|
expect(managerPromptRegistry.contextWarnings.moderate).toContain('{percent}');
|
|
@@ -36,9 +40,30 @@ describe('managerPromptRegistry', () => {
|
|
|
36
40
|
expect(managerPromptRegistry.planSynthesisPrompt).toContain('## Recommended Question');
|
|
37
41
|
expect(managerPromptRegistry.planSynthesisPrompt).toContain('## Recommended Answer');
|
|
38
42
|
});
|
|
39
|
-
it('teamPlannerPrompt directs the agent to call plan_with_team
|
|
43
|
+
it('teamPlannerPrompt directs the agent to call plan_with_team with autonomous engineer selection', () => {
|
|
40
44
|
expect(managerPromptRegistry.teamPlannerPrompt).toContain('plan_with_team');
|
|
41
|
-
expect(managerPromptRegistry.teamPlannerPrompt).toContain('
|
|
45
|
+
expect(managerPromptRegistry.teamPlannerPrompt).toContain('auto-select');
|
|
42
46
|
expect(managerPromptRegistry.teamPlannerPrompt).toContain('engineer');
|
|
43
47
|
});
|
|
48
|
+
it('ctoSystemPrompt delegates single work to named engineers via task() and dual work to team-planner', () => {
|
|
49
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('task(subagent_type:');
|
|
50
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('single-engineer');
|
|
51
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('team-planner');
|
|
52
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('automatically selects');
|
|
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
|
+
});
|
|
44
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.
|
|
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
|
|
@@ -163,6 +171,19 @@ describe('reportClaudeEvent — via plugin onEvent chain', () => {
|
|
|
163
171
|
expect(call.title).toBe('⚡ Maya → git_status');
|
|
164
172
|
expect(call.metadata.toolArgs).toEqual({});
|
|
165
173
|
});
|
|
174
|
+
it('surfaces status event as visible metadata', async () => {
|
|
175
|
+
const event = {
|
|
176
|
+
type: 'status',
|
|
177
|
+
text: 'Context exhausted; resetting session and retrying once with a fresh session.',
|
|
178
|
+
};
|
|
179
|
+
const { plugin } = await setupPlugin([event]);
|
|
180
|
+
const { metadata, ctx } = makeContext(tempRoot, ENGINEER_AGENT_IDS.Sara, 'wrapper-6');
|
|
181
|
+
await executeClaude(plugin, ctx);
|
|
182
|
+
const statusCall = metadata.mock.calls.find(([c]) => c?.title?.includes('ℹ️'))?.[0];
|
|
183
|
+
expect(statusCall).toBeDefined();
|
|
184
|
+
expect(statusCall.title).toBe('ℹ️ Sara: Context exhausted; resetting session and retrying once with a fresh session.');
|
|
185
|
+
expect(statusCall.metadata.status).toBe('Context exhausted; resetting session and retrying once with a fresh session.');
|
|
186
|
+
});
|
|
166
187
|
});
|
|
167
188
|
// ── Second-invocation continuity ─────────────────────────────────────────────
|
|
168
189
|
describe('second invocation continuity', () => {
|
|
@@ -180,7 +201,7 @@ describe('second invocation continuity', () => {
|
|
|
180
201
|
// ── Phase 1: first task via orchestrator (no real SDK needed) ──────────
|
|
181
202
|
const store = new TeamStateStore();
|
|
182
203
|
await store.setActiveTeam(tempRoot, 'cto-1');
|
|
183
|
-
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 });
|
|
184
205
|
await orchestrator.recordWrapperSession(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1');
|
|
185
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.');
|
|
186
207
|
// ── Phase 2: process restart ───────────────────────────────────────────
|
|
@@ -206,7 +227,7 @@ describe('second invocation continuity', () => {
|
|
|
206
227
|
// ── Phase 1: pre-seed Tom with a claudeSessionId ───────────────────────
|
|
207
228
|
const store = new TeamStateStore();
|
|
208
229
|
await store.setActiveTeam(tempRoot, 'cto-1');
|
|
209
|
-
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 });
|
|
210
231
|
await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
|
|
211
232
|
await store.updateTeam(tempRoot, 'cto-1', (team) => ({
|
|
212
233
|
...team,
|
|
@@ -243,4 +264,37 @@ describe('second invocation continuity', () => {
|
|
|
243
264
|
});
|
|
244
265
|
expect(runTask.mock.calls[0]?.[0].systemPrompt).toBeUndefined(); // no system prompt when resuming
|
|
245
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
|
+
});
|
|
246
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');
|
|
@@ -203,4 +212,145 @@ describe('TeamOrchestrator', () => {
|
|
|
203
212
|
engineer: 'Tom',
|
|
204
213
|
});
|
|
205
214
|
});
|
|
215
|
+
it('planWithTeam auto-selects two distinct engineers when names are omitted', async () => {
|
|
216
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
217
|
+
const runTask = vi.fn(async (_input, _onEvent) => {
|
|
218
|
+
// Return different results for lead vs challenger
|
|
219
|
+
const calls = runTask.mock.calls.length;
|
|
220
|
+
if (calls === 1) {
|
|
221
|
+
return {
|
|
222
|
+
sessionId: 'ses_lead',
|
|
223
|
+
events: [],
|
|
224
|
+
finalText: 'Lead plan',
|
|
225
|
+
turns: 1,
|
|
226
|
+
totalCostUsd: 0.01,
|
|
227
|
+
inputTokens: 100,
|
|
228
|
+
outputTokens: 50,
|
|
229
|
+
contextWindowSize: 200_000,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
else if (calls === 2) {
|
|
233
|
+
return {
|
|
234
|
+
sessionId: 'ses_challenger',
|
|
235
|
+
events: [],
|
|
236
|
+
finalText: 'Challenger plan',
|
|
237
|
+
turns: 1,
|
|
238
|
+
totalCostUsd: 0.01,
|
|
239
|
+
inputTokens: 100,
|
|
240
|
+
outputTokens: 50,
|
|
241
|
+
contextWindowSize: 200_000,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
return {
|
|
246
|
+
sessionId: undefined,
|
|
247
|
+
events: [],
|
|
248
|
+
finalText: '## Synthesis\nBest plan\n## Recommended Question\nNONE\n## Recommended Answer\nNONE',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
253
|
+
const result = await orchestrator.planWithTeam({
|
|
254
|
+
teamId: 'team-1',
|
|
255
|
+
cwd: tempRoot,
|
|
256
|
+
request: 'Plan the refactor',
|
|
257
|
+
// NOTE: both leadEngineer and challengerEngineer are omitted
|
|
258
|
+
});
|
|
259
|
+
expect(result.leadEngineer).toBeDefined();
|
|
260
|
+
expect(result.challengerEngineer).toBeDefined();
|
|
261
|
+
expect(result.leadEngineer).not.toEqual(result.challengerEngineer);
|
|
262
|
+
});
|
|
263
|
+
it('throws error when fewer than 2 viable engineers exist for planning', async () => {
|
|
264
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
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 });
|
|
266
|
+
// Mark all engineers as busy
|
|
267
|
+
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
268
|
+
for (const engineer of team.engineers) {
|
|
269
|
+
await orchestrator['updateEngineer'](tempRoot, 'team-1', engineer.name, (e) => ({
|
|
270
|
+
...e,
|
|
271
|
+
busy: true,
|
|
272
|
+
busySince: new Date().toISOString(),
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
await expect(orchestrator.planWithTeam({
|
|
276
|
+
teamId: 'team-1',
|
|
277
|
+
cwd: tempRoot,
|
|
278
|
+
request: 'Plan something',
|
|
279
|
+
})).rejects.toThrow('Not enough available engineers for dual planning');
|
|
280
|
+
});
|
|
281
|
+
it('context exhaustion retries exactly once with same assignment message and fresh session', async () => {
|
|
282
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
283
|
+
let callCount = 0;
|
|
284
|
+
let lastInputMessage = '';
|
|
285
|
+
const runTask = vi.fn(async (input) => {
|
|
286
|
+
callCount++;
|
|
287
|
+
lastInputMessage = input.prompt ?? '';
|
|
288
|
+
// First call throws context exhaustion, second succeeds
|
|
289
|
+
if (callCount === 1) {
|
|
290
|
+
const error = new Error('Token limit exceeded: context exhausted');
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
sessionId: 'ses_retry',
|
|
295
|
+
events: [],
|
|
296
|
+
finalText: 'Success after retry',
|
|
297
|
+
turns: 1,
|
|
298
|
+
totalCostUsd: 0.02,
|
|
299
|
+
inputTokens: 100,
|
|
300
|
+
outputTokens: 50,
|
|
301
|
+
contextWindowSize: 200_000,
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt', { BrowserQA: BROWSER_QA_TEST_CAPS });
|
|
305
|
+
const allEvents = [];
|
|
306
|
+
const result = await orchestrator.dispatchEngineer({
|
|
307
|
+
teamId: 'team-1',
|
|
308
|
+
cwd: tempRoot,
|
|
309
|
+
engineer: 'Tom',
|
|
310
|
+
mode: 'implement',
|
|
311
|
+
message: 'Fix the bug',
|
|
312
|
+
onEvent: (event) => {
|
|
313
|
+
allEvents.push({ type: event.type, text: event.text });
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
// Verify retry happened exactly once (2 runTask calls total)
|
|
317
|
+
expect(callCount).toBe(2);
|
|
318
|
+
// Verify status event was emitted for context exhaustion
|
|
319
|
+
const statusEvent = allEvents.find((e) => e.type === 'status');
|
|
320
|
+
expect(statusEvent?.text).toContain('Context exhausted');
|
|
321
|
+
// Verify the retry message is the same (contains the original task)
|
|
322
|
+
expect(lastInputMessage).toContain('Fix the bug');
|
|
323
|
+
// Verify success result
|
|
324
|
+
expect(result.finalText).toBe('Success after retry');
|
|
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
|
+
});
|
|
206
356
|
});
|
|
@@ -2,10 +2,14 @@ export interface ManagerPromptRegistry {
|
|
|
2
2
|
ctoSystemPrompt: string;
|
|
3
3
|
engineerAgentPrompt: string;
|
|
4
4
|
engineerSessionPrompt: string;
|
|
5
|
-
/** Prompt
|
|
5
|
+
/** Prompt prepended to the user prompt of the synthesis runTask call inside plan_with_team. */
|
|
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;
|
package/dist/types/contracts.js
CHANGED
|
@@ -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'];
|