@doingdev/opencode-claude-manager-plugin 0.1.56 → 0.1.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/manager/team-orchestrator.d.ts +13 -5
- package/dist/manager/team-orchestrator.js +134 -15
- package/dist/plugin/agent-hierarchy.d.ts +1 -54
- package/dist/plugin/agent-hierarchy.js +2 -123
- package/dist/plugin/agents/browser-qa.d.ts +14 -0
- package/dist/plugin/agents/browser-qa.js +27 -0
- package/dist/plugin/agents/common.d.ts +37 -0
- package/dist/plugin/agents/common.js +59 -0
- package/dist/plugin/agents/cto.d.ts +9 -0
- package/dist/plugin/agents/cto.js +39 -0
- package/dist/plugin/agents/engineers.d.ts +9 -0
- package/dist/plugin/agents/engineers.js +11 -0
- package/dist/plugin/agents/index.d.ts +6 -0
- package/dist/plugin/agents/index.js +5 -0
- package/dist/plugin/agents/team-planner.d.ts +10 -0
- package/dist/plugin/agents/team-planner.js +23 -0
- package/dist/plugin/claude-manager.plugin.js +68 -32
- package/dist/plugin/service-factory.d.ts +4 -3
- package/dist/plugin/service-factory.js +4 -1
- package/dist/prompts/registry.js +142 -57
- package/dist/src/manager/team-orchestrator.d.ts +13 -5
- package/dist/src/manager/team-orchestrator.js +134 -15
- package/dist/src/plugin/agent-hierarchy.d.ts +1 -54
- package/dist/src/plugin/agent-hierarchy.js +2 -123
- package/dist/src/plugin/agents/browser-qa.d.ts +14 -0
- package/dist/src/plugin/agents/browser-qa.js +27 -0
- package/dist/src/plugin/agents/common.d.ts +37 -0
- package/dist/src/plugin/agents/common.js +59 -0
- package/dist/src/plugin/agents/cto.d.ts +9 -0
- package/dist/src/plugin/agents/cto.js +39 -0
- package/dist/src/plugin/agents/engineers.d.ts +9 -0
- package/dist/src/plugin/agents/engineers.js +11 -0
- package/dist/src/plugin/agents/index.d.ts +6 -0
- package/dist/src/plugin/agents/index.js +5 -0
- package/dist/src/plugin/agents/team-planner.d.ts +10 -0
- package/dist/src/plugin/agents/team-planner.js +23 -0
- package/dist/src/plugin/claude-manager.plugin.js +68 -32
- package/dist/src/plugin/service-factory.d.ts +4 -3
- package/dist/src/plugin/service-factory.js +4 -1
- package/dist/src/prompts/registry.js +142 -57
- package/dist/src/team/roster.d.ts +3 -2
- package/dist/src/team/roster.js +2 -1
- package/dist/src/types/contracts.d.ts +26 -2
- package/dist/src/types/contracts.js +2 -1
- package/dist/team/roster.d.ts +3 -2
- package/dist/team/roster.js +2 -1
- package/dist/test/claude-manager.plugin.test.js +70 -0
- package/dist/test/prompt-registry.test.js +31 -6
- package/dist/test/report-claude-event.test.js +57 -3
- package/dist/test/team-orchestrator.test.js +155 -5
- package/dist/types/contracts.d.ts +26 -2
- package/dist/types/contracts.js +2 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapt
|
|
|
2
2
|
import type { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
3
3
|
import type { TeamStateStore } from '../state/team-state-store.js';
|
|
4
4
|
import type { TranscriptStore } from '../state/transcript-store.js';
|
|
5
|
-
import type { EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord } from '../types/contracts.js';
|
|
5
|
+
import type { EngineerFailureResult, EngineerName, EngineerTaskResult, EngineerWorkMode, SynthesizedPlanResult, TeamRecord, WorkerCapabilities } from '../types/contracts.js';
|
|
6
6
|
interface DispatchEngineerInput {
|
|
7
7
|
teamId: string;
|
|
8
8
|
cwd: string;
|
|
@@ -19,7 +19,8 @@ export declare class TeamOrchestrator {
|
|
|
19
19
|
private readonly transcriptStore;
|
|
20
20
|
private readonly engineerSessionPrompt;
|
|
21
21
|
private readonly planSynthesisPrompt;
|
|
22
|
-
|
|
22
|
+
private readonly workerCapabilities;
|
|
23
|
+
constructor(sessions: ClaudeSessionService, teamStore: TeamStateStore, transcriptStore: TranscriptStore, engineerSessionPrompt: string, planSynthesisPrompt: string, workerCapabilities: Partial<Record<EngineerName, WorkerCapabilities>>);
|
|
23
24
|
getOrCreateTeam(cwd: string, teamId: string): Promise<TeamRecord>;
|
|
24
25
|
listTeams(cwd: string): Promise<TeamRecord[]>;
|
|
25
26
|
recordWrapperSession(cwd: string, teamId: string, engineer: EngineerName, wrapperSessionId: string): Promise<void>;
|
|
@@ -33,7 +34,7 @@ export declare class TeamOrchestrator {
|
|
|
33
34
|
clearSession?: boolean;
|
|
34
35
|
clearHistory?: boolean;
|
|
35
36
|
}): Promise<void>;
|
|
36
|
-
dispatchEngineer(input: DispatchEngineerInput): Promise<EngineerTaskResult>;
|
|
37
|
+
dispatchEngineer(input: DispatchEngineerInput, retryCount?: number): Promise<EngineerTaskResult>;
|
|
37
38
|
static classifyError(error: unknown): EngineerFailureResult & {
|
|
38
39
|
cause: unknown;
|
|
39
40
|
};
|
|
@@ -41,8 +42,8 @@ export declare class TeamOrchestrator {
|
|
|
41
42
|
teamId: string;
|
|
42
43
|
cwd: string;
|
|
43
44
|
request: string;
|
|
44
|
-
leadEngineer
|
|
45
|
-
challengerEngineer
|
|
45
|
+
leadEngineer?: EngineerName;
|
|
46
|
+
challengerEngineer?: EngineerName;
|
|
46
47
|
model?: string;
|
|
47
48
|
abortSignal?: AbortSignal;
|
|
48
49
|
onLeadEvent?: ClaudeSessionEventHandler;
|
|
@@ -53,7 +54,14 @@ export declare class TeamOrchestrator {
|
|
|
53
54
|
private reserveEngineer;
|
|
54
55
|
private getEngineerState;
|
|
55
56
|
private normalizeTeamRecord;
|
|
57
|
+
getAvailableEngineers(team: TeamRecord): EngineerName[];
|
|
58
|
+
selectPlanEngineers(cwd: string, teamId: string, preferredLead?: EngineerName, preferredChallenger?: EngineerName): Promise<{
|
|
59
|
+
lead: EngineerName;
|
|
60
|
+
challenger: EngineerName;
|
|
61
|
+
}>;
|
|
56
62
|
private buildSessionSystemPrompt;
|
|
57
63
|
private buildEngineerPrompt;
|
|
58
64
|
}
|
|
65
|
+
export declare function getFailureGuidanceText(failureKind: string): string;
|
|
66
|
+
export declare function createActionableError(failure: EngineerFailureResult, originalError: unknown): Error;
|
|
59
67
|
export {};
|
|
@@ -7,12 +7,14 @@ export class TeamOrchestrator {
|
|
|
7
7
|
transcriptStore;
|
|
8
8
|
engineerSessionPrompt;
|
|
9
9
|
planSynthesisPrompt;
|
|
10
|
-
|
|
10
|
+
workerCapabilities;
|
|
11
|
+
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, planSynthesisPrompt, workerCapabilities) {
|
|
11
12
|
this.sessions = sessions;
|
|
12
13
|
this.teamStore = teamStore;
|
|
13
14
|
this.transcriptStore = transcriptStore;
|
|
14
15
|
this.engineerSessionPrompt = engineerSessionPrompt;
|
|
15
16
|
this.planSynthesisPrompt = planSynthesisPrompt;
|
|
17
|
+
this.workerCapabilities = workerCapabilities;
|
|
16
18
|
}
|
|
17
19
|
async getOrCreateTeam(cwd, teamId) {
|
|
18
20
|
const existing = await this.teamStore.getTeam(cwd, teamId);
|
|
@@ -88,7 +90,14 @@ export class TeamOrchestrator {
|
|
|
88
90
|
context: options?.clearSession ? createEmptyEngineerRecord(engineer).context : entry.context,
|
|
89
91
|
}));
|
|
90
92
|
}
|
|
91
|
-
async dispatchEngineer(input) {
|
|
93
|
+
async dispatchEngineer(input, retryCount = 0) {
|
|
94
|
+
const workerCaps = this.workerCapabilities[input.engineer];
|
|
95
|
+
// Reject write-restricted workers in implement mode
|
|
96
|
+
if (workerCaps?.restrictWriteTools && input.mode === 'implement') {
|
|
97
|
+
throw new Error(`${input.engineer} is a browser QA specialist and does not support implement mode. ` +
|
|
98
|
+
'It can only verify and explore (test browser interactions via Playwright). ' +
|
|
99
|
+
'For code changes, use a general engineer (Tom, John, Maya, Sara, Alex).');
|
|
100
|
+
}
|
|
92
101
|
const team = await this.getOrCreateTeam(input.cwd, input.teamId);
|
|
93
102
|
const engineerState = this.getEngineerState(team, input.engineer);
|
|
94
103
|
await this.reserveEngineer(input.cwd, input.teamId, input.engineer);
|
|
@@ -107,15 +116,19 @@ export class TeamOrchestrator {
|
|
|
107
116
|
const result = await this.sessions.runTask({
|
|
108
117
|
cwd: input.cwd,
|
|
109
118
|
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)}`,
|
|
119
|
+
? this.buildEngineerPrompt(input.mode, input.message, input.engineer)
|
|
120
|
+
: `${this.buildSessionSystemPrompt(input.engineer, input.mode)}\n\n${this.buildEngineerPrompt(input.mode, input.message, input.engineer)}`,
|
|
112
121
|
resumeSessionId: engineerState.claudeSessionId ?? undefined,
|
|
113
122
|
persistSession: true,
|
|
114
123
|
includePartialMessages: true,
|
|
115
124
|
permissionMode: 'acceptEdits',
|
|
116
|
-
restrictWriteTools: input.mode === 'explore',
|
|
125
|
+
restrictWriteTools: input.mode === 'explore' || (workerCaps?.restrictWriteTools ?? false),
|
|
117
126
|
model: input.model,
|
|
118
|
-
effort:
|
|
127
|
+
effort: (workerCaps?.restrictWriteTools ?? false)
|
|
128
|
+
? 'medium'
|
|
129
|
+
: input.mode === 'implement'
|
|
130
|
+
? 'high'
|
|
131
|
+
: 'medium',
|
|
119
132
|
settingSources: ['user', 'project', 'local'],
|
|
120
133
|
abortSignal: input.abortSignal,
|
|
121
134
|
}, input.onEvent);
|
|
@@ -161,6 +174,39 @@ export class TeamOrchestrator {
|
|
|
161
174
|
busy: false,
|
|
162
175
|
busySince: null,
|
|
163
176
|
}));
|
|
177
|
+
// Handle context exhaustion with automatic retry (max 1 retry)
|
|
178
|
+
const classified = TeamOrchestrator.classifyError(error);
|
|
179
|
+
if (classified.failureKind === 'contextExhausted' && retryCount === 0) {
|
|
180
|
+
// Reset the engineer's session and retry once with fresh session
|
|
181
|
+
await this.resetEngineer(input.cwd, input.teamId, input.engineer, {
|
|
182
|
+
clearSession: true,
|
|
183
|
+
clearHistory: false,
|
|
184
|
+
});
|
|
185
|
+
// Emit status event before retry
|
|
186
|
+
await input.onEvent?.({
|
|
187
|
+
type: 'status',
|
|
188
|
+
text: 'Context exhausted; resetting session and retrying once with a fresh session.',
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
// Retry dispatch with fresh session (retryCount=1 prevents infinite loop)
|
|
192
|
+
// Use the exact same assignment message without modification
|
|
193
|
+
return await this.dispatchEngineer(input, 1);
|
|
194
|
+
}
|
|
195
|
+
catch (retryError) {
|
|
196
|
+
// If retry also fails with a different error, preserve retry failure info
|
|
197
|
+
const retryClassified = TeamOrchestrator.classifyError(retryError);
|
|
198
|
+
if (retryClassified.failureKind !== classified.failureKind) {
|
|
199
|
+
// Create an error that shows both failures
|
|
200
|
+
const combinedMessage = `Initial: ${classified.failureKind} (${classified.message})\n` +
|
|
201
|
+
`After retry: ${retryClassified.failureKind} (${retryClassified.message})`;
|
|
202
|
+
const combinedError = new Error(combinedMessage);
|
|
203
|
+
Object.assign(combinedError, { cause: retryError });
|
|
204
|
+
throw combinedError;
|
|
205
|
+
}
|
|
206
|
+
// Same error type on retry, throw the retry error (more recent state)
|
|
207
|
+
throw retryError;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
164
210
|
throw error;
|
|
165
211
|
}
|
|
166
212
|
}
|
|
@@ -192,14 +238,13 @@ export class TeamOrchestrator {
|
|
|
192
238
|
};
|
|
193
239
|
}
|
|
194
240
|
async planWithTeam(input) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
241
|
+
// Auto-select engineers if not provided
|
|
242
|
+
const { lead: leadEngineer, challenger: challengerEngineer } = await this.selectPlanEngineers(input.cwd, input.teamId, input.leadEngineer, input.challengerEngineer);
|
|
198
243
|
const [leadDraft, challengerDraft] = await Promise.all([
|
|
199
244
|
this.dispatchEngineer({
|
|
200
245
|
teamId: input.teamId,
|
|
201
246
|
cwd: input.cwd,
|
|
202
|
-
engineer:
|
|
247
|
+
engineer: leadEngineer,
|
|
203
248
|
mode: 'explore',
|
|
204
249
|
message: buildPlanDraftRequest('lead', input.request),
|
|
205
250
|
model: input.model,
|
|
@@ -209,7 +254,7 @@ export class TeamOrchestrator {
|
|
|
209
254
|
this.dispatchEngineer({
|
|
210
255
|
teamId: input.teamId,
|
|
211
256
|
cwd: input.cwd,
|
|
212
|
-
engineer:
|
|
257
|
+
engineer: challengerEngineer,
|
|
213
258
|
mode: 'explore',
|
|
214
259
|
message: buildPlanDraftRequest('challenger', input.request),
|
|
215
260
|
model: input.model,
|
|
@@ -237,8 +282,8 @@ export class TeamOrchestrator {
|
|
|
237
282
|
return {
|
|
238
283
|
teamId: input.teamId,
|
|
239
284
|
request: input.request,
|
|
240
|
-
leadEngineer
|
|
241
|
-
challengerEngineer
|
|
285
|
+
leadEngineer,
|
|
286
|
+
challengerEngineer,
|
|
242
287
|
drafts,
|
|
243
288
|
synthesis: parsedSynthesis.synthesis,
|
|
244
289
|
recommendedQuestion: parsedSynthesis.recommendedQuestion,
|
|
@@ -289,12 +334,60 @@ export class TeamOrchestrator {
|
|
|
289
334
|
}
|
|
290
335
|
normalizeTeamRecord(team) {
|
|
291
336
|
const engineerMap = new Map(team.engineers.map((engineer) => [engineer.name, engineer]));
|
|
337
|
+
const emptyTeam = createEmptyTeamRecord(team.id, team.cwd);
|
|
292
338
|
return {
|
|
293
339
|
...team,
|
|
294
|
-
engineers:
|
|
340
|
+
engineers: emptyTeam.engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
|
|
295
341
|
};
|
|
296
342
|
}
|
|
343
|
+
getAvailableEngineers(team) {
|
|
344
|
+
const now = Date.now();
|
|
345
|
+
return team.engineers
|
|
346
|
+
.filter((engineer) => {
|
|
347
|
+
if (!engineer.busy)
|
|
348
|
+
return true;
|
|
349
|
+
// If an engineer has been marked busy but the lease expired, they're available
|
|
350
|
+
if (engineer.busySince) {
|
|
351
|
+
const leaseExpired = now - new Date(engineer.busySince).getTime() > BUSY_LEASE_MS;
|
|
352
|
+
return leaseExpired;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
})
|
|
356
|
+
.sort((a, b) => {
|
|
357
|
+
// Prefer engineers with lower context pressure and less-recently-used
|
|
358
|
+
const aContext = a.context.estimatedContextPercent ?? 0;
|
|
359
|
+
const bContext = b.context.estimatedContextPercent ?? 0;
|
|
360
|
+
if (aContext !== bContext) {
|
|
361
|
+
return aContext - bContext; // Lower context first
|
|
362
|
+
}
|
|
363
|
+
// If context is equal, prefer less-recently-used
|
|
364
|
+
const aTime = a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0;
|
|
365
|
+
const bTime = b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0;
|
|
366
|
+
return aTime - bTime; // Earlier usage time first
|
|
367
|
+
})
|
|
368
|
+
.map((engineer) => engineer.name);
|
|
369
|
+
}
|
|
370
|
+
async selectPlanEngineers(cwd, teamId, preferredLead, preferredChallenger) {
|
|
371
|
+
const team = await this.getOrCreateTeam(cwd, teamId);
|
|
372
|
+
const allAvailable = this.getAvailableEngineers(team);
|
|
373
|
+
// Filter to only planner-eligible engineers (specialists with plannerEligible: false are excluded)
|
|
374
|
+
const available = allAvailable.filter((e) => this.workerCapabilities[e]?.plannerEligible !== false);
|
|
375
|
+
if (available.length < 2) {
|
|
376
|
+
throw new Error(`Not enough available engineers for dual planning. Need 2 general engineers (specialists excluded), found ${available.length}.`);
|
|
377
|
+
}
|
|
378
|
+
const lead = preferredLead ?? available[0];
|
|
379
|
+
const foundChallenger = preferredChallenger ?? available.find((e) => e !== lead);
|
|
380
|
+
const challenger = foundChallenger ?? available[1];
|
|
381
|
+
if (lead === challenger) {
|
|
382
|
+
throw new Error('Cannot use the same engineer for both lead and challenger.');
|
|
383
|
+
}
|
|
384
|
+
return { lead, challenger };
|
|
385
|
+
}
|
|
297
386
|
buildSessionSystemPrompt(engineer, mode) {
|
|
387
|
+
const specialistPrompt = this.workerCapabilities[engineer]?.sessionPrompt;
|
|
388
|
+
if (specialistPrompt) {
|
|
389
|
+
return specialistPrompt;
|
|
390
|
+
}
|
|
298
391
|
return [
|
|
299
392
|
this.engineerSessionPrompt,
|
|
300
393
|
'',
|
|
@@ -304,7 +397,10 @@ export class TeamOrchestrator {
|
|
|
304
397
|
.join('\n')
|
|
305
398
|
.trim();
|
|
306
399
|
}
|
|
307
|
-
buildEngineerPrompt(mode, message) {
|
|
400
|
+
buildEngineerPrompt(mode, message, engineer) {
|
|
401
|
+
if (this.workerCapabilities[engineer]?.skipModeInstructions) {
|
|
402
|
+
return message;
|
|
403
|
+
}
|
|
308
404
|
return `${buildModeInstruction(mode)}\n\n${message}`;
|
|
309
405
|
}
|
|
310
406
|
}
|
|
@@ -398,3 +494,26 @@ function normalizeOptionalSection(value) {
|
|
|
398
494
|
function escapeRegExp(value) {
|
|
399
495
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
400
496
|
}
|
|
497
|
+
export function getFailureGuidanceText(failureKind) {
|
|
498
|
+
switch (failureKind) {
|
|
499
|
+
case 'contextExhausted':
|
|
500
|
+
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.';
|
|
501
|
+
case 'engineerBusy':
|
|
502
|
+
return 'This engineer is currently working on another assignment. Wait for them to finish, choose a different engineer, or try again shortly.';
|
|
503
|
+
case 'toolDenied':
|
|
504
|
+
return 'A tool permission was denied during the assignment. Check the approval policy and tool permissions, then retry.';
|
|
505
|
+
case 'aborted':
|
|
506
|
+
return 'The assignment was cancelled by the user or an abort signal was triggered. Review the request and try again.';
|
|
507
|
+
case 'sdkError':
|
|
508
|
+
return 'An SDK error occurred during the assignment. Check logs for details, ensure the Claude session is healthy, and retry.';
|
|
509
|
+
default:
|
|
510
|
+
return 'An unknown error occurred during the assignment. Check logs and retry.';
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
export function createActionableError(failure, originalError) {
|
|
514
|
+
const guidance = getFailureGuidanceText(failure.failureKind);
|
|
515
|
+
const errorMessage = `[${failure.failureKind}] ${failure.message}\n\n` + `Next steps: ${guidance}`;
|
|
516
|
+
const error = new Error(errorMessage);
|
|
517
|
+
Object.assign(error, { cause: originalError });
|
|
518
|
+
return error;
|
|
519
|
+
}
|
|
@@ -1,54 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const AGENT_CTO = "cto";
|
|
3
|
-
export declare const AGENT_TEAM_PLANNER = "team-planner";
|
|
4
|
-
export declare const ENGINEER_AGENT_IDS: {
|
|
5
|
-
readonly Tom: "tom";
|
|
6
|
-
readonly John: "john";
|
|
7
|
-
readonly Maya: "maya";
|
|
8
|
-
readonly Sara: "sara";
|
|
9
|
-
readonly Alex: "alex";
|
|
10
|
-
};
|
|
11
|
-
export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
12
|
-
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
13
|
-
type AgentPermission = {
|
|
14
|
-
'*'?: ToolPermission;
|
|
15
|
-
read?: ToolPermission;
|
|
16
|
-
grep?: ToolPermission;
|
|
17
|
-
glob?: ToolPermission;
|
|
18
|
-
list?: ToolPermission;
|
|
19
|
-
codesearch?: ToolPermission;
|
|
20
|
-
webfetch?: ToolPermission;
|
|
21
|
-
websearch?: ToolPermission;
|
|
22
|
-
lsp?: ToolPermission;
|
|
23
|
-
todowrite?: ToolPermission;
|
|
24
|
-
todoread?: ToolPermission;
|
|
25
|
-
question?: ToolPermission;
|
|
26
|
-
task?: ToolPermission | Record<string, ToolPermission>;
|
|
27
|
-
bash?: ToolPermission | Record<string, ToolPermission>;
|
|
28
|
-
[tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
|
|
29
|
-
};
|
|
30
|
-
export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
|
|
31
|
-
description: string;
|
|
32
|
-
mode: "primary";
|
|
33
|
-
color: string;
|
|
34
|
-
permission: AgentPermission;
|
|
35
|
-
prompt: string;
|
|
36
|
-
};
|
|
37
|
-
export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry, engineer: EngineerName): {
|
|
38
|
-
description: string;
|
|
39
|
-
mode: "subagent";
|
|
40
|
-
hidden: boolean;
|
|
41
|
-
color: string;
|
|
42
|
-
permission: AgentPermission;
|
|
43
|
-
prompt: string;
|
|
44
|
-
};
|
|
45
|
-
export declare function buildTeamPlannerAgentConfig(prompts: ManagerPromptRegistry): {
|
|
46
|
-
description: string;
|
|
47
|
-
mode: "subagent";
|
|
48
|
-
hidden: boolean;
|
|
49
|
-
color: string;
|
|
50
|
-
permission: AgentPermission;
|
|
51
|
-
prompt: string;
|
|
52
|
-
};
|
|
53
|
-
export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
|
|
54
|
-
export {};
|
|
1
|
+
export * from './agents/index.js';
|
|
@@ -1,123 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export const AGENT_TEAM_PLANNER = 'team-planner';
|
|
4
|
-
export const ENGINEER_AGENT_IDS = {
|
|
5
|
-
Tom: 'tom',
|
|
6
|
-
John: 'john',
|
|
7
|
-
Maya: 'maya',
|
|
8
|
-
Sara: 'sara',
|
|
9
|
-
Alex: 'alex',
|
|
10
|
-
};
|
|
11
|
-
export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
|
|
12
|
-
const CTO_ONLY_TOOL_IDS = [
|
|
13
|
-
'team_status',
|
|
14
|
-
'reset_engineer',
|
|
15
|
-
'list_transcripts',
|
|
16
|
-
'list_history',
|
|
17
|
-
'git_diff',
|
|
18
|
-
'git_commit',
|
|
19
|
-
'git_reset',
|
|
20
|
-
'git_status',
|
|
21
|
-
'git_log',
|
|
22
|
-
'approval_policy',
|
|
23
|
-
'approval_decisions',
|
|
24
|
-
'approval_update',
|
|
25
|
-
];
|
|
26
|
-
const ENGINEER_TOOL_IDS = ['claude'];
|
|
27
|
-
const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
|
|
28
|
-
const CTO_READONLY_TOOLS = {
|
|
29
|
-
read: 'allow',
|
|
30
|
-
grep: 'allow',
|
|
31
|
-
glob: 'allow',
|
|
32
|
-
list: 'allow',
|
|
33
|
-
codesearch: 'allow',
|
|
34
|
-
webfetch: 'allow',
|
|
35
|
-
websearch: 'allow',
|
|
36
|
-
lsp: 'allow',
|
|
37
|
-
todowrite: 'allow',
|
|
38
|
-
todoread: 'allow',
|
|
39
|
-
question: 'allow',
|
|
40
|
-
};
|
|
41
|
-
function buildTeamPlannerPermissions() {
|
|
42
|
-
const denied = {};
|
|
43
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
44
|
-
denied[toolId] = 'deny';
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
'*': 'deny',
|
|
48
|
-
plan_with_team: 'allow',
|
|
49
|
-
question: 'allow',
|
|
50
|
-
...denied,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function buildCtoPermissions() {
|
|
54
|
-
const denied = {};
|
|
55
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
56
|
-
denied[toolId] = 'deny';
|
|
57
|
-
}
|
|
58
|
-
const allowed = {};
|
|
59
|
-
for (const toolId of CTO_ONLY_TOOL_IDS) {
|
|
60
|
-
allowed[toolId] = 'allow';
|
|
61
|
-
}
|
|
62
|
-
const taskPermissions = { '*': 'deny' };
|
|
63
|
-
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
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.
|
|
69
|
-
}
|
|
70
|
-
taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
|
|
71
|
-
return {
|
|
72
|
-
'*': 'deny',
|
|
73
|
-
...CTO_READONLY_TOOLS,
|
|
74
|
-
...denied,
|
|
75
|
-
...allowed,
|
|
76
|
-
task: taskPermissions,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function buildEngineerPermissions() {
|
|
80
|
-
const denied = {};
|
|
81
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
82
|
-
denied[toolId] = 'deny';
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
'*': 'deny',
|
|
86
|
-
...denied,
|
|
87
|
-
claude: 'allow',
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
export function buildCtoAgentConfig(prompts) {
|
|
91
|
-
return {
|
|
92
|
-
description: 'Principal engineer who orchestrates AI-powered engineers. Decomposes work, asks clarifying questions, delegates precisely, reviews diffs, and owns the outcome.',
|
|
93
|
-
mode: 'primary',
|
|
94
|
-
color: '#D97757',
|
|
95
|
-
permission: buildCtoPermissions(),
|
|
96
|
-
prompt: prompts.ctoSystemPrompt,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
export function buildEngineerAgentConfig(prompts, engineer) {
|
|
100
|
-
return {
|
|
101
|
-
description: `${engineer} is a persistent engineer who works through one Claude Code session and remembers prior turns.`,
|
|
102
|
-
mode: 'subagent',
|
|
103
|
-
hidden: false,
|
|
104
|
-
color: '#D97757',
|
|
105
|
-
permission: buildEngineerPermissions(),
|
|
106
|
-
prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
export function buildTeamPlannerAgentConfig(prompts) {
|
|
110
|
-
return {
|
|
111
|
-
description: 'Runs dual-engineer planning by calling plan_with_team. Asks for engineer names if not provided.',
|
|
112
|
-
mode: 'subagent',
|
|
113
|
-
hidden: false,
|
|
114
|
-
color: '#D97757',
|
|
115
|
-
permission: buildTeamPlannerPermissions(),
|
|
116
|
-
prompt: prompts.teamPlannerPrompt,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
export function denyRestrictedToolsGlobally(permissions) {
|
|
120
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
121
|
-
permissions[toolId] ??= 'deny';
|
|
122
|
-
}
|
|
123
|
-
}
|
|
1
|
+
// Re-export barrel — all symbols now live in src/plugin/agents/.
|
|
2
|
+
export * from './agents/index.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EngineerName, ManagerPromptRegistry, WorkerCapabilities } from '../../types/contracts.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the worker capabilities map for all specialist workers.
|
|
4
|
+
* Called once at service-factory construction time to avoid re-building on each tool call.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildWorkerCapabilities(prompts: ManagerPromptRegistry): Partial<Record<EngineerName, WorkerCapabilities>>;
|
|
7
|
+
export declare function buildBrowserQaAgentConfig(prompts: ManagerPromptRegistry): {
|
|
8
|
+
description: string;
|
|
9
|
+
mode: "subagent";
|
|
10
|
+
hidden: boolean;
|
|
11
|
+
color: string;
|
|
12
|
+
permission: import("./common.js").AgentPermission;
|
|
13
|
+
prompt: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { buildEngineerPermissions } from './common.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the worker capabilities map for all specialist workers.
|
|
4
|
+
* Called once at service-factory construction time to avoid re-building on each tool call.
|
|
5
|
+
*/
|
|
6
|
+
export function buildWorkerCapabilities(prompts) {
|
|
7
|
+
return {
|
|
8
|
+
BrowserQA: {
|
|
9
|
+
sessionPrompt: prompts.browserQaSessionPrompt,
|
|
10
|
+
restrictWriteTools: true,
|
|
11
|
+
skipModeInstructions: true,
|
|
12
|
+
plannerEligible: false,
|
|
13
|
+
isRuntimeUnavailableResponse: (text) => text.trimStart().startsWith('PLAYWRIGHT_UNAVAILABLE:'),
|
|
14
|
+
runtimeUnavailableTitle: '❌ Playwright unavailable',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function buildBrowserQaAgentConfig(prompts) {
|
|
19
|
+
return {
|
|
20
|
+
description: 'Browser QA specialist who uses the Playwright skill/command to test web features and user flows. Maintains a persistent Claude Code session that remembers prior verification runs.',
|
|
21
|
+
mode: 'subagent',
|
|
22
|
+
hidden: false,
|
|
23
|
+
color: '#D97757',
|
|
24
|
+
permission: buildEngineerPermissions(), // Same permissions as engineers (claude tool only)
|
|
25
|
+
prompt: prompts.browserQaAgentPrompt,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const AGENT_CTO = "cto";
|
|
2
|
+
export declare const AGENT_TEAM_PLANNER = "team-planner";
|
|
3
|
+
export declare const AGENT_BROWSER_QA = "browser-qa";
|
|
4
|
+
export declare const ENGINEER_AGENT_IDS: {
|
|
5
|
+
readonly Tom: "tom";
|
|
6
|
+
readonly John: "john";
|
|
7
|
+
readonly Maya: "maya";
|
|
8
|
+
readonly Sara: "sara";
|
|
9
|
+
readonly Alex: "alex";
|
|
10
|
+
readonly BrowserQA: "browser-qa";
|
|
11
|
+
};
|
|
12
|
+
/** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
|
|
13
|
+
export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
14
|
+
export declare const CTO_ONLY_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
|
|
15
|
+
export declare const ENGINEER_TOOL_IDS: readonly ["claude"];
|
|
16
|
+
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
|
|
17
|
+
export type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
18
|
+
export type AgentPermission = {
|
|
19
|
+
'*'?: ToolPermission;
|
|
20
|
+
read?: ToolPermission;
|
|
21
|
+
grep?: ToolPermission;
|
|
22
|
+
glob?: ToolPermission;
|
|
23
|
+
list?: ToolPermission;
|
|
24
|
+
codesearch?: ToolPermission;
|
|
25
|
+
webfetch?: ToolPermission;
|
|
26
|
+
websearch?: ToolPermission;
|
|
27
|
+
lsp?: ToolPermission;
|
|
28
|
+
todowrite?: ToolPermission;
|
|
29
|
+
todoread?: ToolPermission;
|
|
30
|
+
question?: ToolPermission;
|
|
31
|
+
task?: ToolPermission | Record<string, ToolPermission>;
|
|
32
|
+
bash?: ToolPermission | Record<string, ToolPermission>;
|
|
33
|
+
[tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
|
|
34
|
+
};
|
|
35
|
+
export declare const CTO_READONLY_TOOLS: Record<string, ToolPermission>;
|
|
36
|
+
export declare function buildEngineerPermissions(): AgentPermission;
|
|
37
|
+
export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PLANNER_ELIGIBLE_ENGINEERS } from '../../team/roster.js';
|
|
2
|
+
export const AGENT_CTO = 'cto';
|
|
3
|
+
export const AGENT_TEAM_PLANNER = 'team-planner';
|
|
4
|
+
export const AGENT_BROWSER_QA = 'browser-qa';
|
|
5
|
+
export const ENGINEER_AGENT_IDS = {
|
|
6
|
+
Tom: 'tom',
|
|
7
|
+
John: 'john',
|
|
8
|
+
Maya: 'maya',
|
|
9
|
+
Sara: 'sara',
|
|
10
|
+
Alex: 'alex',
|
|
11
|
+
BrowserQA: 'browser-qa',
|
|
12
|
+
};
|
|
13
|
+
/** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
|
|
14
|
+
export const ENGINEER_AGENT_NAMES = PLANNER_ELIGIBLE_ENGINEERS;
|
|
15
|
+
export const CTO_ONLY_TOOL_IDS = [
|
|
16
|
+
'team_status',
|
|
17
|
+
'reset_engineer',
|
|
18
|
+
'list_transcripts',
|
|
19
|
+
'list_history',
|
|
20
|
+
'git_diff',
|
|
21
|
+
'git_commit',
|
|
22
|
+
'git_reset',
|
|
23
|
+
'git_status',
|
|
24
|
+
'git_log',
|
|
25
|
+
'approval_policy',
|
|
26
|
+
'approval_decisions',
|
|
27
|
+
'approval_update',
|
|
28
|
+
];
|
|
29
|
+
export const ENGINEER_TOOL_IDS = ['claude'];
|
|
30
|
+
export const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
|
|
31
|
+
export const CTO_READONLY_TOOLS = {
|
|
32
|
+
read: 'allow',
|
|
33
|
+
grep: 'allow',
|
|
34
|
+
glob: 'allow',
|
|
35
|
+
list: 'allow',
|
|
36
|
+
codesearch: 'allow',
|
|
37
|
+
webfetch: 'allow',
|
|
38
|
+
websearch: 'allow',
|
|
39
|
+
lsp: 'allow',
|
|
40
|
+
todowrite: 'allow',
|
|
41
|
+
todoread: 'allow',
|
|
42
|
+
question: 'allow',
|
|
43
|
+
};
|
|
44
|
+
export function buildEngineerPermissions() {
|
|
45
|
+
const denied = {};
|
|
46
|
+
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
47
|
+
denied[toolId] = 'deny';
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
'*': 'deny',
|
|
51
|
+
...denied,
|
|
52
|
+
claude: 'allow',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function denyRestrictedToolsGlobally(permissions) {
|
|
56
|
+
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
57
|
+
permissions[toolId] ??= 'deny';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ManagerPromptRegistry } from '../../types/contracts.js';
|
|
2
|
+
import type { AgentPermission } from './common.js';
|
|
3
|
+
export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
|
|
4
|
+
description: string;
|
|
5
|
+
mode: "primary";
|
|
6
|
+
color: string;
|
|
7
|
+
permission: AgentPermission;
|
|
8
|
+
prompt: string;
|
|
9
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AGENT_BROWSER_QA, AGENT_TEAM_PLANNER, ALL_RESTRICTED_TOOL_IDS, CTO_ONLY_TOOL_IDS, CTO_READONLY_TOOLS, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './common.js';
|
|
2
|
+
function buildCtoPermissions() {
|
|
3
|
+
const denied = {};
|
|
4
|
+
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
5
|
+
denied[toolId] = 'deny';
|
|
6
|
+
}
|
|
7
|
+
const allowed = {};
|
|
8
|
+
for (const toolId of CTO_ONLY_TOOL_IDS) {
|
|
9
|
+
allowed[toolId] = 'allow';
|
|
10
|
+
}
|
|
11
|
+
const taskPermissions = { '*': 'deny' };
|
|
12
|
+
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
13
|
+
const agentId = ENGINEER_AGENT_IDS[engineer];
|
|
14
|
+
// Support both uppercase (user-friendly) and lowercase (canonical) agent IDs.
|
|
15
|
+
// This ensures both task({ subagent_type: 'Tom' }) and task({ subagent_type: 'tom' }) work.
|
|
16
|
+
taskPermissions[engineer] = 'allow'; // 'Tom', 'John', etc.
|
|
17
|
+
taskPermissions[agentId] = 'allow'; // 'tom', 'john', etc.
|
|
18
|
+
}
|
|
19
|
+
// BrowserQA is a specialist registered separately from ENGINEER_AGENT_NAMES; add both forms.
|
|
20
|
+
taskPermissions['BrowserQA'] = 'allow'; // uppercase (user-friendly)
|
|
21
|
+
taskPermissions[AGENT_BROWSER_QA] = 'allow'; // 'browser-qa' canonical
|
|
22
|
+
taskPermissions[AGENT_TEAM_PLANNER] = 'allow';
|
|
23
|
+
return {
|
|
24
|
+
'*': 'deny',
|
|
25
|
+
...CTO_READONLY_TOOLS,
|
|
26
|
+
...denied,
|
|
27
|
+
...allowed,
|
|
28
|
+
task: taskPermissions,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function buildCtoAgentConfig(prompts) {
|
|
32
|
+
return {
|
|
33
|
+
description: 'Principal engineer who orchestrates AI-powered engineers. Decomposes work, asks clarifying questions, delegates precisely, reviews diffs, and owns the outcome.',
|
|
34
|
+
mode: 'primary',
|
|
35
|
+
color: '#D97757',
|
|
36
|
+
permission: buildCtoPermissions(),
|
|
37
|
+
prompt: prompts.ctoSystemPrompt,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EngineerName, ManagerPromptRegistry } from '../../types/contracts.js';
|
|
2
|
+
export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry, engineer: EngineerName): {
|
|
3
|
+
description: string;
|
|
4
|
+
mode: "subagent";
|
|
5
|
+
hidden: boolean;
|
|
6
|
+
color: string;
|
|
7
|
+
permission: import("./common.js").AgentPermission;
|
|
8
|
+
prompt: string;
|
|
9
|
+
};
|