@doingdev/opencode-claude-manager-plugin 0.1.55 → 0.1.57
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 +10 -3
- package/dist/manager/team-orchestrator.js +108 -17
- package/dist/plugin/agent-hierarchy.js +7 -3
- package/dist/plugin/claude-manager.plugin.d.ts +8 -0
- package/dist/plugin/claude-manager.plugin.js +38 -15
- package/dist/prompts/registry.js +107 -57
- package/dist/src/manager/team-orchestrator.d.ts +12 -5
- package/dist/src/manager/team-orchestrator.js +111 -20
- package/dist/src/plugin/agent-hierarchy.d.ts +2 -2
- package/dist/src/plugin/agent-hierarchy.js +15 -20
- package/dist/src/plugin/claude-manager.plugin.d.ts +8 -0
- package/dist/src/plugin/claude-manager.plugin.js +51 -27
- package/dist/src/plugin/service-factory.js +1 -1
- package/dist/src/prompts/registry.js +115 -57
- package/dist/src/types/contracts.d.ts +4 -1
- package/dist/test/claude-manager.plugin.test.js +94 -13
- package/dist/test/prompt-registry.test.js +26 -12
- package/dist/test/report-claude-event.test.js +16 -3
- package/dist/test/team-orchestrator.test.js +127 -7
- package/dist/types/contracts.d.ts +1 -1
- package/package.json +1 -1
|
@@ -33,7 +33,7 @@ export declare class TeamOrchestrator {
|
|
|
33
33
|
clearSession?: boolean;
|
|
34
34
|
clearHistory?: boolean;
|
|
35
35
|
}): Promise<void>;
|
|
36
|
-
dispatchEngineer(input: DispatchEngineerInput): Promise<EngineerTaskResult>;
|
|
36
|
+
dispatchEngineer(input: DispatchEngineerInput, retryCount?: number): Promise<EngineerTaskResult>;
|
|
37
37
|
static classifyError(error: unknown): EngineerFailureResult & {
|
|
38
38
|
cause: unknown;
|
|
39
39
|
};
|
|
@@ -41,8 +41,8 @@ export declare class TeamOrchestrator {
|
|
|
41
41
|
teamId: string;
|
|
42
42
|
cwd: string;
|
|
43
43
|
request: string;
|
|
44
|
-
leadEngineer
|
|
45
|
-
challengerEngineer
|
|
44
|
+
leadEngineer?: EngineerName;
|
|
45
|
+
challengerEngineer?: EngineerName;
|
|
46
46
|
model?: string;
|
|
47
47
|
abortSignal?: AbortSignal;
|
|
48
48
|
onLeadEvent?: ClaudeSessionEventHandler;
|
|
@@ -53,7 +53,14 @@ export declare class TeamOrchestrator {
|
|
|
53
53
|
private reserveEngineer;
|
|
54
54
|
private getEngineerState;
|
|
55
55
|
private normalizeTeamRecord;
|
|
56
|
+
getAvailableEngineers(team: TeamRecord): EngineerName[];
|
|
57
|
+
selectPlanEngineers(cwd: string, teamId: string, preferredLead?: EngineerName, preferredChallenger?: EngineerName): Promise<{
|
|
58
|
+
lead: EngineerName;
|
|
59
|
+
challenger: EngineerName;
|
|
60
|
+
}>;
|
|
56
61
|
private buildSessionSystemPrompt;
|
|
57
62
|
private buildEngineerPrompt;
|
|
58
63
|
}
|
|
64
|
+
export declare function getFailureGuidanceText(failureKind: string): string;
|
|
65
|
+
export declare function createActionableError(failure: EngineerFailureResult, originalError: unknown): Error;
|
|
59
66
|
export {};
|
|
@@ -88,7 +88,7 @@ export class TeamOrchestrator {
|
|
|
88
88
|
context: options?.clearSession ? createEmptyEngineerRecord(engineer).context : entry.context,
|
|
89
89
|
}));
|
|
90
90
|
}
|
|
91
|
-
async dispatchEngineer(input) {
|
|
91
|
+
async dispatchEngineer(input, retryCount = 0) {
|
|
92
92
|
const team = await this.getOrCreateTeam(input.cwd, input.teamId);
|
|
93
93
|
const engineerState = this.getEngineerState(team, input.engineer);
|
|
94
94
|
await this.reserveEngineer(input.cwd, input.teamId, input.engineer);
|
|
@@ -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,
|
|
@@ -162,6 +161,39 @@ export class TeamOrchestrator {
|
|
|
162
161
|
busy: false,
|
|
163
162
|
busySince: null,
|
|
164
163
|
}));
|
|
164
|
+
// Handle context exhaustion with automatic retry (max 1 retry)
|
|
165
|
+
const classified = TeamOrchestrator.classifyError(error);
|
|
166
|
+
if (classified.failureKind === 'contextExhausted' && retryCount === 0) {
|
|
167
|
+
// Reset the engineer's session and retry once with fresh session
|
|
168
|
+
await this.resetEngineer(input.cwd, input.teamId, input.engineer, {
|
|
169
|
+
clearSession: true,
|
|
170
|
+
clearHistory: false,
|
|
171
|
+
});
|
|
172
|
+
// Emit status event before retry
|
|
173
|
+
await input.onEvent?.({
|
|
174
|
+
type: 'status',
|
|
175
|
+
text: 'Context exhausted; resetting session and retrying once with a fresh session.',
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
// Retry dispatch with fresh session (retryCount=1 prevents infinite loop)
|
|
179
|
+
// Use the exact same assignment message without modification
|
|
180
|
+
return await this.dispatchEngineer(input, 1);
|
|
181
|
+
}
|
|
182
|
+
catch (retryError) {
|
|
183
|
+
// If retry also fails with a different error, preserve retry failure info
|
|
184
|
+
const retryClassified = TeamOrchestrator.classifyError(retryError);
|
|
185
|
+
if (retryClassified.failureKind !== classified.failureKind) {
|
|
186
|
+
// Create an error that shows both failures
|
|
187
|
+
const combinedMessage = `Initial: ${classified.failureKind} (${classified.message})\n` +
|
|
188
|
+
`After retry: ${retryClassified.failureKind} (${retryClassified.message})`;
|
|
189
|
+
const combinedError = new Error(combinedMessage);
|
|
190
|
+
Object.assign(combinedError, { cause: retryError });
|
|
191
|
+
throw combinedError;
|
|
192
|
+
}
|
|
193
|
+
// Same error type on retry, throw the retry error (more recent state)
|
|
194
|
+
throw retryError;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
165
197
|
throw error;
|
|
166
198
|
}
|
|
167
199
|
}
|
|
@@ -193,14 +225,13 @@ export class TeamOrchestrator {
|
|
|
193
225
|
};
|
|
194
226
|
}
|
|
195
227
|
async planWithTeam(input) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
228
|
+
// Auto-select engineers if not provided
|
|
229
|
+
const { lead: leadEngineer, challenger: challengerEngineer } = await this.selectPlanEngineers(input.cwd, input.teamId, input.leadEngineer, input.challengerEngineer);
|
|
199
230
|
const [leadDraft, challengerDraft] = await Promise.all([
|
|
200
231
|
this.dispatchEngineer({
|
|
201
232
|
teamId: input.teamId,
|
|
202
233
|
cwd: input.cwd,
|
|
203
|
-
engineer:
|
|
234
|
+
engineer: leadEngineer,
|
|
204
235
|
mode: 'explore',
|
|
205
236
|
message: buildPlanDraftRequest('lead', input.request),
|
|
206
237
|
model: input.model,
|
|
@@ -210,7 +241,7 @@ export class TeamOrchestrator {
|
|
|
210
241
|
this.dispatchEngineer({
|
|
211
242
|
teamId: input.teamId,
|
|
212
243
|
cwd: input.cwd,
|
|
213
|
-
engineer:
|
|
244
|
+
engineer: challengerEngineer,
|
|
214
245
|
mode: 'explore',
|
|
215
246
|
message: buildPlanDraftRequest('challenger', input.request),
|
|
216
247
|
model: input.model,
|
|
@@ -224,8 +255,7 @@ export class TeamOrchestrator {
|
|
|
224
255
|
];
|
|
225
256
|
const synthesisResult = await this.sessions.runTask({
|
|
226
257
|
cwd: input.cwd,
|
|
227
|
-
prompt: buildSynthesisPrompt(input.request, drafts)
|
|
228
|
-
systemPrompt: buildSynthesisSystemPrompt(this.planSynthesisPrompt),
|
|
258
|
+
prompt: `${this.planSynthesisPrompt}\n\n${buildSynthesisPrompt(input.request, drafts)}`,
|
|
229
259
|
persistSession: false,
|
|
230
260
|
includePartialMessages: false,
|
|
231
261
|
permissionMode: 'acceptEdits',
|
|
@@ -239,8 +269,8 @@ export class TeamOrchestrator {
|
|
|
239
269
|
return {
|
|
240
270
|
teamId: input.teamId,
|
|
241
271
|
request: input.request,
|
|
242
|
-
leadEngineer
|
|
243
|
-
challengerEngineer
|
|
272
|
+
leadEngineer,
|
|
273
|
+
challengerEngineer,
|
|
244
274
|
drafts,
|
|
245
275
|
synthesis: parsedSynthesis.synthesis,
|
|
246
276
|
recommendedQuestion: parsedSynthesis.recommendedQuestion,
|
|
@@ -296,6 +326,47 @@ export class TeamOrchestrator {
|
|
|
296
326
|
engineers: createEmptyTeamRecord(team.id, team.cwd).engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
|
|
297
327
|
};
|
|
298
328
|
}
|
|
329
|
+
getAvailableEngineers(team) {
|
|
330
|
+
const now = Date.now();
|
|
331
|
+
return team.engineers
|
|
332
|
+
.filter((engineer) => {
|
|
333
|
+
if (!engineer.busy)
|
|
334
|
+
return true;
|
|
335
|
+
// If an engineer has been marked busy but the lease expired, they're available
|
|
336
|
+
if (engineer.busySince) {
|
|
337
|
+
const leaseExpired = now - new Date(engineer.busySince).getTime() > BUSY_LEASE_MS;
|
|
338
|
+
return leaseExpired;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
})
|
|
342
|
+
.sort((a, b) => {
|
|
343
|
+
// Prefer engineers with lower context pressure and less-recently-used
|
|
344
|
+
const aContext = a.context.estimatedContextPercent ?? 0;
|
|
345
|
+
const bContext = b.context.estimatedContextPercent ?? 0;
|
|
346
|
+
if (aContext !== bContext) {
|
|
347
|
+
return aContext - bContext; // Lower context first
|
|
348
|
+
}
|
|
349
|
+
// If context is equal, prefer less-recently-used
|
|
350
|
+
const aTime = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0;
|
|
351
|
+
const bTime = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0;
|
|
352
|
+
return aTime - bTime; // Earlier usage time first
|
|
353
|
+
})
|
|
354
|
+
.map((engineer) => engineer.name);
|
|
355
|
+
}
|
|
356
|
+
async selectPlanEngineers(cwd, teamId, preferredLead, preferredChallenger) {
|
|
357
|
+
const team = await this.getOrCreateTeam(cwd, teamId);
|
|
358
|
+
const available = this.getAvailableEngineers(team);
|
|
359
|
+
if (available.length < 2) {
|
|
360
|
+
throw new Error(`Not enough available engineers for dual planning. Need 2, found ${available.length}.`);
|
|
361
|
+
}
|
|
362
|
+
const lead = preferredLead ?? available[0];
|
|
363
|
+
const foundChallenger = preferredChallenger ?? available.find((e) => e !== lead);
|
|
364
|
+
const challenger = foundChallenger ?? available[1];
|
|
365
|
+
if (lead === challenger) {
|
|
366
|
+
throw new Error('Cannot use the same engineer for both lead and challenger.');
|
|
367
|
+
}
|
|
368
|
+
return { lead, challenger };
|
|
369
|
+
}
|
|
299
370
|
buildSessionSystemPrompt(engineer, mode) {
|
|
300
371
|
return [
|
|
301
372
|
this.engineerSessionPrompt,
|
|
@@ -365,9 +436,6 @@ function buildPlanDraftRequest(perspective, request) {
|
|
|
365
436
|
`User request: ${request}`,
|
|
366
437
|
].join('\n');
|
|
367
438
|
}
|
|
368
|
-
function buildSynthesisSystemPrompt(planSynthesisPrompt) {
|
|
369
|
-
return planSynthesisPrompt;
|
|
370
|
-
}
|
|
371
439
|
function buildSynthesisPrompt(request, drafts) {
|
|
372
440
|
return [
|
|
373
441
|
`User request: ${request}`,
|
|
@@ -403,3 +471,26 @@ function normalizeOptionalSection(value) {
|
|
|
403
471
|
function escapeRegExp(value) {
|
|
404
472
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
405
473
|
}
|
|
474
|
+
export function getFailureGuidanceText(failureKind) {
|
|
475
|
+
switch (failureKind) {
|
|
476
|
+
case 'contextExhausted':
|
|
477
|
+
return 'Context exhausted after using all available tokens. The engineer was reset and the assignment retried once. If it still fails, the task may be too large; consider breaking it into smaller steps.';
|
|
478
|
+
case 'engineerBusy':
|
|
479
|
+
return 'This engineer is currently working on another assignment. Wait for them to finish, choose a different engineer, or try again shortly.';
|
|
480
|
+
case 'toolDenied':
|
|
481
|
+
return 'A tool permission was denied during the assignment. Check the approval policy and tool permissions, then retry.';
|
|
482
|
+
case 'aborted':
|
|
483
|
+
return 'The assignment was cancelled by the user or an abort signal was triggered. Review the request and try again.';
|
|
484
|
+
case 'sdkError':
|
|
485
|
+
return 'An SDK error occurred during the assignment. Check logs for details, ensure the Claude session is healthy, and retry.';
|
|
486
|
+
default:
|
|
487
|
+
return 'An unknown error occurred during the assignment. Check logs and retry.';
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
export function createActionableError(failure, originalError) {
|
|
491
|
+
const guidance = getFailureGuidanceText(failure.failureKind);
|
|
492
|
+
const errorMessage = `[${failure.failureKind}] ${failure.message}\n\n` + `Next steps: ${guidance}`;
|
|
493
|
+
const error = new Error(errorMessage);
|
|
494
|
+
Object.assign(error, { cause: originalError });
|
|
495
|
+
return error;
|
|
496
|
+
}
|
|
@@ -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 {
|
|
@@ -94,7 +98,7 @@ export function buildCtoAgentConfig(prompts) {
|
|
|
94
98
|
}
|
|
95
99
|
export function buildEngineerAgentConfig(prompts, engineer) {
|
|
96
100
|
return {
|
|
97
|
-
description: `${engineer} is a persistent engineer who works through one Claude Code session and remembers prior turns.`,
|
|
101
|
+
description: `${engineer} is a persistent engineer who works through one Claude Code session and remembers prior turns. Receives structured assignments (goal, mode, context, acceptance criteria, relevant paths, constraints, verification).`,
|
|
98
102
|
mode: 'subagent',
|
|
99
103
|
hidden: false,
|
|
100
104
|
color: '#D97757',
|
|
@@ -104,7 +108,7 @@ export function buildEngineerAgentConfig(prompts, engineer) {
|
|
|
104
108
|
}
|
|
105
109
|
export function buildTeamPlannerAgentConfig(prompts) {
|
|
106
110
|
return {
|
|
107
|
-
description: 'Runs dual-engineer planning by calling plan_with_team.
|
|
111
|
+
description: 'Runs dual-engineer planning by calling plan_with_team. Automatically selects two non-overlapping available engineers if engineer names are not provided.',
|
|
108
112
|
mode: 'subagent',
|
|
109
113
|
hidden: false,
|
|
110
114
|
color: '#D97757',
|
|
@@ -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;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin';
|
|
2
2
|
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
3
3
|
import { isEngineerName } from '../team/roster.js';
|
|
4
|
-
import { TeamOrchestrator } from '../manager/team-orchestrator.js';
|
|
4
|
+
import { TeamOrchestrator, createActionableError, getFailureGuidanceText, } from '../manager/team-orchestrator.js';
|
|
5
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'];
|
|
@@ -105,30 +105,32 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
105
105
|
},
|
|
106
106
|
}),
|
|
107
107
|
plan_with_team: tool({
|
|
108
|
-
description: 'Run dual-engineer plan synthesis. Two engineers explore in parallel (lead + challenger), then their plans are synthesized into one stronger plan.',
|
|
108
|
+
description: 'Run dual-engineer plan synthesis. Two engineers explore in parallel (lead + challenger), then their plans are synthesized into one stronger plan. Automatically selects distinct available engineers if names are not provided.',
|
|
109
109
|
args: {
|
|
110
110
|
request: tool.schema.string().min(1),
|
|
111
|
-
leadEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
112
|
-
challengerEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
111
|
+
leadEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']).optional(),
|
|
112
|
+
challengerEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']).optional(),
|
|
113
113
|
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
114
114
|
},
|
|
115
115
|
async execute(args, context) {
|
|
116
116
|
const teamId = getActiveTeamSession(context.worktree) ?? context.sessionID;
|
|
117
|
+
// Pre-determine engineers for event labeling (using orchestrator selection logic)
|
|
118
|
+
const { lead, challenger } = await services.orchestrator.selectPlanEngineers(context.worktree, teamId, args.leadEngineer, args.challengerEngineer);
|
|
117
119
|
annotateToolRun(context, 'Running dual-engineer plan synthesis', {
|
|
118
120
|
teamId,
|
|
119
|
-
lead
|
|
120
|
-
challenger
|
|
121
|
+
lead,
|
|
122
|
+
challenger,
|
|
121
123
|
});
|
|
122
124
|
const result = await services.orchestrator.planWithTeam({
|
|
123
125
|
teamId,
|
|
124
126
|
cwd: context.worktree,
|
|
125
127
|
request: args.request,
|
|
126
|
-
leadEngineer:
|
|
127
|
-
challengerEngineer:
|
|
128
|
+
leadEngineer: lead,
|
|
129
|
+
challengerEngineer: challenger,
|
|
128
130
|
model: args.model,
|
|
129
131
|
abortSignal: context.abort,
|
|
130
|
-
onLeadEvent: (event) => reportClaudeEvent(context,
|
|
131
|
-
onChallengerEvent: (event) => reportClaudeEvent(context,
|
|
132
|
+
onLeadEvent: (event) => reportClaudeEvent(context, lead, event),
|
|
133
|
+
onChallengerEvent: (event) => reportClaudeEvent(context, challenger, event),
|
|
132
134
|
onSynthesisEvent: (event) => reportPlanSynthesisEvent(context, event),
|
|
133
135
|
});
|
|
134
136
|
context.metadata({
|
|
@@ -386,6 +388,7 @@ async function runEngineerAssignment(input, context) {
|
|
|
386
388
|
failure.teamId = input.teamId;
|
|
387
389
|
failure.engineer = input.engineer;
|
|
388
390
|
failure.mode = input.mode;
|
|
391
|
+
const guidance = getFailureGuidanceText(failure.failureKind);
|
|
389
392
|
context.metadata({
|
|
390
393
|
title: `❌ ${input.engineer} failed (${failure.failureKind})`,
|
|
391
394
|
metadata: {
|
|
@@ -393,9 +396,10 @@ async function runEngineerAssignment(input, context) {
|
|
|
393
396
|
engineer: failure.engineer,
|
|
394
397
|
failureKind: failure.failureKind,
|
|
395
398
|
message: failure.message.slice(0, 200),
|
|
399
|
+
guidance,
|
|
396
400
|
},
|
|
397
401
|
});
|
|
398
|
-
throw error;
|
|
402
|
+
throw createActionableError(failure, error);
|
|
399
403
|
}
|
|
400
404
|
await services.orchestrator.recordWrapperExchange(context.worktree, input.teamId, input.engineer, context.sessionID, input.mode, input.message, result.finalText);
|
|
401
405
|
context.metadata({
|
|
@@ -411,16 +415,25 @@ async function runEngineerAssignment(input, context) {
|
|
|
411
415
|
});
|
|
412
416
|
return result;
|
|
413
417
|
}
|
|
414
|
-
|
|
415
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Normalize an agent ID to its lowercase canonical form.
|
|
420
|
+
* Handles both uppercase (e.g., 'Tom') and lowercase (e.g., 'tom') inputs.
|
|
421
|
+
*/
|
|
422
|
+
export function normalizeAgentId(agentId) {
|
|
423
|
+
return agentId.toLowerCase();
|
|
424
|
+
}
|
|
425
|
+
export function engineerFromAgent(agentId) {
|
|
426
|
+
const normalized = normalizeAgentId(agentId);
|
|
427
|
+
const engineerEntry = Object.entries(ENGINEER_AGENT_IDS).find(([, value]) => value === normalized);
|
|
416
428
|
const engineer = engineerEntry?.[0];
|
|
417
429
|
if (!engineer || !isEngineerName(engineer)) {
|
|
418
430
|
throw new Error(`The claude tool can only be used from a named engineer agent. Received agent ${agentId}.`);
|
|
419
431
|
}
|
|
420
432
|
return engineer;
|
|
421
433
|
}
|
|
422
|
-
function isEngineerAgent(agentId) {
|
|
423
|
-
|
|
434
|
+
export function isEngineerAgent(agentId) {
|
|
435
|
+
const normalized = normalizeAgentId(agentId);
|
|
436
|
+
return Object.values(ENGINEER_AGENT_IDS).some((id) => id === normalized);
|
|
424
437
|
}
|
|
425
438
|
/**
|
|
426
439
|
* Resolves the team ID for an engineer session.
|
|
@@ -496,6 +509,16 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
496
509
|
});
|
|
497
510
|
return;
|
|
498
511
|
}
|
|
512
|
+
if (event.type === 'status') {
|
|
513
|
+
context.metadata({
|
|
514
|
+
title: `ℹ️ ${engineer}: ${event.text}`,
|
|
515
|
+
metadata: {
|
|
516
|
+
engineer,
|
|
517
|
+
status: event.text,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
499
522
|
if (event.type === 'init') {
|
|
500
523
|
context.metadata({
|
|
501
524
|
title: `⚡ ${engineer} session ready`,
|
package/dist/prompts/registry.js
CHANGED
|
@@ -1,68 +1,93 @@
|
|
|
1
1
|
export const managerPromptRegistry = {
|
|
2
2
|
ctoSystemPrompt: [
|
|
3
3
|
'You are a principal engineer orchestrating a team of AI-powered engineers.',
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
'',
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'-
|
|
11
|
-
'-
|
|
12
|
-
'-
|
|
13
|
-
'-
|
|
14
|
-
'-
|
|
15
|
-
'',
|
|
16
|
-
'
|
|
17
|
-
'-
|
|
18
|
-
'-
|
|
19
|
-
'-
|
|
20
|
-
'-
|
|
21
|
-
'',
|
|
22
|
-
'
|
|
23
|
-
'-
|
|
24
|
-
|
|
25
|
-
'-
|
|
26
|
-
'-
|
|
27
|
-
'',
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
|
|
32
|
-
'',
|
|
33
|
-
'
|
|
34
|
-
'-
|
|
35
|
-
'-
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'',
|
|
39
|
-
'
|
|
40
|
-
'-
|
|
41
|
-
'-
|
|
42
|
-
'-
|
|
43
|
-
'-
|
|
44
|
-
'-
|
|
45
|
-
'-
|
|
46
|
-
'-
|
|
47
|
-
'',
|
|
48
|
-
'
|
|
4
|
+
'Your role is to decompose work, delegate precisely, review diffs for production risks, and verify outcomes.',
|
|
5
|
+
'You do not write code. All edits go through engineers. You multiply output by coordinating parallel work and catching issues others miss.',
|
|
6
|
+
'',
|
|
7
|
+
'# Operating Loop: Orient → Classify → Plan → Delegate → Review → Verify → Close',
|
|
8
|
+
'',
|
|
9
|
+
'## Orient: Understand the request',
|
|
10
|
+
'- Extract what you can from the user message, codebase (read/grep/glob/codesearch), prior engineer results, and `websearch`/`webfetch` when relevant.',
|
|
11
|
+
'- Light investigation is fine: read files briefly to understand scope, check what already exists, avoid re-inventing.',
|
|
12
|
+
'- When a bug is reported, ask: what is the root cause? Do not assume. Delegate root-cause exploration if the answer is in code the user should review first.',
|
|
13
|
+
'- If requirements are vague or architecture is unclear, use `question` tool with 2–3 concrete options, your recommendation, and what breaks if user picks differently.',
|
|
14
|
+
'- Only ask when the decision will materially change scope, architecture, risk, or how you verify—and you cannot resolve it from context.',
|
|
15
|
+
'',
|
|
16
|
+
'## Classify: Frame the work',
|
|
17
|
+
'- Is this a bug fix, feature, refactor, or something else?',
|
|
18
|
+
'- What could go wrong? Is it reversible or irreversible? Can it fail in prod?',
|
|
19
|
+
'- Does it require careful rollout, data migration, observability, or backwards compatibility handling?',
|
|
20
|
+
'- Are there decisions the user has not explicitly made (architecture, scope, deployment strategy)?',
|
|
21
|
+
'',
|
|
22
|
+
'## Plan: Decompose into engineer work',
|
|
23
|
+
'- For small, focused tasks: delegate to a named engineer with structured context (goal, acceptance criteria, relevant files, constraints, verification).',
|
|
24
|
+
"- For medium or large tasks: use `task(subagent_type: 'team-planner', ...)` for dual-engineer exploration and plan synthesis.",
|
|
25
|
+
' - Team-planner automatically selects two non-overlapping engineers by availability and context; you may optionally specify lead and challenger.',
|
|
26
|
+
' - Challenger engineer identifies missing decisions, risks, and scope gaps before implementation.',
|
|
27
|
+
'- Break work into independent pieces that can run in parallel. Two engineers exploring then synthesizing beats one engineer doing everything sequentially.',
|
|
28
|
+
'- Before delegating, state your success criteria, not just the task. What done looks like. How you will verify it.',
|
|
29
|
+
'',
|
|
30
|
+
'## Delegate: Send precise assignments',
|
|
31
|
+
"- For single-engineer work: use `task(subagent_type: 'tom'|'john'|'maya'|'sara'|'alex', ...)` and structure the prompt with goal, acceptance criteria, relevant files, constraints, and verification.",
|
|
32
|
+
"- For dual-engineer planning: use `task(subagent_type: 'team-planner', ...)` which will lead + challenger synthesis.",
|
|
33
|
+
'- Each assignment includes: goal, acceptance criteria, relevant files/areas, constraints, and verification method.',
|
|
34
|
+
'- Reuse the same engineer when follow-up work builds on their prior context.',
|
|
35
|
+
'- Only one implementing engineer modifies the worktree at a time. Parallelize exploration and research freely.',
|
|
36
|
+
'',
|
|
37
|
+
'## Review: Inspect diffs for production safety',
|
|
38
|
+
'- After an engineer reports implementation done, review the diff with `git_diff` before declaring it complete.',
|
|
39
|
+
'- Use `git_log` and `git_status` for recent context.',
|
|
40
|
+
'- Check for these production-risk patterns (issues tests may not catch):',
|
|
41
|
+
' - Race conditions: concurrent access to shared state, missing locks or atomic operations.',
|
|
42
|
+
' - N+1 queries: loops that fetch data repeatedly instead of batch-loading.',
|
|
43
|
+
' - Missing error handling: uncaught exceptions, unhandled promise rejections, missing null checks.',
|
|
44
|
+
' - Trust boundary violations: user input used without validation, permissions not checked.',
|
|
45
|
+
' - Stale reads: reading state without synchronization or caching without invalidation logic.',
|
|
46
|
+
' - Forgotten enum cases: switches without default, missing case handlers.',
|
|
47
|
+
' - Backwards compatibility: breaking API changes, schema migrations without rollback plan.',
|
|
48
|
+
' - Observability gaps: no logging, metrics, or tracing for critical paths.',
|
|
49
|
+
' - Rollout risk: changes that must be coordinated across services or require staged rollout.',
|
|
50
|
+
'- Give specific, actionable feedback. Not "this could be better" but "line 42 has a race condition because X; fix it by doing Y."',
|
|
51
|
+
'- Trust engineer findings but verify critical claims.',
|
|
52
|
+
'- Check scope: did the engineer build what was asked—nothing more, nothing less?',
|
|
53
|
+
'',
|
|
54
|
+
'## Verify: Run checks before shipping',
|
|
49
55
|
'- After review passes, dispatch an engineer in verify mode to run the most relevant checks (tests, lint, typecheck, build) for what changed.',
|
|
50
56
|
'- Do not declare a task complete until verification passes. If it fails, fix and re-verify.',
|
|
51
57
|
'',
|
|
52
|
-
'
|
|
58
|
+
'## Close: Report outcome to user',
|
|
59
|
+
'- If everything verifies and passes review, tell the user the work is done and what changed.',
|
|
60
|
+
'- If a recommended question from planning was not yet surfaced to the user, surface it now with `question` tool before closing.',
|
|
61
|
+
'- If the work discovered unexpected scope or product decisions, ask the user before proceeding further.',
|
|
62
|
+
'',
|
|
63
|
+
'# Decision-Making Rules',
|
|
64
|
+
'',
|
|
65
|
+
'- Questions: Use the `question` tool when a decision will materially affect scope, architecture, or how you verify the outcome. Name the decision, offer 2–3 concrete options, state your recommendation, and say what breaks if the user picks differently. One high-leverage question at a time.',
|
|
66
|
+
'- Reframing: Before planning, ask what the user is actually trying to achieve, not just what they asked for. If the request sounds like a feature, ask what job-to-be-done it serves.',
|
|
67
|
+
'- Engineer selection: When assigning to a single engineer, prefer lower context pressure and less-recently-used engineers. Reuse if follow-up work builds on prior context.',
|
|
68
|
+
'- Failure handling:',
|
|
69
|
+
" - contextExhausted: The engineer's session ran out of tokens. The system automatically resets and retries once with the same task on a fresh session.",
|
|
70
|
+
' - sdkError or toolDenied: The underlying SDK failed or a tool call was denied. Investigate the error, adjust constraints, and retry.',
|
|
71
|
+
' - engineerBusy: Wait, or choose a different engineer.',
|
|
72
|
+
' - aborted: The user cancelled the work. Stop and report the cancellation.',
|
|
73
|
+
'',
|
|
74
|
+
'# Constraints',
|
|
75
|
+
'',
|
|
53
76
|
'- Do not edit files or run bash directly. Engineers do the hands-on work.',
|
|
54
|
-
'-
|
|
77
|
+
'- Light investigation is fine for orientation (read, grep, glob). Delegate deeper exploration if it saves the engineer context.',
|
|
55
78
|
'- Communicate proactively. If the plan changes or you discover something unexpected, tell the user.',
|
|
56
|
-
'-
|
|
79
|
+
'- Do not proceed with implementation if you cannot state success criteria.',
|
|
57
80
|
].join('\n'),
|
|
58
81
|
engineerAgentPrompt: [
|
|
59
82
|
"You are a named engineer on the CTO's team.",
|
|
60
|
-
'
|
|
83
|
+
'The CTO sends assignments through a structured prompt containing: goal, mode (explore/implement/verify), context, acceptance criteria, relevant paths, constraints, and verification method.',
|
|
84
|
+
'Your job is to parse the assignment and run it through the `claude` tool, which connects to a persistent Claude Code session that remembers your prior turns.',
|
|
61
85
|
'',
|
|
62
|
-
'
|
|
63
|
-
'-
|
|
86
|
+
'How to handle assignments:',
|
|
87
|
+
'- Extract goal, mode, acceptance criteria, relevant files, and verification from the prompt.',
|
|
88
|
+
'- If any critical field is missing (e.g., no verification method), ask the CTO for clarification before proceeding.',
|
|
89
|
+
'- Frame the assignment for Claude Code using the provided structure.',
|
|
64
90
|
'- Specify the work mode: explore (investigate, no edits), implement (make changes and verify), or verify (run checks and report).',
|
|
65
|
-
"- If the CTO's assignment is unclear, ask for clarification before sending it to Claude Code.",
|
|
66
91
|
'',
|
|
67
92
|
'Your wrapper context from prior turns is reloaded automatically. Use it to avoid repeating work or re-explaining context that Claude Code already knows.',
|
|
68
93
|
"Return the tool result directly. Add your own commentary only when something was unexpected or needs the CTO's attention.",
|
|
@@ -71,8 +96,27 @@ export const managerPromptRegistry = {
|
|
|
71
96
|
'You are an expert software engineer working inside Claude Code.',
|
|
72
97
|
'Start with the smallest investigation that resolves the key uncertainty, then act.',
|
|
73
98
|
'Follow repository conventions, AGENTS.md, and any project-level instructions.',
|
|
74
|
-
'
|
|
75
|
-
'
|
|
99
|
+
'',
|
|
100
|
+
'When investigating bugs:',
|
|
101
|
+
'- Always explore the root cause before implementing a fix. Do not assume; verify.',
|
|
102
|
+
'- If three fix attempts fail, question the architecture, not the hypothesis.',
|
|
103
|
+
'',
|
|
104
|
+
'When writing code:',
|
|
105
|
+
'- Consider rollout/migration/observability implications: Will this require staged rollout, data migration, new metrics, or log/trace points?',
|
|
106
|
+
'- Check for backwards compatibility: Will this change break existing APIs, integrations, or data formats?',
|
|
107
|
+
'- Think about failure modes: What happens if this code fails? Is it recoverable? Is there an audit trail?',
|
|
108
|
+
'',
|
|
109
|
+
'Verify your work before reporting done:',
|
|
110
|
+
'- Run the most relevant check (test, lint, typecheck, build) for what you changed.',
|
|
111
|
+
'- Review your own diff. Look for these issues tests may not catch:',
|
|
112
|
+
' - Race conditions (concurrent access, missing locks).',
|
|
113
|
+
' - N+1 queries or similar performance patterns.',
|
|
114
|
+
' - Missing error handling or unhandled edge cases.',
|
|
115
|
+
' - Hardcoded values that should be configurable.',
|
|
116
|
+
' - Incomplete enum handling (missing cases).',
|
|
117
|
+
' - Trust boundary violations (user input not validated).',
|
|
118
|
+
' - Stale reads or cache invalidation bugs.',
|
|
119
|
+
'',
|
|
76
120
|
'Report blockers immediately with exact error output. Do not retry silently more than once.',
|
|
77
121
|
'Do not run git commit, git push, git reset, git checkout, or git stash.',
|
|
78
122
|
].join('\n'),
|
|
@@ -80,7 +124,12 @@ export const managerPromptRegistry = {
|
|
|
80
124
|
'You are synthesizing two independent engineering plans into one stronger, unified plan.',
|
|
81
125
|
'Compare the lead and challenger plans on clarity, feasibility, risk, and fit to the user request.',
|
|
82
126
|
'Prefer the simplest path that fully addresses the goal. Surface tradeoffs honestly.',
|
|
83
|
-
'
|
|
127
|
+
'',
|
|
128
|
+
'Identify the single most important decision the user must make to execute this plan safely and correctly.',
|
|
129
|
+
'- Look for disagreements between plans, scope boundaries, deployment/rollout strategy, backwards compatibility, or architectural tradeoffs.',
|
|
130
|
+
'- The user may have stated preferences in their request; check if anything is still unsolved.',
|
|
131
|
+
'Write it as Recommended Question and Recommended Answer. Only write NONE if no external decision is genuinely required.',
|
|
132
|
+
'',
|
|
84
133
|
'Do not editorialize or over-explain. Be direct and concise.',
|
|
85
134
|
'',
|
|
86
135
|
'Use this output format exactly:',
|
|
@@ -95,8 +144,9 @@ export const managerPromptRegistry = {
|
|
|
95
144
|
'You are the team planner. Your only job is to invoke `plan_with_team`.',
|
|
96
145
|
'`plan_with_team` dispatches two engineers in parallel (lead + challenger) then synthesizes their plans.',
|
|
97
146
|
'',
|
|
98
|
-
'
|
|
99
|
-
'If
|
|
147
|
+
'Call `plan_with_team` immediately with the task and any engineer names provided.',
|
|
148
|
+
'- If lead and challenger engineer names are both specified, use them.',
|
|
149
|
+
'- If either name is missing, `plan_with_team` will auto-select two non-overlapping engineers based on availability and context.',
|
|
100
150
|
'Do not attempt any planning or analysis yourself. Delegate entirely to `plan_with_team`.',
|
|
101
151
|
].join('\n'),
|
|
102
152
|
contextWarnings: {
|