@doingdev/opencode-claude-manager-plugin 0.1.53 → 0.1.54
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 +5 -1
- package/dist/manager/team-orchestrator.js +9 -17
- package/dist/plugin/agent-hierarchy.d.ts +9 -0
- package/dist/plugin/agent-hierarchy.js +33 -1
- package/dist/plugin/claude-manager.plugin.js +78 -2
- package/dist/plugin/service-factory.js +1 -1
- package/dist/prompts/registry.js +16 -1
- package/dist/src/manager/team-orchestrator.d.ts +5 -1
- package/dist/src/manager/team-orchestrator.js +9 -17
- package/dist/src/plugin/agent-hierarchy.d.ts +9 -0
- package/dist/src/plugin/agent-hierarchy.js +33 -1
- package/dist/src/plugin/claude-manager.plugin.js +78 -2
- package/dist/src/plugin/service-factory.js +1 -1
- package/dist/src/prompts/registry.js +16 -1
- package/dist/src/types/contracts.d.ts +1 -0
- package/dist/test/claude-manager.plugin.test.js +23 -3
- package/dist/test/prompt-registry.test.js +10 -1
- package/dist/test/report-claude-event.test.js +2 -2
- package/dist/test/team-orchestrator.test.js +53 -4
- package/dist/types/contracts.d.ts +1 -0
- package/package.json +1 -1
|
@@ -18,7 +18,8 @@ export declare class TeamOrchestrator {
|
|
|
18
18
|
private readonly teamStore;
|
|
19
19
|
private readonly transcriptStore;
|
|
20
20
|
private readonly engineerSessionPrompt;
|
|
21
|
-
|
|
21
|
+
private readonly architectSystemPrompt;
|
|
22
|
+
constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, architectSystemPrompt: string);
|
|
22
23
|
getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
|
|
23
24
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
24
25
|
recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
|
|
@@ -44,6 +45,9 @@ export declare class TeamOrchestrator {
|
|
|
44
45
|
challengerEngineer: EngineerName;
|
|
45
46
|
model?: string;
|
|
46
47
|
abortSignal?: AbortSignal;
|
|
48
|
+
onLeadEvent?: ClaudeSessionEventHandler;
|
|
49
|
+
onChallengerEvent?: ClaudeSessionEventHandler;
|
|
50
|
+
onSynthesisEvent?: ClaudeSessionEventHandler;
|
|
47
51
|
}): Promise<SynthesizedPlanResult>;
|
|
48
52
|
private updateEngineer;
|
|
49
53
|
private reserveEngineer;
|
|
@@ -6,11 +6,13 @@ export class TeamOrchestrator {
|
|
|
6
6
|
teamStore;
|
|
7
7
|
transcriptStore;
|
|
8
8
|
engineerSessionPrompt;
|
|
9
|
-
|
|
9
|
+
architectSystemPrompt;
|
|
10
|
+
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, architectSystemPrompt) {
|
|
10
11
|
this.sessions = sessions;
|
|
11
12
|
this.teamStore = teamStore;
|
|
12
13
|
this.transcriptStore = transcriptStore;
|
|
13
14
|
this.engineerSessionPrompt = engineerSessionPrompt;
|
|
15
|
+
this.architectSystemPrompt = architectSystemPrompt;
|
|
14
16
|
}
|
|
15
17
|
async getOrCreateTeam(cwd, teamId) {
|
|
16
18
|
const existing = await this.teamStore.getTeam(cwd, teamId);
|
|
@@ -203,6 +205,7 @@ export class TeamOrchestrator {
|
|
|
203
205
|
message: buildPlanDraftRequest('lead', input.request),
|
|
204
206
|
model: input.model,
|
|
205
207
|
abortSignal: input.abortSignal,
|
|
208
|
+
onEvent: input.onLeadEvent,
|
|
206
209
|
}),
|
|
207
210
|
this.dispatchEngineer({
|
|
208
211
|
teamId: input.teamId,
|
|
@@ -212,6 +215,7 @@ export class TeamOrchestrator {
|
|
|
212
215
|
message: buildPlanDraftRequest('challenger', input.request),
|
|
213
216
|
model: input.model,
|
|
214
217
|
abortSignal: input.abortSignal,
|
|
218
|
+
onEvent: input.onChallengerEvent,
|
|
215
219
|
}),
|
|
216
220
|
]);
|
|
217
221
|
const drafts = [
|
|
@@ -221,7 +225,7 @@ export class TeamOrchestrator {
|
|
|
221
225
|
const synthesisResult = await this.sessions.runTask({
|
|
222
226
|
cwd: input.cwd,
|
|
223
227
|
prompt: buildSynthesisPrompt(input.request, drafts),
|
|
224
|
-
systemPrompt: buildSynthesisSystemPrompt(),
|
|
228
|
+
systemPrompt: buildSynthesisSystemPrompt(this.architectSystemPrompt),
|
|
225
229
|
persistSession: false,
|
|
226
230
|
includePartialMessages: false,
|
|
227
231
|
permissionMode: 'acceptEdits',
|
|
@@ -230,7 +234,7 @@ export class TeamOrchestrator {
|
|
|
230
234
|
effort: 'high',
|
|
231
235
|
settingSources: ['user', 'project', 'local'],
|
|
232
236
|
abortSignal: input.abortSignal,
|
|
233
|
-
});
|
|
237
|
+
}, input.onSynthesisEvent);
|
|
234
238
|
const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
|
|
235
239
|
return {
|
|
236
240
|
teamId: input.teamId,
|
|
@@ -361,20 +365,8 @@ function buildPlanDraftRequest(perspective, request) {
|
|
|
361
365
|
`User request: ${request}`,
|
|
362
366
|
].join('\n');
|
|
363
367
|
}
|
|
364
|
-
function buildSynthesisSystemPrompt() {
|
|
365
|
-
return
|
|
366
|
-
'You are synthesizing two independent engineering plans into one stronger plan.',
|
|
367
|
-
'Compare them on clarity, feasibility, risk, and fit to the user request.',
|
|
368
|
-
'Prefer the simplest path that fully addresses the goal.',
|
|
369
|
-
'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
|
|
370
|
-
'Use this output format exactly:',
|
|
371
|
-
'## Synthesis',
|
|
372
|
-
'<combined plan>',
|
|
373
|
-
'## Recommended Question',
|
|
374
|
-
'<question or NONE>',
|
|
375
|
-
'## Recommended Answer',
|
|
376
|
-
'<answer or NONE>',
|
|
377
|
-
].join('\n');
|
|
368
|
+
function buildSynthesisSystemPrompt(architectSystemPrompt) {
|
|
369
|
+
return architectSystemPrompt;
|
|
378
370
|
}
|
|
379
371
|
function buildSynthesisPrompt(request, drafts) {
|
|
380
372
|
return [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EngineerName, ManagerPromptRegistry } from '../types/contracts.js';
|
|
2
2
|
export declare const AGENT_CTO = "cto";
|
|
3
|
+
export declare const AGENT_ARCHITECT = "architect";
|
|
3
4
|
export declare const ENGINEER_AGENT_IDS: {
|
|
4
5
|
readonly Tom: "tom";
|
|
5
6
|
readonly John: "john";
|
|
@@ -41,5 +42,13 @@ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry,
|
|
|
41
42
|
permission: AgentPermission;
|
|
42
43
|
prompt: string;
|
|
43
44
|
};
|
|
45
|
+
export declare function buildArchitectAgentConfig(prompts: ManagerPromptRegistry): {
|
|
46
|
+
description: string;
|
|
47
|
+
mode: "subagent";
|
|
48
|
+
hidden: boolean;
|
|
49
|
+
color: string;
|
|
50
|
+
permission: AgentPermission;
|
|
51
|
+
prompt: string;
|
|
52
|
+
};
|
|
44
53
|
export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
|
|
45
54
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TEAM_ENGINEERS } from '../team/roster.js';
|
|
2
2
|
export const AGENT_CTO = 'cto';
|
|
3
|
+
export const AGENT_ARCHITECT = 'architect';
|
|
3
4
|
export const ENGINEER_AGENT_IDS = {
|
|
4
5
|
Tom: 'tom',
|
|
5
6
|
John: 'john',
|
|
@@ -10,7 +11,6 @@ export const ENGINEER_AGENT_IDS = {
|
|
|
10
11
|
export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
|
|
11
12
|
const CTO_ONLY_TOOL_IDS = [
|
|
12
13
|
'team_status',
|
|
13
|
-
'plan_with_team',
|
|
14
14
|
'reset_engineer',
|
|
15
15
|
'list_transcripts',
|
|
16
16
|
'list_history',
|
|
@@ -38,6 +38,27 @@ const CTO_READONLY_TOOLS = {
|
|
|
38
38
|
todoread: 'allow',
|
|
39
39
|
question: 'allow',
|
|
40
40
|
};
|
|
41
|
+
function buildArchitectPermissions() {
|
|
42
|
+
const denied = {};
|
|
43
|
+
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
44
|
+
denied[toolId] = 'deny';
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
'*': 'deny',
|
|
48
|
+
read: 'allow',
|
|
49
|
+
grep: 'allow',
|
|
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',
|
|
59
|
+
...denied,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
41
62
|
function buildCtoPermissions() {
|
|
42
63
|
const denied = {};
|
|
43
64
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
@@ -51,6 +72,7 @@ function buildCtoPermissions() {
|
|
|
51
72
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
52
73
|
taskPermissions[ENGINEER_AGENT_IDS[engineer]] = 'allow';
|
|
53
74
|
}
|
|
75
|
+
taskPermissions[AGENT_ARCHITECT] = 'allow';
|
|
54
76
|
return {
|
|
55
77
|
'*': 'deny',
|
|
56
78
|
...CTO_READONLY_TOOLS,
|
|
@@ -89,6 +111,16 @@ export function buildEngineerAgentConfig(prompts, engineer) {
|
|
|
89
111
|
prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
|
|
90
112
|
};
|
|
91
113
|
}
|
|
114
|
+
export function buildArchitectAgentConfig(prompts) {
|
|
115
|
+
return {
|
|
116
|
+
description: 'Synthesizes two engineer plan drafts into one stronger, actionable plan.',
|
|
117
|
+
mode: 'subagent',
|
|
118
|
+
hidden: false,
|
|
119
|
+
color: '#D97757',
|
|
120
|
+
permission: buildArchitectPermissions(),
|
|
121
|
+
prompt: prompts.architectSystemPrompt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
92
124
|
export function denyRestrictedToolsGlobally(permissions) {
|
|
93
125
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
94
126
|
permissions[toolId] ??= 'deny';
|
|
@@ -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, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
|
|
5
|
+
import { AGENT_CTO, AGENT_ARCHITECT, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildArchitectAgentConfig, 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,6 +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[AGENT_ARCHITECT] ??= buildArchitectAgentConfig(managerPromptRegistry);
|
|
18
19
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
19
20
|
config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
|
|
20
21
|
}
|
|
@@ -126,9 +127,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
126
127
|
challengerEngineer: args.challengerEngineer,
|
|
127
128
|
model: args.model,
|
|
128
129
|
abortSignal: context.abort,
|
|
130
|
+
onLeadEvent: (event) => reportClaudeEvent(context, args.leadEngineer, event),
|
|
131
|
+
onChallengerEvent: (event) => reportClaudeEvent(context, args.challengerEngineer, event),
|
|
132
|
+
onSynthesisEvent: (event) => reportArchitectEvent(context, event),
|
|
129
133
|
});
|
|
130
134
|
context.metadata({
|
|
131
|
-
title: '✅
|
|
135
|
+
title: '✅ Architect finished',
|
|
132
136
|
metadata: {
|
|
133
137
|
teamId: result.teamId,
|
|
134
138
|
lead: result.leadEngineer,
|
|
@@ -558,6 +562,78 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
558
562
|
});
|
|
559
563
|
}
|
|
560
564
|
}
|
|
565
|
+
function reportArchitectEvent(context, event) {
|
|
566
|
+
if (event.type === 'error') {
|
|
567
|
+
context.metadata({
|
|
568
|
+
title: `❌ Architect hit an error`,
|
|
569
|
+
metadata: {
|
|
570
|
+
sessionId: event.sessionId,
|
|
571
|
+
error: event.text.slice(0, 200),
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (event.type === 'init') {
|
|
577
|
+
context.metadata({
|
|
578
|
+
title: `⚡ Architect session ready`,
|
|
579
|
+
metadata: {
|
|
580
|
+
sessionId: event.sessionId,
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (event.type === 'tool_call') {
|
|
586
|
+
let toolName;
|
|
587
|
+
let toolId;
|
|
588
|
+
let toolArgs;
|
|
589
|
+
try {
|
|
590
|
+
const parsed = JSON.parse(event.text);
|
|
591
|
+
toolName = parsed.name;
|
|
592
|
+
toolId = parsed.id;
|
|
593
|
+
if (typeof parsed.input === 'string') {
|
|
594
|
+
try {
|
|
595
|
+
toolArgs = JSON.parse(parsed.input);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
toolArgs = parsed.input;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
toolArgs = parsed.input;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// event.text is not valid JSON — fall back to generic title
|
|
607
|
+
}
|
|
608
|
+
const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
|
|
609
|
+
context.metadata({
|
|
610
|
+
title: toolDescription
|
|
611
|
+
? `⚡ Architect → ${toolDescription}`
|
|
612
|
+
: toolName
|
|
613
|
+
? `⚡ Architect → ${toolName}`
|
|
614
|
+
: `⚡ Architect is using Claude Code tools`,
|
|
615
|
+
metadata: {
|
|
616
|
+
sessionId: event.sessionId,
|
|
617
|
+
...(toolName !== undefined && { toolName }),
|
|
618
|
+
...(toolId !== undefined && { toolId }),
|
|
619
|
+
...(toolArgs !== undefined && { toolArgs }),
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (event.type === 'assistant' || event.type === 'partial') {
|
|
625
|
+
const isThinking = event.text.startsWith('<thinking>');
|
|
626
|
+
const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
|
|
627
|
+
context.metadata({
|
|
628
|
+
title: `⚡ Architect ${stateLabel}`,
|
|
629
|
+
metadata: {
|
|
630
|
+
sessionId: event.sessionId,
|
|
631
|
+
preview: event.text.slice(0, 160),
|
|
632
|
+
isThinking,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
561
637
|
function annotateToolRun(context, title, metadata) {
|
|
562
638
|
context.metadata({
|
|
563
639
|
title,
|
|
@@ -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);
|
|
27
|
+
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.architectSystemPrompt);
|
|
28
28
|
const services = {
|
|
29
29
|
manager,
|
|
30
30
|
sessions: sessionService,
|
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
|
-
'- For medium or large tasks,
|
|
30
|
+
'- For medium or large tasks, delegate dual-engineer exploration to two engineers, then task the `architect` subagent to synthesize their independent plans into one stronger plan.',
|
|
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,6 +76,21 @@ 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
|
+
architectSystemPrompt: [
|
|
80
|
+
'You are the Architect. Your role is to synthesize two independent engineering plans into one stronger, unified plan.',
|
|
81
|
+
'Compare the lead and challenger plans on clarity, feasibility, risk, and fit to the user request.',
|
|
82
|
+
'Prefer the simplest path that fully addresses the goal. Surface tradeoffs honestly.',
|
|
83
|
+
'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
|
|
84
|
+
'Do not editorialize or over-explain. Be direct and concise.',
|
|
85
|
+
'',
|
|
86
|
+
'Use this output format exactly:',
|
|
87
|
+
'## Synthesis',
|
|
88
|
+
'<combined plan>',
|
|
89
|
+
'## Recommended Question',
|
|
90
|
+
'<question or NONE>',
|
|
91
|
+
'## Recommended Answer',
|
|
92
|
+
'<answer or NONE>',
|
|
93
|
+
].join('\n'),
|
|
79
94
|
contextWarnings: {
|
|
80
95
|
moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
|
|
81
96
|
high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
|
|
@@ -18,7 +18,8 @@ export declare class TeamOrchestrator {
|
|
|
18
18
|
private readonly teamStore;
|
|
19
19
|
private readonly transcriptStore;
|
|
20
20
|
private readonly engineerSessionPrompt;
|
|
21
|
-
|
|
21
|
+
private readonly architectSystemPrompt;
|
|
22
|
+
constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, architectSystemPrompt: string);
|
|
22
23
|
getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
|
|
23
24
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
24
25
|
recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
|
|
@@ -44,6 +45,9 @@ export declare class TeamOrchestrator {
|
|
|
44
45
|
challengerEngineer: EngineerName;
|
|
45
46
|
model?: string;
|
|
46
47
|
abortSignal?: AbortSignal;
|
|
48
|
+
onLeadEvent?: ClaudeSessionEventHandler;
|
|
49
|
+
onChallengerEvent?: ClaudeSessionEventHandler;
|
|
50
|
+
onSynthesisEvent?: ClaudeSessionEventHandler;
|
|
47
51
|
}): Promise<SynthesizedPlanResult>;
|
|
48
52
|
private updateEngineer;
|
|
49
53
|
private reserveEngineer;
|
|
@@ -6,11 +6,13 @@ export class TeamOrchestrator {
|
|
|
6
6
|
teamStore;
|
|
7
7
|
transcriptStore;
|
|
8
8
|
engineerSessionPrompt;
|
|
9
|
-
|
|
9
|
+
architectSystemPrompt;
|
|
10
|
+
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, architectSystemPrompt) {
|
|
10
11
|
this.sessions = sessions;
|
|
11
12
|
this.teamStore = teamStore;
|
|
12
13
|
this.transcriptStore = transcriptStore;
|
|
13
14
|
this.engineerSessionPrompt = engineerSessionPrompt;
|
|
15
|
+
this.architectSystemPrompt = architectSystemPrompt;
|
|
14
16
|
}
|
|
15
17
|
async getOrCreateTeam(cwd, teamId) {
|
|
16
18
|
const existing = await this.teamStore.getTeam(cwd, teamId);
|
|
@@ -203,6 +205,7 @@ export class TeamOrchestrator {
|
|
|
203
205
|
message: buildPlanDraftRequest('lead', input.request),
|
|
204
206
|
model: input.model,
|
|
205
207
|
abortSignal: input.abortSignal,
|
|
208
|
+
onEvent: input.onLeadEvent,
|
|
206
209
|
}),
|
|
207
210
|
this.dispatchEngineer({
|
|
208
211
|
teamId: input.teamId,
|
|
@@ -212,6 +215,7 @@ export class TeamOrchestrator {
|
|
|
212
215
|
message: buildPlanDraftRequest('challenger', input.request),
|
|
213
216
|
model: input.model,
|
|
214
217
|
abortSignal: input.abortSignal,
|
|
218
|
+
onEvent: input.onChallengerEvent,
|
|
215
219
|
}),
|
|
216
220
|
]);
|
|
217
221
|
const drafts = [
|
|
@@ -221,7 +225,7 @@ export class TeamOrchestrator {
|
|
|
221
225
|
const synthesisResult = await this.sessions.runTask({
|
|
222
226
|
cwd: input.cwd,
|
|
223
227
|
prompt: buildSynthesisPrompt(input.request, drafts),
|
|
224
|
-
systemPrompt: buildSynthesisSystemPrompt(),
|
|
228
|
+
systemPrompt: buildSynthesisSystemPrompt(this.architectSystemPrompt),
|
|
225
229
|
persistSession: false,
|
|
226
230
|
includePartialMessages: false,
|
|
227
231
|
permissionMode: 'acceptEdits',
|
|
@@ -230,7 +234,7 @@ export class TeamOrchestrator {
|
|
|
230
234
|
effort: 'high',
|
|
231
235
|
settingSources: ['user', 'project', 'local'],
|
|
232
236
|
abortSignal: input.abortSignal,
|
|
233
|
-
});
|
|
237
|
+
}, input.onSynthesisEvent);
|
|
234
238
|
const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
|
|
235
239
|
return {
|
|
236
240
|
teamId: input.teamId,
|
|
@@ -361,20 +365,8 @@ function buildPlanDraftRequest(perspective, request) {
|
|
|
361
365
|
`User request: ${request}`,
|
|
362
366
|
].join('\n');
|
|
363
367
|
}
|
|
364
|
-
function buildSynthesisSystemPrompt() {
|
|
365
|
-
return
|
|
366
|
-
'You are synthesizing two independent engineering plans into one stronger plan.',
|
|
367
|
-
'Compare them on clarity, feasibility, risk, and fit to the user request.',
|
|
368
|
-
'Prefer the simplest path that fully addresses the goal.',
|
|
369
|
-
'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
|
|
370
|
-
'Use this output format exactly:',
|
|
371
|
-
'## Synthesis',
|
|
372
|
-
'<combined plan>',
|
|
373
|
-
'## Recommended Question',
|
|
374
|
-
'<question or NONE>',
|
|
375
|
-
'## Recommended Answer',
|
|
376
|
-
'<answer or NONE>',
|
|
377
|
-
].join('\n');
|
|
368
|
+
function buildSynthesisSystemPrompt(architectSystemPrompt) {
|
|
369
|
+
return architectSystemPrompt;
|
|
378
370
|
}
|
|
379
371
|
function buildSynthesisPrompt(request, drafts) {
|
|
380
372
|
return [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EngineerName, ManagerPromptRegistry } from '../types/contracts.js';
|
|
2
2
|
export declare const AGENT_CTO = "cto";
|
|
3
|
+
export declare const AGENT_ARCHITECT = "architect";
|
|
3
4
|
export declare const ENGINEER_AGENT_IDS: {
|
|
4
5
|
readonly Tom: "tom";
|
|
5
6
|
readonly John: "john";
|
|
@@ -41,5 +42,13 @@ export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry,
|
|
|
41
42
|
permission: AgentPermission;
|
|
42
43
|
prompt: string;
|
|
43
44
|
};
|
|
45
|
+
export declare function buildArchitectAgentConfig(prompts: ManagerPromptRegistry): {
|
|
46
|
+
description: string;
|
|
47
|
+
mode: "subagent";
|
|
48
|
+
hidden: boolean;
|
|
49
|
+
color: string;
|
|
50
|
+
permission: AgentPermission;
|
|
51
|
+
prompt: string;
|
|
52
|
+
};
|
|
44
53
|
export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
|
|
45
54
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TEAM_ENGINEERS } from '../team/roster.js';
|
|
2
2
|
export const AGENT_CTO = 'cto';
|
|
3
|
+
export const AGENT_ARCHITECT = 'architect';
|
|
3
4
|
export const ENGINEER_AGENT_IDS = {
|
|
4
5
|
Tom: 'tom',
|
|
5
6
|
John: 'john',
|
|
@@ -10,7 +11,6 @@ export const ENGINEER_AGENT_IDS = {
|
|
|
10
11
|
export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
|
|
11
12
|
const CTO_ONLY_TOOL_IDS = [
|
|
12
13
|
'team_status',
|
|
13
|
-
'plan_with_team',
|
|
14
14
|
'reset_engineer',
|
|
15
15
|
'list_transcripts',
|
|
16
16
|
'list_history',
|
|
@@ -38,6 +38,27 @@ const CTO_READONLY_TOOLS = {
|
|
|
38
38
|
todoread: 'allow',
|
|
39
39
|
question: 'allow',
|
|
40
40
|
};
|
|
41
|
+
function buildArchitectPermissions() {
|
|
42
|
+
const denied = {};
|
|
43
|
+
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
44
|
+
denied[toolId] = 'deny';
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
'*': 'deny',
|
|
48
|
+
read: 'allow',
|
|
49
|
+
grep: 'allow',
|
|
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',
|
|
59
|
+
...denied,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
41
62
|
function buildCtoPermissions() {
|
|
42
63
|
const denied = {};
|
|
43
64
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
@@ -51,6 +72,7 @@ function buildCtoPermissions() {
|
|
|
51
72
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
52
73
|
taskPermissions[ENGINEER_AGENT_IDS[engineer]] = 'allow';
|
|
53
74
|
}
|
|
75
|
+
taskPermissions[AGENT_ARCHITECT] = 'allow';
|
|
54
76
|
return {
|
|
55
77
|
'*': 'deny',
|
|
56
78
|
...CTO_READONLY_TOOLS,
|
|
@@ -89,6 +111,16 @@ export function buildEngineerAgentConfig(prompts, engineer) {
|
|
|
89
111
|
prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
|
|
90
112
|
};
|
|
91
113
|
}
|
|
114
|
+
export function buildArchitectAgentConfig(prompts) {
|
|
115
|
+
return {
|
|
116
|
+
description: 'Synthesizes two engineer plan drafts into one stronger, actionable plan.',
|
|
117
|
+
mode: 'subagent',
|
|
118
|
+
hidden: false,
|
|
119
|
+
color: '#D97757',
|
|
120
|
+
permission: buildArchitectPermissions(),
|
|
121
|
+
prompt: prompts.architectSystemPrompt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
92
124
|
export function denyRestrictedToolsGlobally(permissions) {
|
|
93
125
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
94
126
|
permissions[toolId] ??= 'deny';
|
|
@@ -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, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
|
|
5
|
+
import { AGENT_CTO, AGENT_ARCHITECT, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, buildArchitectAgentConfig, 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,6 +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[AGENT_ARCHITECT] ??= buildArchitectAgentConfig(managerPromptRegistry);
|
|
18
19
|
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
19
20
|
config.agent[ENGINEER_AGENT_IDS[engineer]] ??= buildEngineerAgentConfig(managerPromptRegistry, engineer);
|
|
20
21
|
}
|
|
@@ -126,9 +127,12 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
126
127
|
challengerEngineer: args.challengerEngineer,
|
|
127
128
|
model: args.model,
|
|
128
129
|
abortSignal: context.abort,
|
|
130
|
+
onLeadEvent: (event) => reportClaudeEvent(context, args.leadEngineer, event),
|
|
131
|
+
onChallengerEvent: (event) => reportClaudeEvent(context, args.challengerEngineer, event),
|
|
132
|
+
onSynthesisEvent: (event) => reportArchitectEvent(context, event),
|
|
129
133
|
});
|
|
130
134
|
context.metadata({
|
|
131
|
-
title: '✅
|
|
135
|
+
title: '✅ Architect finished',
|
|
132
136
|
metadata: {
|
|
133
137
|
teamId: result.teamId,
|
|
134
138
|
lead: result.leadEngineer,
|
|
@@ -558,6 +562,78 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
558
562
|
});
|
|
559
563
|
}
|
|
560
564
|
}
|
|
565
|
+
function reportArchitectEvent(context, event) {
|
|
566
|
+
if (event.type === 'error') {
|
|
567
|
+
context.metadata({
|
|
568
|
+
title: `❌ Architect hit an error`,
|
|
569
|
+
metadata: {
|
|
570
|
+
sessionId: event.sessionId,
|
|
571
|
+
error: event.text.slice(0, 200),
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (event.type === 'init') {
|
|
577
|
+
context.metadata({
|
|
578
|
+
title: `⚡ Architect session ready`,
|
|
579
|
+
metadata: {
|
|
580
|
+
sessionId: event.sessionId,
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (event.type === 'tool_call') {
|
|
586
|
+
let toolName;
|
|
587
|
+
let toolId;
|
|
588
|
+
let toolArgs;
|
|
589
|
+
try {
|
|
590
|
+
const parsed = JSON.parse(event.text);
|
|
591
|
+
toolName = parsed.name;
|
|
592
|
+
toolId = parsed.id;
|
|
593
|
+
if (typeof parsed.input === 'string') {
|
|
594
|
+
try {
|
|
595
|
+
toolArgs = JSON.parse(parsed.input);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
toolArgs = parsed.input;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
toolArgs = parsed.input;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// event.text is not valid JSON — fall back to generic title
|
|
607
|
+
}
|
|
608
|
+
const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
|
|
609
|
+
context.metadata({
|
|
610
|
+
title: toolDescription
|
|
611
|
+
? `⚡ Architect → ${toolDescription}`
|
|
612
|
+
: toolName
|
|
613
|
+
? `⚡ Architect → ${toolName}`
|
|
614
|
+
: `⚡ Architect is using Claude Code tools`,
|
|
615
|
+
metadata: {
|
|
616
|
+
sessionId: event.sessionId,
|
|
617
|
+
...(toolName !== undefined && { toolName }),
|
|
618
|
+
...(toolId !== undefined && { toolId }),
|
|
619
|
+
...(toolArgs !== undefined && { toolArgs }),
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (event.type === 'assistant' || event.type === 'partial') {
|
|
625
|
+
const isThinking = event.text.startsWith('<thinking>');
|
|
626
|
+
const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
|
|
627
|
+
context.metadata({
|
|
628
|
+
title: `⚡ Architect ${stateLabel}`,
|
|
629
|
+
metadata: {
|
|
630
|
+
sessionId: event.sessionId,
|
|
631
|
+
preview: event.text.slice(0, 160),
|
|
632
|
+
isThinking,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
561
637
|
function annotateToolRun(context, title, metadata) {
|
|
562
638
|
context.metadata({
|
|
563
639
|
title,
|
|
@@ -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);
|
|
27
|
+
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, managerPromptRegistry.architectSystemPrompt);
|
|
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
|
-
'- For medium or large tasks,
|
|
30
|
+
'- For medium or large tasks, delegate dual-engineer exploration to two engineers, then task the `architect` subagent to synthesize their independent plans into one stronger plan.',
|
|
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,6 +76,21 @@ 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
|
+
architectSystemPrompt: [
|
|
80
|
+
'You are the Architect. Your role is to synthesize two independent engineering plans into one stronger, unified plan.',
|
|
81
|
+
'Compare the lead and challenger plans on clarity, feasibility, risk, and fit to the user request.',
|
|
82
|
+
'Prefer the simplest path that fully addresses the goal. Surface tradeoffs honestly.',
|
|
83
|
+
'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
|
|
84
|
+
'Do not editorialize or over-explain. Be direct and concise.',
|
|
85
|
+
'',
|
|
86
|
+
'Use this output format exactly:',
|
|
87
|
+
'## Synthesis',
|
|
88
|
+
'<combined plan>',
|
|
89
|
+
'## Recommended Question',
|
|
90
|
+
'<question or NONE>',
|
|
91
|
+
'## Recommended Answer',
|
|
92
|
+
'<answer or NONE>',
|
|
93
|
+
].join('\n'),
|
|
79
94
|
contextWarnings: {
|
|
80
95
|
moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
|
|
81
96
|
high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
|
|
@@ -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, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from '../src/plugin/agent-hierarchy.js';
|
|
3
|
+
import { AGENT_CTO, AGENT_ARCHITECT, 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({
|
|
@@ -26,7 +26,6 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
26
26
|
todoread: 'allow',
|
|
27
27
|
question: 'allow',
|
|
28
28
|
team_status: 'allow',
|
|
29
|
-
plan_with_team: 'allow',
|
|
30
29
|
reset_engineer: 'allow',
|
|
31
30
|
git_diff: 'allow',
|
|
32
31
|
git_commit: 'allow',
|
|
@@ -42,6 +41,7 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
42
41
|
maya: 'allow',
|
|
43
42
|
sara: 'allow',
|
|
44
43
|
alex: 'allow',
|
|
44
|
+
architect: 'allow',
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
it('configures every named engineer with only the claude bridge tool', async () => {
|
|
@@ -63,13 +63,33 @@ describe('ClaudeManagerPlugin', () => {
|
|
|
63
63
|
claude: 'allow',
|
|
64
64
|
git_diff: 'deny',
|
|
65
65
|
git_commit: 'deny',
|
|
66
|
-
plan_with_team: 'deny',
|
|
67
66
|
reset_engineer: 'deny',
|
|
68
67
|
});
|
|
69
68
|
expect(agent.permission).not.toHaveProperty('read');
|
|
70
69
|
expect(agent.permission).not.toHaveProperty('grep');
|
|
71
70
|
}
|
|
72
71
|
});
|
|
72
|
+
it('configures architect as a read-only subagent for plan synthesis', async () => {
|
|
73
|
+
const plugin = await ClaudeManagerPlugin({
|
|
74
|
+
worktree: '/tmp/project',
|
|
75
|
+
});
|
|
76
|
+
const config = {};
|
|
77
|
+
await plugin.config?.(config);
|
|
78
|
+
const agents = (config.agent ?? {});
|
|
79
|
+
const architect = agents[AGENT_ARCHITECT];
|
|
80
|
+
expect(architect).toBeDefined();
|
|
81
|
+
expect(architect.mode).toBe('subagent');
|
|
82
|
+
expect(architect.description.toLowerCase()).toContain('synthesiz');
|
|
83
|
+
expect(architect.permission).toMatchObject({
|
|
84
|
+
'*': 'deny',
|
|
85
|
+
read: 'allow',
|
|
86
|
+
grep: 'allow',
|
|
87
|
+
glob: 'allow',
|
|
88
|
+
list: 'allow',
|
|
89
|
+
codesearch: 'allow',
|
|
90
|
+
claude: 'deny',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
73
93
|
it('registers the named engineer bridge and team status tools', async () => {
|
|
74
94
|
const plugin = await ClaudeManagerPlugin({
|
|
75
95
|
worktree: '/tmp/project',
|
|
@@ -4,7 +4,8 @@ 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
6
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Task tool');
|
|
7
|
-
expect(managerPromptRegistry.ctoSystemPrompt).toContain('
|
|
7
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('dual-engineer');
|
|
8
|
+
expect(managerPromptRegistry.ctoSystemPrompt).toContain('architect');
|
|
8
9
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('question');
|
|
9
10
|
expect(managerPromptRegistry.ctoSystemPrompt).toContain('Tom, John, Maya, Sara, and Alex');
|
|
10
11
|
expect(managerPromptRegistry.ctoSystemPrompt).not.toContain('clear_session');
|
|
@@ -28,4 +29,12 @@ describe('managerPromptRegistry', () => {
|
|
|
28
29
|
expect(managerPromptRegistry.contextWarnings.high).toContain('{turns}');
|
|
29
30
|
expect(managerPromptRegistry.contextWarnings.critical).toContain('near capacity');
|
|
30
31
|
});
|
|
32
|
+
it('gives the architect synthesis guidance with complete output format', () => {
|
|
33
|
+
expect(managerPromptRegistry.architectSystemPrompt).toContain('Architect');
|
|
34
|
+
expect(managerPromptRegistry.architectSystemPrompt).toContain('synthesiz');
|
|
35
|
+
expect(managerPromptRegistry.architectSystemPrompt).toContain('two independent');
|
|
36
|
+
expect(managerPromptRegistry.architectSystemPrompt).toContain('## Synthesis');
|
|
37
|
+
expect(managerPromptRegistry.architectSystemPrompt).toContain('## Recommended Question');
|
|
38
|
+
expect(managerPromptRegistry.architectSystemPrompt).toContain('## Recommended Answer');
|
|
39
|
+
});
|
|
31
40
|
});
|
|
@@ -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', 'Architect 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', 'Architect prompt');
|
|
210
210
|
await orchestrator.getOrCreateTeam(tempRoot, 'cto-1');
|
|
211
211
|
await store.updateTeam(tempRoot, 'cto-1', (team) => ({
|
|
212
212
|
...team,
|
|
@@ -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', 'Architect prompt');
|
|
39
39
|
const first = await orchestrator.dispatchEngineer({
|
|
40
40
|
teamId: 'team-1',
|
|
41
41
|
cwd: tempRoot,
|
|
@@ -74,7 +74,7 @@ describe('TeamOrchestrator', () => {
|
|
|
74
74
|
it('rejects work when the same engineer is already busy', async () => {
|
|
75
75
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
76
76
|
const store = new TeamStateStore('.state');
|
|
77
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
|
|
77
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, store, { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
|
|
78
78
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|
|
79
79
|
await store.saveTeam({
|
|
80
80
|
...team,
|
|
@@ -112,7 +112,7 @@ describe('TeamOrchestrator', () => {
|
|
|
112
112
|
events: [],
|
|
113
113
|
finalText: '## Synthesis\nCombined plan\n## Recommended Question\nShould we migrate now?\n## Recommended Answer\nNo, defer it.',
|
|
114
114
|
});
|
|
115
|
-
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
|
|
115
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
|
|
116
116
|
const result = await orchestrator.planWithTeam({
|
|
117
117
|
teamId: 'team-1',
|
|
118
118
|
cwd: tempRoot,
|
|
@@ -126,9 +126,58 @@ describe('TeamOrchestrator', () => {
|
|
|
126
126
|
expect(result.recommendedAnswer).toBe('No, defer it.');
|
|
127
127
|
expect(runTask).toHaveBeenCalledTimes(3);
|
|
128
128
|
});
|
|
129
|
+
it('invokes lead, challenger, and synthesis event callbacks', async () => {
|
|
130
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
131
|
+
const runTask = vi.fn(async (input, onEvent) => {
|
|
132
|
+
// Simulate event callbacks being invoked by the session
|
|
133
|
+
if (onEvent) {
|
|
134
|
+
await Promise.resolve(onEvent({ type: 'init', text: 'initialized' }));
|
|
135
|
+
}
|
|
136
|
+
// Return appropriate result based on call count
|
|
137
|
+
const calls = runTask.mock.calls.length;
|
|
138
|
+
if (calls === 1) {
|
|
139
|
+
return {
|
|
140
|
+
sessionId: 'ses_tom',
|
|
141
|
+
events: [{ type: 'init', text: 'initialized', sessionId: 'ses_tom' }],
|
|
142
|
+
finalText: 'Lead plan',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
else if (calls === 2) {
|
|
146
|
+
return {
|
|
147
|
+
sessionId: 'ses_maya',
|
|
148
|
+
events: [{ type: 'init', text: 'initialized', sessionId: 'ses_maya' }],
|
|
149
|
+
finalText: 'Challenger plan',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
return {
|
|
154
|
+
sessionId: undefined,
|
|
155
|
+
events: [{ type: 'init', text: 'initialized', sessionId: undefined }],
|
|
156
|
+
finalText: '## Synthesis\nSynthesis\n## Recommended Question\nNONE\n## Recommended Answer\nNONE',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const orchestrator = new TeamOrchestrator({ runTask }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
|
|
161
|
+
const onLeadEvent = vi.fn();
|
|
162
|
+
const onChallengerEvent = vi.fn();
|
|
163
|
+
const onSynthesisEvent = vi.fn();
|
|
164
|
+
await orchestrator.planWithTeam({
|
|
165
|
+
teamId: 'team-1',
|
|
166
|
+
cwd: tempRoot,
|
|
167
|
+
request: 'Plan the refactor',
|
|
168
|
+
leadEngineer: 'Tom',
|
|
169
|
+
challengerEngineer: 'Maya',
|
|
170
|
+
onLeadEvent,
|
|
171
|
+
onChallengerEvent,
|
|
172
|
+
onSynthesisEvent,
|
|
173
|
+
});
|
|
174
|
+
expect(onLeadEvent).toHaveBeenCalled();
|
|
175
|
+
expect(onChallengerEvent).toHaveBeenCalled();
|
|
176
|
+
expect(onSynthesisEvent).toHaveBeenCalled();
|
|
177
|
+
});
|
|
129
178
|
it('persists wrapper session memory for an engineer', async () => {
|
|
130
179
|
tempRoot = await mkdtemp(join(tmpdir(), 'team-orchestrator-'));
|
|
131
|
-
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt');
|
|
180
|
+
const orchestrator = new TeamOrchestrator({ runTask: vi.fn() }, new TeamStateStore('.state'), { appendEvents: vi.fn(async () => { }) }, 'Base engineer prompt', 'Architect prompt');
|
|
132
181
|
await orchestrator.recordWrapperSession(tempRoot, 'team-1', 'Tom', 'wrapper-tom');
|
|
133
182
|
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.');
|
|
134
183
|
const team = await orchestrator.getOrCreateTeam(tempRoot, 'team-1');
|