@doingdev/opencode-claude-manager-plugin 0.1.55 → 0.1.56
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.js +4 -9
- package/dist/plugin/agent-hierarchy.js +5 -1
- package/dist/plugin/claude-manager.plugin.d.ts +8 -0
- package/dist/plugin/claude-manager.plugin.js +13 -4
- package/dist/prompts/registry.js +1 -1
- package/dist/src/manager/team-orchestrator.d.ts +2 -2
- package/dist/src/manager/team-orchestrator.js +7 -12
- package/dist/src/plugin/agent-hierarchy.d.ts +2 -2
- package/dist/src/plugin/agent-hierarchy.js +14 -19
- package/dist/src/plugin/claude-manager.plugin.d.ts +8 -0
- package/dist/src/plugin/claude-manager.plugin.js +26 -16
- package/dist/src/plugin/service-factory.js +1 -1
- package/dist/src/prompts/registry.js +11 -3
- package/dist/src/types/contracts.d.ts +4 -1
- package/dist/test/claude-manager.plugin.test.js +84 -13
- package/dist/test/prompt-registry.test.js +12 -8
- package/dist/test/report-claude-event.test.js +3 -3
- package/dist/test/team-orchestrator.test.js +16 -7
- package/package.json +1 -1
|
@@ -106,10 +106,9 @@ export class TeamOrchestrator {
|
|
|
106
106
|
}
|
|
107
107
|
const result = await this.sessions.runTask({
|
|
108
108
|
cwd: input.cwd,
|
|
109
|
-
prompt:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: this.buildSessionSystemPrompt(input.engineer, input.mode),
|
|
109
|
+
prompt: engineerState.claudeSessionId
|
|
110
|
+
? this.buildEngineerPrompt(input.mode, input.message)
|
|
111
|
+
: `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message)}`,
|
|
113
112
|
resumeSessionId: engineerState.claudeSessionId ?? undefined,
|
|
114
113
|
persistSession: true,
|
|
115
114
|
includePartialMessages: true,
|
|
@@ -224,8 +223,7 @@ export class TeamOrchestrator {
|
|
|
224
223
|
];
|
|
225
224
|
const synthesisResult = await this.sessions.runTask({
|
|
226
225
|
cwd: input.cwd,
|
|
227
|
-
prompt: buildSynthesisPrompt(input.request, drafts)
|
|
228
|
-
systemPrompt: buildSynthesisSystemPrompt(this.planSynthesisPrompt),
|
|
226
|
+
prompt: `${this.planSynthesisPrompt}\n\n${buildSynthesisPrompt(input.request, drafts)}`,
|
|
229
227
|
persistSession: false,
|
|
230
228
|
includePartialMessages: false,
|
|
231
229
|
permissionMode: 'acceptEdits',
|
|
@@ -365,9 +363,6 @@ function buildPlanDraftRequest(perspective, request) {
|
|
|
365
363
|
`User request: ${request}`,
|
|
366
364
|
].join('\n');
|
|
367
365
|
}
|
|
368
|
-
function buildSynthesisSystemPrompt(planSynthesisPrompt) {
|
|
369
|
-
return planSynthesisPrompt;
|
|
370
|
-
}
|
|
371
366
|
function buildSynthesisPrompt(request, drafts) {
|
|
372
367
|
return [
|
|
373
368
|
`User request: ${request}`,
|
|
@@ -61,7 +61,11 @@ function buildCtoPermissions() {
|
|
|
61
61
|
}
|
|
62
62
|
const taskPermissions = { '*': 'deny' };
|
|
63
63
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
64
|
-
|
|
64
|
+
const agentId = ENGINEER_AGENT_IDS[engineer];
|
|
65
|
+
// Support both uppercase (user-friendly) and lowercase (canonical) agent IDs.
|
|
66
|
+
// This ensures both task({ subagent_type: 'Tom' }) and task({ subagent_type: 'tom' }) work.
|
|
67
|
+
taskPermissions[engineer] = 'allow'; // 'Tom', 'John', etc.
|
|
68
|
+
taskPermissions[agentId] = 'allow'; // 'tom', 'john', etc.
|
|
65
69
|
}
|
|
66
70
|
taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
|
|
67
71
|
return {
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import { type Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
import type { EngineerName } from '../types/contracts.js';
|
|
2
3
|
export declare const ClaudeManagerPlugin: Plugin;
|
|
4
|
+
/**
|
|
5
|
+
* Normalize an agent ID to its lowercase canonical form.
|
|
6
|
+
* Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeAgentId(agentId: string): string;
|
|
9
|
+
export declare function engineerFromAgent(agentId: string): EngineerName;
|
|
10
|
+
export declare function isEngineerAgent(agentId: string): boolean;
|
|
@@ -411,16 +411,25 @@ async function runEngineerAssignment(input, context) {
|
|
|
411
411
|
});
|
|
412
412
|
return result;
|
|
413
413
|
}
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Normalize an agent ID to its lowercase canonical form.
|
|
416
|
+
* Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
|
|
417
|
+
*/
|
|
418
|
+
export function normalizeAgentId(agentId) {
|
|
419
|
+
return agentId.toLowerCase();
|
|
420
|
+
}
|
|
421
|
+
export function engineerFromAgent(agentId) {
|
|
422
|
+
const normalized = normalizeAgentId(agentId);
|
|
423
|
+
const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === normalized);
|
|
416
424
|
const engineer = engineerEntry?.[0];
|
|
417
425
|
if (!engineer || !isEngineerName(engineer)) {
|
|
418
426
|
throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
|
|
419
427
|
}
|
|
420
428
|
return engineer;
|
|
421
429
|
}
|
|
422
|
-
function isEngineerAgent(agentId) {
|
|
423
|
-
|
|
430
|
+
export function isEngineerAgent(agentId) {
|
|
431
|
+
const normalized = normalizeAgentId(agentId);
|
|
432
|
+
return Object.values(ENGINEER_AGENT_IDS).some((id) => id === normalized);
|
|
424
433
|
}
|
|
425
434
|
/**
|
|
426
435
|
* Resolves the team ID for an engineer session.
|
package/dist/prompts/registry.js
CHANGED
|
@@ -27,7 +27,7 @@ export const managerPromptRegistry = {
|
|
|
27
27
|
'',
|
|
28
28
|
'Plan and decompose:',
|
|
29
29
|
'- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
|
|
30
|
-
|
|
30
|
+
"- For medium or large tasks, delegate dual-engineer exploration and synthesis to the `team-planner` subagent: use `task(subagent_type: 'team-planner', ...)`. When tasking engineer subagents directly, use lowercase subagent IDs: tom, john, maya, sara, alex.",
|
|
31
31
|
'- Define clear success criteria before delegating. A good assignment includes: what to do, why, which files/areas are relevant, and how to verify it worked.',
|
|
32
32
|
'',
|
|
33
33
|
'Delegate through the Task tool:',
|
|
@@ -18,8 +18,8 @@ export declare class TeamOrchestrator {
|
|
|
18
18
|
private readonly teamStore;
|
|
19
19
|
private readonly transcriptStore;
|
|
20
20
|
private readonly engineerSessionPrompt;
|
|
21
|
-
private readonly
|
|
22
|
-
constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string,
|
|
21
|
+
private readonly planSynthesisPrompt;
|
|
22
|
+
constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, planSynthesisPrompt: string);
|
|
23
23
|
getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
|
|
24
24
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
25
25
|
recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
|
|
@@ -6,13 +6,13 @@ export class TeamOrchestrator {
|
|
|
6
6
|
teamStore;
|
|
7
7
|
transcriptStore;
|
|
8
8
|
engineerSessionPrompt;
|
|
9
|
-
|
|
10
|
-
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt,
|
|
9
|
+
planSynthesisPrompt;
|
|
10
|
+
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, planSynthesisPrompt) {
|
|
11
11
|
this.sessions = sessions;
|
|
12
12
|
this.teamStore = teamStore;
|
|
13
13
|
this.transcriptStore = transcriptStore;
|
|
14
14
|
this.engineerSessionPrompt = engineerSessionPrompt;
|
|
15
|
-
this.
|
|
15
|
+
this.planSynthesisPrompt = planSynthesisPrompt;
|
|
16
16
|
}
|
|
17
17
|
async getOrCreateTeam(cwd, teamId) {
|
|
18
18
|
const existing = await this.teamStore.getTeam(cwd, teamId);
|
|
@@ -106,10 +106,9 @@ export class TeamOrchestrator {
|
|
|
106
106
|
}
|
|
107
107
|
const result = await this.sessions.runTask({
|
|
108
108
|
cwd: input.cwd,
|
|
109
|
-
prompt:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: this.buildSessionSystemPrompt(input.engineer, input.mode),
|
|
109
|
+
prompt: engineerState.claudeSessionId
|
|
110
|
+
? this.buildEngineerPrompt(input.mode, input.message)
|
|
111
|
+
: `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message)}`,
|
|
113
112
|
resumeSessionId: engineerState.claudeSessionId ?? undefined,
|
|
114
113
|
persistSession: true,
|
|
115
114
|
includePartialMessages: true,
|
|
@@ -224,8 +223,7 @@ export class TeamOrchestrator {
|
|
|
224
223
|
];
|
|
225
224
|
const synthesisResult = await this.sessions.runTask({
|
|
226
225
|
cwd: input.cwd,
|
|
227
|
-
prompt: buildSynthesisPrompt(input.request, drafts)
|
|
228
|
-
systemPrompt: buildSynthesisSystemPrompt(this.architectSystemPrompt),
|
|
226
|
+
prompt: `${this.planSynthesisPrompt}\n\n${buildSynthesisPrompt(input.request, drafts)}`,
|
|
229
227
|
persistSession: false,
|
|
230
228
|
includePartialMessages: false,
|
|
231
229
|
permissionMode: 'acceptEdits',
|
|
@@ -365,9 +363,6 @@ function buildPlanDraftRequest(perspective, request) {
|
|
|
365
363
|
`User request: ${request}`,
|
|
366
364
|
].join('\n');
|
|
367
365
|
}
|
|
368
|
-
function buildSynthesisSystemPrompt(architectSystemPrompt) {
|
|
369
|
-
return architectSystemPrompt;
|
|
370
|
-
}
|
|
371
366
|
function buildSynthesisPrompt(request, drafts) {
|
|
372
367
|
return [
|
|
373
368
|
`User request: ${request}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EngineerName, ManagerPromptRegistry } from '../types/contracts.js';
|
|
2
2
|
export declare const AGENT_CTO = "cto";
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const AGENT_TEAM_PLANNER = "team-planner";
|
|
4
4
|
export declare const ENGINEER_AGENT_IDS: {
|
|
5
5
|
readonly Tom: "tom";
|
|
6
6
|
readonly John: "john";
|
|
@@ -42,7 +42,7 @@ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry,
|
|
|
42
42
|
permission: AgentPermission;
|
|
43
43
|
prompt: string;
|
|
44
44
|
};
|
|
45
|
-
export declare function
|
|
45
|
+
export declare function buildTeamPlannerAgentConfig(prompts: ManagerPromptRegistry): {
|
|
46
46
|
description: string;
|
|
47
47
|
mode: "subagent";
|
|
48
48
|
hidden: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TEAM_ENGINEERS } from '../team/roster.js';
|
|
2
2
|
export const AGENT_CTO = 'cto';
|
|
3
|
-
export const
|
|
3
|
+
export const AGENT_TEAM_PLANNER = 'team-planner';
|
|
4
4
|
export const ENGINEER_AGENT_IDS = {
|
|
5
5
|
Tom: 'tom',
|
|
6
6
|
John: 'john',
|
|
@@ -38,24 +38,15 @@ const CTO_READONLY_TOOLS = {
|
|
|
38
38
|
todoread: 'allow',
|
|
39
39
|
question: 'allow',
|
|
40
40
|
};
|
|
41
|
-
function
|
|
41
|
+
function buildTeamPlannerPermissions() {
|
|
42
42
|
const denied = {};
|
|
43
43
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
44
44
|
denied[toolId] = 'deny';
|
|
45
45
|
}
|
|
46
46
|
return {
|
|
47
47
|
'*': 'deny',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
glob: 'allow',
|
|
51
|
-
list: 'allow',
|
|
52
|
-
codesearch: 'allow',
|
|
53
|
-
webfetch: 'deny',
|
|
54
|
-
websearch: 'deny',
|
|
55
|
-
lsp: 'deny',
|
|
56
|
-
todowrite: 'deny',
|
|
57
|
-
todoread: 'deny',
|
|
58
|
-
question: 'deny',
|
|
48
|
+
plan_with_team: 'allow',
|
|
49
|
+
question: 'allow',
|
|
59
50
|
...denied,
|
|
60
51
|
};
|
|
61
52
|
}
|
|
@@ -70,9 +61,13 @@ function buildCtoPermissions() {
|
|
|
70
61
|
}
|
|
71
62
|
const taskPermissions = { '*': 'deny' };
|
|
72
63
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
73
|
-
|
|
64
|
+
const agentId = ENGINEER_AGENT_IDS[engineer];
|
|
65
|
+
// Support both uppercase (user-friendly) and lowercase (canonical) agent IDs.
|
|
66
|
+
// This ensures both task({ subagent_type: 'Tom' }) and task({ subagent_type: 'tom' }) work.
|
|
67
|
+
taskPermissions[engineer] = 'allow'; // 'Tom', 'John', etc.
|
|
68
|
+
taskPermissions[agentId] = 'allow'; // 'tom', 'john', etc.
|
|
74
69
|
}
|
|
75
|
-
taskPermissions[
|
|
70
|
+
taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
|
|
76
71
|
return {
|
|
77
72
|
'*': 'deny',
|
|
78
73
|
...CTO_READONLY_TOOLS,
|
|
@@ -111,14 +106,14 @@ export function buildEngineerAgentConfig(prompts, engineer) {
|
|
|
111
106
|
prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
|
|
112
107
|
};
|
|
113
108
|
}
|
|
114
|
-
export function
|
|
109
|
+
export function buildTeamPlannerAgentConfig(prompts) {
|
|
115
110
|
return {
|
|
116
|
-
description: '
|
|
111
|
+
description: 'Runs dual-engineer planning by calling plan_with_team. Asks for engineer names if not provided.',
|
|
117
112
|
mode: 'subagent',
|
|
118
113
|
hidden: false,
|
|
119
114
|
color: '#D97757',
|
|
120
|
-
permission:
|
|
121
|
-
prompt: prompts.
|
|
115
|
+
permission: buildTeamPlannerPermissions(),
|
|
116
|
+
prompt: prompts.teamPlannerPrompt,
|
|
122
117
|
};
|
|
123
118
|
}
|
|
124
119
|
export function denyRestrictedToolsGlobally(permissions) {
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import { type Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
import type { EngineerName } from '../types/contracts.js';
|
|
2
3
|
export declare const ClaudeManagerPlugin: Plugin;
|
|
4
|
+
/**
|
|
5
|
+
* Normalize an agent ID to its lowercase canonical form.
|
|
6
|
+
* Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeAgentId(agentId: string): string;
|
|
9
|
+
export declare function engineerFromAgent(agentId: string): EngineerName;
|
|
10
|
+
export declare function isEngineerAgent(agentId: string): boolean;
|
|
@@ -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 } from '../manager/team-orchestrator.js';
|
|
5
|
-
import { AGENT_CTO,
|
|
5
|
+
import { AGENT_CTO, AGENT_TEAM_PLANNER, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.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'];
|
|
@@ -15,7 +15,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
15
15
|
config.permission ??= {};
|
|
16
16
|
denyRestrictedToolsGlobally(config.permission);
|
|
17
17
|
config.agent[AGENT_CTO] ??= buildCtoAgentConfig(managerPromptRegistry);
|
|
18
|
-
config.agent[
|
|
18
|
+
config.agent[AGENT_TEAM_PLANNER] ??= buildTeamPlannerAgentConfig(managerPromptRegistry);
|
|
19
19
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
20
20
|
config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
|
|
21
21
|
}
|
|
@@ -129,10 +129,10 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
129
129
|
abortSignal: context.abort,
|
|
130
130
|
onLeadEvent: (event) => reportClaudeEvent(context, args.leadEngineer, event),
|
|
131
131
|
onChallengerEvent: (event) => reportClaudeEvent(context, args.challengerEngineer, event),
|
|
132
|
-
onSynthesisEvent: (event) =>
|
|
132
|
+
onSynthesisEvent: (event) => reportPlanSynthesisEvent(context, event),
|
|
133
133
|
});
|
|
134
134
|
context.metadata({
|
|
135
|
-
title: '✅
|
|
135
|
+
title: '✅ Plan synthesis finished',
|
|
136
136
|
metadata: {
|
|
137
137
|
teamId: result.teamId,
|
|
138
138
|
lead: result.leadEngineer,
|
|
@@ -411,16 +411,25 @@ async function runEngineerAssignment(input, context) {
|
|
|
411
411
|
});
|
|
412
412
|
return result;
|
|
413
413
|
}
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Normalize an agent ID to its lowercase canonical form.
|
|
416
|
+
* Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
|
|
417
|
+
*/
|
|
418
|
+
export function normalizeAgentId(agentId) {
|
|
419
|
+
return agentId.toLowerCase();
|
|
420
|
+
}
|
|
421
|
+
export function engineerFromAgent(agentId) {
|
|
422
|
+
const normalized = normalizeAgentId(agentId);
|
|
423
|
+
const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === normalized);
|
|
416
424
|
const engineer = engineerEntry?.[0];
|
|
417
425
|
if (!engineer || !isEngineerName(engineer)) {
|
|
418
426
|
throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
|
|
419
427
|
}
|
|
420
428
|
return engineer;
|
|
421
429
|
}
|
|
422
|
-
function isEngineerAgent(agentId) {
|
|
423
|
-
|
|
430
|
+
export function isEngineerAgent(agentId) {
|
|
431
|
+
const normalized = normalizeAgentId(agentId);
|
|
432
|
+
return Object.values(ENGINEER_AGENT_IDS).some((id) => id === normalized);
|
|
424
433
|
}
|
|
425
434
|
/**
|
|
426
435
|
* Resolves the team ID for an engineer session.
|
|
@@ -562,10 +571,10 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
562
571
|
});
|
|
563
572
|
}
|
|
564
573
|
}
|
|
565
|
-
function
|
|
574
|
+
function reportPlanSynthesisEvent(context, event) {
|
|
566
575
|
if (event.type === 'error') {
|
|
567
576
|
context.metadata({
|
|
568
|
-
title: `❌
|
|
577
|
+
title: `❌ Plan synthesis hit an error`,
|
|
569
578
|
metadata: {
|
|
570
579
|
sessionId: event.sessionId,
|
|
571
580
|
error: event.text.slice(0, 200),
|
|
@@ -575,7 +584,7 @@ function reportArchitectEvent(context, event) {
|
|
|
575
584
|
}
|
|
576
585
|
if (event.type === 'init') {
|
|
577
586
|
context.metadata({
|
|
578
|
-
title: `⚡
|
|
587
|
+
title: `⚡ Plan synthesis ready`,
|
|
579
588
|
metadata: {
|
|
580
589
|
sessionId: event.sessionId,
|
|
581
590
|
},
|
|
@@ -608,10 +617,10 @@ function reportArchitectEvent(context, event) {
|
|
|
608
617
|
const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
|
|
609
618
|
context.metadata({
|
|
610
619
|
title: toolDescription
|
|
611
|
-
? `⚡
|
|
620
|
+
? `⚡ Plan synthesis → ${toolDescription}`
|
|
612
621
|
: toolName
|
|
613
|
-
? `⚡
|
|
614
|
-
: `⚡
|
|
622
|
+
? `⚡ Plan synthesis → ${toolName}`
|
|
623
|
+
: `⚡ Plan synthesis is running`,
|
|
615
624
|
metadata: {
|
|
616
625
|
sessionId: event.sessionId,
|
|
617
626
|
...(toolName !== undefined && { toolName }),
|
|
@@ -625,7 +634,7 @@ function reportArchitectEvent(context, event) {
|
|
|
625
634
|
const isThinking = event.text.startsWith('<thinking>');
|
|
626
635
|
const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
|
|
627
636
|
context.metadata({
|
|
628
|
-
title: `⚡
|
|
637
|
+
title: `⚡ Plan synthesis ${stateLabel}`,
|
|
629
638
|
metadata: {
|
|
630
639
|
sessionId: event.sessionId,
|
|
631
640
|
preview: event.text.slice(0, 160),
|
|
@@ -635,8 +644,9 @@ function reportArchitectEvent(context, event) {
|
|
|
635
644
|
}
|
|
636
645
|
}
|
|
637
646
|
function annotateToolRun(context, title, metadata) {
|
|
647
|
+
const agentLabel = context.agent === AGENT_CTO ? 'CTO' : undefined;
|
|
638
648
|
context.metadata({
|
|
639
|
-
title,
|
|
649
|
+
title: agentLabel ? `${agentLabel} → ${title}` : title,
|
|
640
650
|
metadata,
|
|
641
651
|
});
|
|
642
652
|
}
|
|
@@ -24,7 +24,7 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
24
24
|
const teamStore = new TeamStateStore();
|
|
25
25
|
const transcriptStore = new TranscriptStore();
|
|
26
26
|
const manager = new PersistentManager(gitOps, transcriptStore);
|
|
27
|
-
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.
|
|
27
|
+
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.planSynthesisPrompt);
|
|
28
28
|
const services = {
|
|
29
29
|
manager,
|
|
30
30
|
sessions: sessionService,
|
|
@@ -27,7 +27,7 @@ export const managerPromptRegistry = {
|
|
|
27
27
|
'',
|
|
28
28
|
'Plan and decompose:',
|
|
29
29
|
'- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
|
|
30
|
-
|
|
30
|
+
"- For medium or large tasks, delegate dual-engineer exploration and synthesis to the `team-planner` subagent: use `task(subagent_type: 'team-planner', ...)`. When tasking engineer subagents directly, use lowercase subagent IDs: tom, john, maya, sara, alex.",
|
|
31
31
|
'- Define clear success criteria before delegating. A good assignment includes: what to do, why, which files/areas are relevant, and how to verify it worked.',
|
|
32
32
|
'',
|
|
33
33
|
'Delegate through the Task tool:',
|
|
@@ -76,8 +76,8 @@ export const managerPromptRegistry = {
|
|
|
76
76
|
'Report blockers immediately with exact error output. Do not retry silently more than once.',
|
|
77
77
|
'Do not run git commit, git push, git reset, git checkout, or git stash.',
|
|
78
78
|
].join('\n'),
|
|
79
|
-
|
|
80
|
-
'You are
|
|
79
|
+
planSynthesisPrompt: [
|
|
80
|
+
'You are synthesizing two independent engineering plans into one stronger, unified plan.',
|
|
81
81
|
'Compare the lead and challenger plans on clarity, feasibility, risk, and fit to the user request.',
|
|
82
82
|
'Prefer the simplest path that fully addresses the goal. Surface tradeoffs honestly.',
|
|
83
83
|
'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
|
|
@@ -91,6 +91,14 @@ export const managerPromptRegistry = {
|
|
|
91
91
|
'## Recommended Answer',
|
|
92
92
|
'<answer or NONE>',
|
|
93
93
|
].join('\n'),
|
|
94
|
+
teamPlannerPrompt: [
|
|
95
|
+
'You are the team planner. Your only job is to invoke `plan_with_team`.',
|
|
96
|
+
'`plan_with_team` dispatches two engineers in parallel (lead + challenger) then synthesizes their plans.',
|
|
97
|
+
'',
|
|
98
|
+
'If the task includes a lead engineer and a challenger engineer, call `plan_with_team` immediately.',
|
|
99
|
+
'If either engineer name is missing, use `question` to ask: which engineers should lead and challenge (Tom, John, Maya, Sara, or Alex)?',
|
|
100
|
+
'Do not attempt any planning or analysis yourself. Delegate entirely to `plan_with_team`.',
|
|
101
|
+
].join('\n'),
|
|
94
102
|
contextWarnings: {
|
|
95
103
|
moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
|
|
96
104
|
high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
|
|
@@ -2,7 +2,10 @@ export interface ManagerPromptRegistry {
|
|
|
2
2
|
ctoSystemPrompt: string;
|
|
3
3
|
engineerAgentPrompt: string;
|
|
4
4
|
engineerSessionPrompt: string;
|
|
5
|
-
|
|
5
|
+
/** Prompt injected as the system prompt of the non-persistent synthesis runTask call inside plan_with_team. */
|
|
6
|
+
planSynthesisPrompt: string;
|
|
7
|
+
/** Visible subagent prompt for teamPlanner — thin bridge that calls plan_with_team. */
|
|
8
|
+
teamPlannerPrompt: string;
|
|
6
9
|
contextWarnings: {
|
|
7
10
|
moderate: string;
|
|
8
11
|
high: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { ClaudeManagerPlugin } from '../src/plugin/claude-manager.plugin.js';
|
|
3
|
-
import { AGENT_CTO,
|
|
3
|
+
import { AGENT_CTO, AGENT_TEAM_PLANNER, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from '../src/plugin/agent-hierarchy.js';
|
|
4
4
|
describe('ClaudeManagerPlugin', () => {
|
|
5
5
|
it('configures CTO with orchestration tools and question access', async () => {
|
|
6
6
|
const plugin = await ClaudeManagerPlugin({
|
|
@@ -34,16 +34,38 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
34
34
|
git_log: 'allow',
|
|
35
35
|
claude: 'deny',
|
|
36
36
|
});
|
|
37
|
+
// Task permissions should include both uppercase (user-friendly) and lowercase (canonical) agent IDs.
|
|
37
38
|
expect(cto.permission.task).toEqual({
|
|
38
39
|
'*': 'deny',
|
|
40
|
+
Tom: 'allow',
|
|
39
41
|
tom: 'allow',
|
|
42
|
+
John: 'allow',
|
|
40
43
|
john: 'allow',
|
|
44
|
+
Maya: 'allow',
|
|
41
45
|
maya: 'allow',
|
|
46
|
+
Sara: 'allow',
|
|
42
47
|
sara: 'allow',
|
|
48
|
+
Alex: 'allow',
|
|
43
49
|
alex: 'allow',
|
|
44
|
-
|
|
50
|
+
'team-planner': 'allow',
|
|
45
51
|
});
|
|
46
52
|
});
|
|
53
|
+
it('allows CTO to delegate to engineers using both uppercase and lowercase agent IDs', async () => {
|
|
54
|
+
const plugin = await ClaudeManagerPlugin({
|
|
55
|
+
worktree: '/tmp/project',
|
|
56
|
+
});
|
|
57
|
+
const config = {};
|
|
58
|
+
await plugin.config?.(config);
|
|
59
|
+
const agents = (config.agent ?? {});
|
|
60
|
+
const cto = agents[AGENT_CTO];
|
|
61
|
+
const taskPerms = cto.permission.task;
|
|
62
|
+
// Verify both uppercase and lowercase can be used for delegation.
|
|
63
|
+
// This prevents delegation failures when users write task(subagent_type: 'Maya') vs task(subagent_type: 'maya').
|
|
64
|
+
expect(taskPerms.Tom).toBe('allow');
|
|
65
|
+
expect(taskPerms.tom).toBe('allow');
|
|
66
|
+
expect(taskPerms.Maya).toBe('allow');
|
|
67
|
+
expect(taskPerms.maya).toBe('allow');
|
|
68
|
+
});
|
|
47
69
|
it('configures every named engineer with only the claude bridge tool', async () => {
|
|
48
70
|
const plugin = await ClaudeManagerPlugin({
|
|
49
71
|
worktree: '/tmp/project',
|
|
@@ -69,26 +91,25 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
69
91
|
expect(agent.permission).not.toHaveProperty('grep');
|
|
70
92
|
}
|
|
71
93
|
});
|
|
72
|
-
it('configures
|
|
94
|
+
it('configures team-planner as a planning-bridge subagent', async () => {
|
|
73
95
|
const plugin = await ClaudeManagerPlugin({
|
|
74
96
|
worktree: '/tmp/project',
|
|
75
97
|
});
|
|
76
98
|
const config = {};
|
|
77
99
|
await plugin.config?.(config);
|
|
78
100
|
const agents = (config.agent ?? {});
|
|
79
|
-
const
|
|
80
|
-
expect(
|
|
81
|
-
expect(
|
|
82
|
-
expect(
|
|
83
|
-
expect(
|
|
101
|
+
const teamPlanner = agents[AGENT_TEAM_PLANNER];
|
|
102
|
+
expect(teamPlanner).toBeDefined();
|
|
103
|
+
expect(teamPlanner.mode).toBe('subagent');
|
|
104
|
+
expect(teamPlanner.description.toLowerCase()).toContain('plan');
|
|
105
|
+
expect(teamPlanner.permission).toMatchObject({
|
|
84
106
|
'*': 'deny',
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
glob: 'allow',
|
|
88
|
-
list: 'allow',
|
|
89
|
-
codesearch: 'allow',
|
|
107
|
+
plan_with_team: 'allow',
|
|
108
|
+
question: 'allow',
|
|
90
109
|
claude: 'deny',
|
|
91
110
|
});
|
|
111
|
+
expect(teamPlanner.permission).not.toHaveProperty('read');
|
|
112
|
+
expect(teamPlanner.permission).not.toHaveProperty('grep');
|
|
92
113
|
});
|
|
93
114
|
it('registers the named engineer bridge and team status tools', async () => {
|
|
94
115
|
const plugin = await ClaudeManagerPlugin({
|
|
@@ -129,3 +150,53 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
129
150
|
expect(plugin['experimental.chat.system.transform']).toBeTypeOf('function');
|
|
130
151
|
});
|
|
131
152
|
});
|
|
153
|
+
describe('Agent ID normalization and lookup helpers', () => {
|
|
154
|
+
it('normalizeAgentId converts mixed-case agent IDs to lowercase', async () => {
|
|
155
|
+
const { normalizeAgentId } = await import('../src/plugin/claude-manager.plugin.js');
|
|
156
|
+
expect(normalizeAgentId('Tom')).toBe('tom');
|
|
157
|
+
expect(normalizeAgentId('MAYA')).toBe('maya');
|
|
158
|
+
expect(normalizeAgentId('john')).toBe('john');
|
|
159
|
+
expect(normalizeAgentId('JoHn')).toBe('john');
|
|
160
|
+
});
|
|
161
|
+
it('engineerFromAgent resolves both uppercase and lowercase agent IDs', async () => {
|
|
162
|
+
const { engineerFromAgent } = await import('../src/plugin/claude-manager.plugin.js');
|
|
163
|
+
// Lowercase (canonical)
|
|
164
|
+
expect(engineerFromAgent('tom')).toBe('Tom');
|
|
165
|
+
expect(engineerFromAgent('maya')).toBe('Maya');
|
|
166
|
+
expect(engineerFromAgent('john')).toBe('John');
|
|
167
|
+
// Uppercase (normalized)
|
|
168
|
+
expect(engineerFromAgent('Tom')).toBe('Tom');
|
|
169
|
+
expect(engineerFromAgent('Maya')).toBe('Maya');
|
|
170
|
+
expect(engineerFromAgent('John')).toBe('John');
|
|
171
|
+
// Mixed case
|
|
172
|
+
expect(engineerFromAgent('JoHn')).toBe('John');
|
|
173
|
+
expect(engineerFromAgent('mAyA')).toBe('Maya');
|
|
174
|
+
});
|
|
175
|
+
it('engineerFromAgent throws on invalid agent IDs', async () => {
|
|
176
|
+
const { engineerFromAgent } = await import('../src/plugin/claude-manager.plugin.js');
|
|
177
|
+
expect(() => engineerFromAgent('invalid')).toThrow('The claude tool can only be used from a named engineer agent');
|
|
178
|
+
expect(() => engineerFromAgent('TomInvalid')).toThrow('The claude tool can only be used from a named engineer agent');
|
|
179
|
+
});
|
|
180
|
+
it('isEngineerAgent identifies both uppercase and lowercase agent IDs', async () => {
|
|
181
|
+
const { isEngineerAgent } = await import('../src/plugin/claude-manager.plugin.js');
|
|
182
|
+
// Lowercase (canonical)
|
|
183
|
+
expect(isEngineerAgent('tom')).toBe(true);
|
|
184
|
+
expect(isEngineerAgent('maya')).toBe(true);
|
|
185
|
+
expect(isEngineerAgent('john')).toBe(true);
|
|
186
|
+
expect(isEngineerAgent('sara')).toBe(true);
|
|
187
|
+
expect(isEngineerAgent('alex')).toBe(true);
|
|
188
|
+
// Uppercase (normalized)
|
|
189
|
+
expect(isEngineerAgent('Tom')).toBe(true);
|
|
190
|
+
expect(isEngineerAgent('Maya')).toBe(true);
|
|
191
|
+
expect(isEngineerAgent('John')).toBe(true);
|
|
192
|
+
expect(isEngineerAgent('Sara')).toBe(true);
|
|
193
|
+
expect(isEngineerAgent('Alex')).toBe(true);
|
|
194
|
+
// Mixed case
|
|
195
|
+
expect(isEngineerAgent('JoHn')).toBe(true);
|
|
196
|
+
expect(isEngineerAgent('mAyA')).toBe(true);
|
|
197
|
+
// Invalid
|
|
198
|
+
expect(isEngineerAgent('invalid')).toBe(false);
|
|
199
|
+
expect(isEngineerAgent('cto')).toBe(false);
|
|
200
|
+
expect(isEngineerAgent('team-planner')).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -5,7 +5,7 @@ describe('managerPromptRegistry', () => {
|
|
|
5
5
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('You are a principal engineer orchestrating a team of AI-powered engineers');
|
|
6
6
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Task tool');
|
|
7
7
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('dual-engineer');
|
|
8
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('
|
|
8
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('team-planner');
|
|
9
9
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('question');
|
|
10
10
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Tom, John, Maya, Sara, and Alex');
|
|
11
11
|
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('clear_session');
|
|
@@ -29,12 +29,16 @@ describe('managerPromptRegistry', () => {
|
|
|
29
29
|
expect(managerPromptRegistry.contextWarnings.high).toContain('{turns}');
|
|
30
30
|
expect(managerPromptRegistry.contextWarnings.critical).toContain('near capacity');
|
|
31
31
|
});
|
|
32
|
-
it('
|
|
33
|
-
expect(managerPromptRegistry.
|
|
34
|
-
expect(managerPromptRegistry.
|
|
35
|
-
expect(managerPromptRegistry.
|
|
36
|
-
expect(managerPromptRegistry.
|
|
37
|
-
expect(managerPromptRegistry.
|
|
38
|
-
|
|
32
|
+
it('planSynthesisPrompt contains synthesis guidance and complete output format', () => {
|
|
33
|
+
expect(managerPromptRegistry.planSynthesisPrompt).toContain('synthesiz');
|
|
34
|
+
expect(managerPromptRegistry.planSynthesisPrompt).toContain('two independent');
|
|
35
|
+
expect(managerPromptRegistry.planSynthesisPrompt).toContain('## Synthesis');
|
|
36
|
+
expect(managerPromptRegistry.planSynthesisPrompt).toContain('## Recommended Question');
|
|
37
|
+
expect(managerPromptRegistry.planSynthesisPrompt).toContain('## Recommended Answer');
|
|
38
|
+
});
|
|
39
|
+
it('teamPlannerPrompt directs the agent to call plan_with_team and ask if engineers are missing', () => {
|
|
40
|
+
expect(managerPromptRegistry.teamPlannerPrompt).toContain('plan_with_team');
|
|
41
|
+
expect(managerPromptRegistry.teamPlannerPrompt).toContain('question');
|
|
42
|
+
expect(managerPromptRegistry.teamPlannerPrompt).toContain('engineer');
|
|
39
43
|
});
|
|
40
44
|
});
|
|
@@ -180,7 +180,7 @@ describe('second invocation continuity', () => {
|
|
|
180
180
|
// ── Phase 1: first task via orchestrator (no real SDK needed) ──────────
|
|
181
181
|
const store = new TeamStateStore();
|
|
182
182
|
await store.setActiveTeam(tempRoot, 'cto-1');
|
|
183
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', '
|
|
183
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt');
|
|
184
184
|
await orchestrator.recordWrapperSession(tempRoot, 'cto-1', 'Tom', 'wrapper-tom-1');
|
|
185
185
|
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
186
|
// ── Phase 2: process restart ───────────────────────────────────────────
|
|
@@ -206,7 +206,7 @@ describe('second invocation continuity', () => {
|
|
|
206
206
|
// ── Phase 1: pre-seed Tom with a claudeSessionId ───────────────────────
|
|
207
207
|
const store = new TeamStateStore();
|
|
208
208
|
await store.setActiveTeam(tempRoot, 'cto-1');
|
|
209
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', '
|
|
209
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => undefined) }, 'Base prompt', 'Synthesis prompt');
|
|
210
210
|
await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
|
|
211
211
|
await store.updateTeam(tempRoot, 'cto-1', (team) => ({
|
|
212
212
|
...team,
|
|
@@ -240,7 +240,7 @@ describe('second invocation continuity', () => {
|
|
|
240
240
|
expect(runTask).toHaveBeenCalledOnce();
|
|
241
241
|
expect(runTask.mock.calls[0]?.[0]).toMatchObject({
|
|
242
242
|
resumeSessionId: 'ses-tom-persisted',
|
|
243
|
-
systemPrompt: undefined, // no new system prompt when resuming
|
|
244
243
|
});
|
|
244
|
+
expect(runTask.mock.calls[0]?.[0].systemPrompt).toBeUndefined(); // no system prompt when resuming
|
|
245
245
|
});
|
|
246
246
|
});
|
|
@@ -35,7 +35,7 @@ describe('TeamOrchestrator', () => {
|
|
|
35
35
|
outputTokens: 300,
|
|
36
36
|
contextWindowSize: 200_000,
|
|
37
37
|
});
|
|
38
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
38
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
39
39
|
const first = await orchestrator.dispatchEngineer({
|
|
40
40
|
teamId: 'team-1',
|
|
41
41
|
cwd: tempRoot,
|
|
@@ -53,17 +53,22 @@ describe('TeamOrchestrator', () => {
|
|
|
53
53
|
expect(first.sessionId).toBe('ses_tom');
|
|
54
54
|
expect(second.sessionId).toBe('ses_tom');
|
|
55
55
|
expect(runTask.mock.calls[0]?.[0]).toMatchObject({
|
|
56
|
-
systemPrompt: expect.stringContaining('Assigned engineer: Tom.'),
|
|
57
56
|
resumeSessionId: undefined,
|
|
58
57
|
permissionMode: 'acceptEdits',
|
|
59
58
|
restrictWriteTools: true,
|
|
60
59
|
});
|
|
60
|
+
expect(runTask.mock.calls[0]?.[0].systemPrompt).toBeUndefined();
|
|
61
|
+
expect(runTask.mock.calls[0]?.[0].prompt).toContain('Base engineer prompt');
|
|
62
|
+
expect(runTask.mock.calls[0]?.[0].prompt).toContain('Assigned engineer: Tom.');
|
|
63
|
+
expect(runTask.mock.calls[0]?.[0].prompt).toContain('Investigate the auth flow');
|
|
61
64
|
expect(runTask.mock.calls[1]?.[0]).toMatchObject({
|
|
62
|
-
systemPrompt: undefined,
|
|
63
65
|
resumeSessionId: 'ses_tom',
|
|
64
66
|
permissionMode: 'acceptEdits',
|
|
65
67
|
restrictWriteTools: false,
|
|
66
68
|
});
|
|
69
|
+
expect(runTask.mock.calls[1]?.[0].systemPrompt).toBeUndefined();
|
|
70
|
+
expect(runTask.mock.calls[1]?.[0].prompt).not.toContain('Assigned engineer: Tom.');
|
|
71
|
+
expect(runTask.mock.calls[1]?.[0].prompt).toContain('Implement the chosen fix');
|
|
67
72
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
68
73
|
expect(team.engineers.find((engineer) => engineer.name === 'Tom')).toMatchObject({
|
|
69
74
|
claudeSessionId: 'ses_tom',
|
|
@@ -74,7 +79,7 @@ describe('TeamOrchestrator', () => {
|
|
|
74
79
|
it('rejects work when the same engineer is already busy', async () => {
|
|
75
80
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
76
81
|
const store = new TeamStateStore('.state');
|
|
77
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
82
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
78
83
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
79
84
|
await store.saveTeam({
|
|
80
85
|
...team,
|
|
@@ -112,7 +117,7 @@ describe('TeamOrchestrator', () => {
|
|
|
112
117
|
events: [],
|
|
113
118
|
finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
|
|
114
119
|
});
|
|
115
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
120
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
116
121
|
const result = await orchestrator.planWithTeam({
|
|
117
122
|
teamId: 'team-1',
|
|
118
123
|
cwd: tempRoot,
|
|
@@ -125,6 +130,10 @@ describe('TeamOrchestrator', () => {
|
|
|
125
130
|
expect(result.recommendedQuestion).toBe('Should we migrate now?');
|
|
126
131
|
expect(result.recommendedAnswer).toBe('No, defer it.');
|
|
127
132
|
expect(runTask).toHaveBeenCalledTimes(3);
|
|
133
|
+
const synthesisCall = runTask.mock.calls[2]?.[0];
|
|
134
|
+
expect(synthesisCall.systemPrompt).toBeUndefined();
|
|
135
|
+
expect(synthesisCall.prompt).toContain('Synthesis prompt');
|
|
136
|
+
expect(synthesisCall.prompt).toContain('Plan the billing refactor');
|
|
128
137
|
});
|
|
129
138
|
it('invokes lead, challenger, and synthesis event callbacks', async () => {
|
|
130
139
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
@@ -157,7 +166,7 @@ describe('TeamOrchestrator', () => {
|
|
|
157
166
|
};
|
|
158
167
|
}
|
|
159
168
|
});
|
|
160
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
169
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
161
170
|
const onLeadEvent = vi.fn();
|
|
162
171
|
const onChallengerEvent = vi.fn();
|
|
163
172
|
const onSynthesisEvent = vi.fn();
|
|
@@ -177,7 +186,7 @@ describe('TeamOrchestrator', () => {
|
|
|
177
186
|
});
|
|
178
187
|
it('persists wrapper session memory for an engineer', async () => {
|
|
179
188
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
180
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', '
|
|
189
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Synthesis prompt');
|
|
181
190
|
await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
|
|
182
191
|
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.');
|
|
183
192
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|