@doingdev/opencode-claude-manager-plugin 0.1.44 → 0.1.47
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/README.md +29 -31
- package/dist/index.d.ts +1 -1
- package/dist/manager/persistent-manager.d.ts +3 -23
- package/dist/manager/persistent-manager.js +2 -95
- package/dist/manager/team-orchestrator.d.ts +50 -0
- package/dist/manager/team-orchestrator.js +360 -0
- package/dist/plugin/agent-hierarchy.d.ts +12 -34
- package/dist/plugin/agent-hierarchy.js +36 -129
- package/dist/plugin/claude-manager.plugin.js +190 -445
- package/dist/plugin/service-factory.d.ts +18 -3
- package/dist/plugin/service-factory.js +32 -1
- package/dist/prompts/registry.d.ts +1 -10
- package/dist/prompts/registry.js +42 -270
- package/dist/src/claude/claude-agent-sdk-adapter.js +2 -1
- package/dist/src/claude/session-live-tailer.js +2 -2
- package/dist/src/index.d.ts +1 -1
- package/dist/src/manager/git-operations.d.ts +10 -1
- package/dist/src/manager/git-operations.js +18 -3
- package/dist/src/manager/persistent-manager.d.ts +18 -6
- package/dist/src/manager/persistent-manager.js +19 -13
- package/dist/src/manager/session-controller.d.ts +7 -10
- package/dist/src/manager/session-controller.js +12 -62
- package/dist/src/manager/team-orchestrator.d.ts +50 -0
- package/dist/src/manager/team-orchestrator.js +360 -0
- package/dist/src/plugin/agent-hierarchy.d.ts +12 -26
- package/dist/src/plugin/agent-hierarchy.js +36 -99
- package/dist/src/plugin/claude-manager.plugin.js +214 -393
- package/dist/src/plugin/service-factory.d.ts +18 -3
- package/dist/src/plugin/service-factory.js +33 -9
- package/dist/src/prompts/registry.d.ts +1 -10
- package/dist/src/prompts/registry.js +41 -246
- package/dist/src/state/team-state-store.d.ts +14 -0
- package/dist/src/state/team-state-store.js +85 -0
- package/dist/src/team/roster.d.ts +5 -0
- package/dist/src/team/roster.js +38 -0
- package/dist/src/types/contracts.d.ts +55 -13
- package/dist/src/types/contracts.js +1 -1
- package/dist/state/team-state-store.d.ts +14 -0
- package/dist/state/team-state-store.js +85 -0
- package/dist/team/roster.d.ts +5 -0
- package/dist/team/roster.js +38 -0
- package/dist/test/claude-manager.plugin.test.js +55 -280
- package/dist/test/git-operations.test.js +65 -1
- package/dist/test/persistent-manager.test.js +3 -3
- package/dist/test/prompt-registry.test.js +32 -252
- package/dist/test/session-controller.test.js +27 -27
- package/dist/test/team-orchestrator.test.d.ts +1 -0
- package/dist/test/team-orchestrator.test.js +146 -0
- package/dist/test/team-state-store.test.d.ts +1 -0
- package/dist/test/team-state-store.test.js +54 -0
- package/dist/types/contracts.d.ts +50 -23
- package/dist/types/contracts.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { createEmptyEngineerRecord, createEmptyTeamRecord } from '../team/roster.js';
|
|
2
|
+
import { ContextTracker } from './context-tracker.js';
|
|
3
|
+
export class TeamOrchestrator {
|
|
4
|
+
sessions;
|
|
5
|
+
teamStore;
|
|
6
|
+
transcriptStore;
|
|
7
|
+
engineerSessionPrompt;
|
|
8
|
+
projectClaudeFiles;
|
|
9
|
+
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, projectClaudeFiles) {
|
|
10
|
+
this.sessions = sessions;
|
|
11
|
+
this.teamStore = teamStore;
|
|
12
|
+
this.transcriptStore = transcriptStore;
|
|
13
|
+
this.engineerSessionPrompt = engineerSessionPrompt;
|
|
14
|
+
this.projectClaudeFiles = projectClaudeFiles;
|
|
15
|
+
}
|
|
16
|
+
async getOrCreateTeam(cwd, teamId) {
|
|
17
|
+
const existing = await this.teamStore.getTeam(cwd, teamId);
|
|
18
|
+
if (existing) {
|
|
19
|
+
return this.normalizeTeamRecord(existing);
|
|
20
|
+
}
|
|
21
|
+
const created = createEmptyTeamRecord(teamId, cwd);
|
|
22
|
+
await this.teamStore.saveTeam(created);
|
|
23
|
+
return created;
|
|
24
|
+
}
|
|
25
|
+
async listTeams(cwd) {
|
|
26
|
+
const teams = await this.teamStore.listTeams(cwd);
|
|
27
|
+
return teams.map((team) => this.normalizeTeamRecord(team));
|
|
28
|
+
}
|
|
29
|
+
async recordWrapperSession(cwd, teamId, engineer, wrapperSessionId) {
|
|
30
|
+
await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
|
|
31
|
+
...entry,
|
|
32
|
+
wrapperSessionId,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
async recordWrapperExchange(cwd, teamId, engineer, wrapperSessionId, mode, assignment, result) {
|
|
36
|
+
const timestamp = new Date().toISOString();
|
|
37
|
+
await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
|
|
38
|
+
...entry,
|
|
39
|
+
wrapperSessionId,
|
|
40
|
+
wrapperHistory: appendWrapperHistoryEntries(entry.wrapperHistory, [
|
|
41
|
+
{ timestamp, type: 'assignment', mode, text: summarizeText(assignment, 320) },
|
|
42
|
+
{ timestamp, type: 'result', mode, text: summarizeText(result, 320) },
|
|
43
|
+
]),
|
|
44
|
+
lastMode: mode,
|
|
45
|
+
lastTaskSummary: summarizeMessage(assignment),
|
|
46
|
+
lastUsedAt: timestamp,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
async getWrapperSystemContext(cwd, teamId, engineer) {
|
|
50
|
+
const team = await this.getOrCreateTeam(cwd, teamId);
|
|
51
|
+
const state = this.getEngineerState(team, engineer);
|
|
52
|
+
if (state.wrapperHistory.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const historyLines = state.wrapperHistory.map((entry) => {
|
|
56
|
+
const modeLabel = entry.mode ? ` [${entry.mode}]` : '';
|
|
57
|
+
return `- ${entry.type}${modeLabel}: ${entry.text}`;
|
|
58
|
+
});
|
|
59
|
+
return [
|
|
60
|
+
`Persistent wrapper memory for ${engineer} in CTO team ${teamId}:`,
|
|
61
|
+
'Use this only to improve delegation quality and continuity.',
|
|
62
|
+
'Prefer the current assignment when it conflicts with older context.',
|
|
63
|
+
...historyLines,
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
async findTeamByWrapperSession(cwd, wrapperSessionId) {
|
|
67
|
+
const teams = await this.listTeams(cwd);
|
|
68
|
+
for (const team of teams) {
|
|
69
|
+
for (const engineer of team.engineers) {
|
|
70
|
+
if (engineer.wrapperSessionId === wrapperSessionId) {
|
|
71
|
+
return {
|
|
72
|
+
teamId: team.id,
|
|
73
|
+
engineer: engineer.name,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
async dispatchEngineer(input) {
|
|
81
|
+
const team = await this.getOrCreateTeam(input.cwd, input.teamId);
|
|
82
|
+
const engineerState = this.getEngineerState(team, input.engineer);
|
|
83
|
+
await this.reserveEngineer(input.cwd, input.teamId, input.engineer);
|
|
84
|
+
try {
|
|
85
|
+
const tracker = new ContextTracker();
|
|
86
|
+
if (engineerState.context.sessionId) {
|
|
87
|
+
tracker.restore({
|
|
88
|
+
sessionId: engineerState.context.sessionId,
|
|
89
|
+
totalTurns: engineerState.context.totalTurns,
|
|
90
|
+
totalCostUsd: engineerState.context.totalCostUsd,
|
|
91
|
+
estimatedContextPercent: engineerState.context.estimatedContextPercent,
|
|
92
|
+
contextWindowSize: engineerState.context.contextWindowSize,
|
|
93
|
+
latestInputTokens: engineerState.context.latestInputTokens,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const result = await this.sessions.runTask({
|
|
97
|
+
cwd: input.cwd,
|
|
98
|
+
prompt: this.buildEngineerPrompt(input.mode, input.message),
|
|
99
|
+
systemPrompt: engineerState.claudeSessionId
|
|
100
|
+
? undefined
|
|
101
|
+
: this.buildSessionSystemPrompt(input.engineer, input.mode),
|
|
102
|
+
resumeSessionId: engineerState.claudeSessionId ?? undefined,
|
|
103
|
+
persistSession: true,
|
|
104
|
+
includePartialMessages: true,
|
|
105
|
+
permissionMode: this.mapWorkModeToSessionMode(input.mode) === 'plan' ? 'plan' : 'acceptEdits',
|
|
106
|
+
model: input.model,
|
|
107
|
+
effort: input.mode === 'implement' ? 'high' : 'medium',
|
|
108
|
+
settingSources: ['user'],
|
|
109
|
+
abortSignal: input.abortSignal,
|
|
110
|
+
}, input.onEvent);
|
|
111
|
+
tracker.recordResult({
|
|
112
|
+
sessionId: result.sessionId,
|
|
113
|
+
turns: result.turns,
|
|
114
|
+
totalCostUsd: result.totalCostUsd,
|
|
115
|
+
inputTokens: result.inputTokens,
|
|
116
|
+
outputTokens: result.outputTokens,
|
|
117
|
+
contextWindowSize: result.contextWindowSize,
|
|
118
|
+
});
|
|
119
|
+
if (result.sessionId && result.events.length > 0) {
|
|
120
|
+
await this.transcriptStore.appendEvents(input.cwd, result.sessionId, result.events);
|
|
121
|
+
}
|
|
122
|
+
const context = tracker.snapshot();
|
|
123
|
+
await this.updateEngineer(input.cwd, input.teamId, input.engineer, (entry) => ({
|
|
124
|
+
...entry,
|
|
125
|
+
claudeSessionId: result.sessionId ?? engineerState.claudeSessionId,
|
|
126
|
+
busy: false,
|
|
127
|
+
lastMode: input.mode,
|
|
128
|
+
lastTaskSummary: summarizeMessage(input.message),
|
|
129
|
+
lastUsedAt: new Date().toISOString(),
|
|
130
|
+
context,
|
|
131
|
+
}));
|
|
132
|
+
return {
|
|
133
|
+
teamId: input.teamId,
|
|
134
|
+
engineer: input.engineer,
|
|
135
|
+
mode: input.mode,
|
|
136
|
+
sessionId: result.sessionId,
|
|
137
|
+
finalText: result.finalText,
|
|
138
|
+
turns: result.turns,
|
|
139
|
+
totalCostUsd: result.totalCostUsd,
|
|
140
|
+
inputTokens: result.inputTokens,
|
|
141
|
+
outputTokens: result.outputTokens,
|
|
142
|
+
contextWindowSize: result.contextWindowSize,
|
|
143
|
+
context,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
await this.updateEngineer(input.cwd, input.teamId, input.engineer, (engineer) => ({
|
|
148
|
+
...engineer,
|
|
149
|
+
busy: false,
|
|
150
|
+
}));
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async planWithTeam(input) {
|
|
155
|
+
if (input.leadEngineer === input.challengerEngineer) {
|
|
156
|
+
throw new Error('Choose two different engineers for plan synthesis.');
|
|
157
|
+
}
|
|
158
|
+
const [leadDraft, challengerDraft] = await Promise.all([
|
|
159
|
+
this.dispatchEngineer({
|
|
160
|
+
teamId: input.teamId,
|
|
161
|
+
cwd: input.cwd,
|
|
162
|
+
engineer: input.leadEngineer,
|
|
163
|
+
mode: 'explore',
|
|
164
|
+
message: buildPlanDraftRequest('lead', input.request),
|
|
165
|
+
model: input.model,
|
|
166
|
+
abortSignal: input.abortSignal,
|
|
167
|
+
}),
|
|
168
|
+
this.dispatchEngineer({
|
|
169
|
+
teamId: input.teamId,
|
|
170
|
+
cwd: input.cwd,
|
|
171
|
+
engineer: input.challengerEngineer,
|
|
172
|
+
mode: 'explore',
|
|
173
|
+
message: buildPlanDraftRequest('challenger', input.request),
|
|
174
|
+
model: input.model,
|
|
175
|
+
abortSignal: input.abortSignal,
|
|
176
|
+
}),
|
|
177
|
+
]);
|
|
178
|
+
const drafts = [
|
|
179
|
+
{ ...leadDraft, request: input.request },
|
|
180
|
+
{ ...challengerDraft, request: input.request },
|
|
181
|
+
];
|
|
182
|
+
const synthesisResult = await this.sessions.runTask({
|
|
183
|
+
cwd: input.cwd,
|
|
184
|
+
prompt: buildSynthesisPrompt(input.request, drafts),
|
|
185
|
+
systemPrompt: buildSynthesisSystemPrompt(),
|
|
186
|
+
persistSession: false,
|
|
187
|
+
includePartialMessages: false,
|
|
188
|
+
permissionMode: 'plan',
|
|
189
|
+
model: input.model,
|
|
190
|
+
effort: 'high',
|
|
191
|
+
settingSources: ['user'],
|
|
192
|
+
abortSignal: input.abortSignal,
|
|
193
|
+
});
|
|
194
|
+
const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
|
|
195
|
+
return {
|
|
196
|
+
teamId: input.teamId,
|
|
197
|
+
request: input.request,
|
|
198
|
+
leadEngineer: input.leadEngineer,
|
|
199
|
+
challengerEngineer: input.challengerEngineer,
|
|
200
|
+
drafts,
|
|
201
|
+
synthesis: parsedSynthesis.synthesis,
|
|
202
|
+
recommendedQuestion: parsedSynthesis.recommendedQuestion,
|
|
203
|
+
recommendedAnswer: parsedSynthesis.recommendedAnswer,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async updateEngineer(cwd, teamId, engineerName, update) {
|
|
207
|
+
await this.getOrCreateTeam(cwd, teamId);
|
|
208
|
+
await this.teamStore.updateTeam(cwd, teamId, (team) => {
|
|
209
|
+
const normalized = this.normalizeTeamRecord(team);
|
|
210
|
+
const existing = this.getEngineerState(normalized, engineerName);
|
|
211
|
+
return {
|
|
212
|
+
...normalized,
|
|
213
|
+
updatedAt: new Date().toISOString(),
|
|
214
|
+
engineers: normalized.engineers.map((engineer) => engineer.name === engineerName ? update(existing) : engineer),
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async reserveEngineer(cwd, teamId, engineerName) {
|
|
219
|
+
await this.getOrCreateTeam(cwd, teamId);
|
|
220
|
+
await this.teamStore.updateTeam(cwd, teamId, (team) => {
|
|
221
|
+
const normalized = this.normalizeTeamRecord(team);
|
|
222
|
+
const engineer = this.getEngineerState(normalized, engineerName);
|
|
223
|
+
if (engineer.busy) {
|
|
224
|
+
throw new Error(`${engineerName} is already working on another assignment.`);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
...normalized,
|
|
228
|
+
updatedAt: new Date().toISOString(),
|
|
229
|
+
engineers: normalized.engineers.map((entry) => entry.name === engineerName
|
|
230
|
+
? {
|
|
231
|
+
...entry,
|
|
232
|
+
busy: true,
|
|
233
|
+
}
|
|
234
|
+
: entry),
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
getEngineerState(team, engineerName) {
|
|
239
|
+
return (team.engineers.find((engineer) => engineer.name === engineerName) ??
|
|
240
|
+
createEmptyEngineerRecord(engineerName));
|
|
241
|
+
}
|
|
242
|
+
normalizeTeamRecord(team) {
|
|
243
|
+
const engineerMap = new Map(team.engineers.map((engineer) => [engineer.name, engineer]));
|
|
244
|
+
return {
|
|
245
|
+
...team,
|
|
246
|
+
engineers: createEmptyTeamRecord(team.id, team.cwd).engineers.map((engineer) => engineerMap.get(engineer.name) ?? engineer),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
buildSessionSystemPrompt(engineer, mode) {
|
|
250
|
+
const claudeFileSection = this.projectClaudeFiles.length
|
|
251
|
+
? `\n\nProject Claude Files:\n${this.projectClaudeFiles
|
|
252
|
+
.map((file) => `## ${file.relativePath}\n${file.content}`)
|
|
253
|
+
.join('\n\n')}`
|
|
254
|
+
: '';
|
|
255
|
+
return [
|
|
256
|
+
this.engineerSessionPrompt,
|
|
257
|
+
'',
|
|
258
|
+
`Assigned engineer: ${engineer}.`,
|
|
259
|
+
`Current work mode: ${mode}.`,
|
|
260
|
+
claudeFileSection,
|
|
261
|
+
]
|
|
262
|
+
.join('\n')
|
|
263
|
+
.trim();
|
|
264
|
+
}
|
|
265
|
+
buildEngineerPrompt(mode, message) {
|
|
266
|
+
return `${buildModeInstruction(mode)}\n\n${message}`;
|
|
267
|
+
}
|
|
268
|
+
mapWorkModeToSessionMode(mode) {
|
|
269
|
+
return mode === 'explore' ? 'plan' : 'free';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function buildModeInstruction(mode) {
|
|
273
|
+
switch (mode) {
|
|
274
|
+
case 'explore':
|
|
275
|
+
return 'Work in planning mode. Investigate, reason, and write the plan inline. Do not make file edits.';
|
|
276
|
+
case 'implement':
|
|
277
|
+
return 'Work in implementation mode. Make the changes, verify them, and report clearly.';
|
|
278
|
+
case 'verify':
|
|
279
|
+
return 'Work in verification mode. Run the narrowest useful checks first, then broaden only if needed.';
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function summarizeMessage(message) {
|
|
283
|
+
const compact = message.replace(/\s+/g, ' ').trim();
|
|
284
|
+
return compact.length > 120 ? `${compact.slice(0, 117)}...` : compact;
|
|
285
|
+
}
|
|
286
|
+
function summarizeText(text, limit) {
|
|
287
|
+
const compact = text.replace(/\s+/g, ' ').trim();
|
|
288
|
+
return compact.length > limit ? `${compact.slice(0, limit - 3)}...` : compact;
|
|
289
|
+
}
|
|
290
|
+
function appendWrapperHistoryEntries(existing, nextEntries) {
|
|
291
|
+
return [...existing, ...nextEntries].slice(-12);
|
|
292
|
+
}
|
|
293
|
+
function buildPlanDraftRequest(perspective, request) {
|
|
294
|
+
const posture = perspective === 'lead'
|
|
295
|
+
? 'Propose the most direct workable plan.'
|
|
296
|
+
: 'Challenge weak assumptions, find missing decisions, and propose a stronger alternative if needed.';
|
|
297
|
+
return [
|
|
298
|
+
posture,
|
|
299
|
+
'',
|
|
300
|
+
'Return exactly these sections:',
|
|
301
|
+
'1. Objective',
|
|
302
|
+
'2. Proposed approach',
|
|
303
|
+
'3. Files or systems likely involved',
|
|
304
|
+
'4. Risks and open questions',
|
|
305
|
+
'5. Verification',
|
|
306
|
+
'6. Step-by-step plan',
|
|
307
|
+
'',
|
|
308
|
+
`User request: ${request}`,
|
|
309
|
+
].join('\n');
|
|
310
|
+
}
|
|
311
|
+
function buildSynthesisSystemPrompt() {
|
|
312
|
+
return [
|
|
313
|
+
'You are the CTO synthesis engine.',
|
|
314
|
+
'Combine two independent engineering plans into one better plan.',
|
|
315
|
+
'Prefer the clearest, simplest, highest-leverage path.',
|
|
316
|
+
'If one user decision is still required, surface exactly one recommended question and one recommended answer.',
|
|
317
|
+
'Use this output format exactly:',
|
|
318
|
+
'## Synthesis',
|
|
319
|
+
'<combined plan>',
|
|
320
|
+
'## Recommended Question',
|
|
321
|
+
'<question or NONE>',
|
|
322
|
+
'## Recommended Answer',
|
|
323
|
+
'<answer or NONE>',
|
|
324
|
+
].join('\n');
|
|
325
|
+
}
|
|
326
|
+
function buildSynthesisPrompt(request, drafts) {
|
|
327
|
+
return [
|
|
328
|
+
`User request: ${request}`,
|
|
329
|
+
'',
|
|
330
|
+
`Lead engineer (${drafts[0].engineer}) draft:`,
|
|
331
|
+
drafts[0].finalText,
|
|
332
|
+
'',
|
|
333
|
+
`Challenger engineer (${drafts[1].engineer}) draft:`,
|
|
334
|
+
drafts[1].finalText,
|
|
335
|
+
].join('\n');
|
|
336
|
+
}
|
|
337
|
+
function parseSynthesisResult(text) {
|
|
338
|
+
const synthesis = extractSection(text, 'Synthesis') ?? text.trim();
|
|
339
|
+
const recommendedQuestion = normalizeOptionalSection(extractSection(text, 'Recommended Question'));
|
|
340
|
+
const recommendedAnswer = normalizeOptionalSection(extractSection(text, 'Recommended Answer'));
|
|
341
|
+
return {
|
|
342
|
+
synthesis,
|
|
343
|
+
recommendedQuestion,
|
|
344
|
+
recommendedAnswer,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function extractSection(text, heading) {
|
|
348
|
+
const regex = new RegExp(`## ${escapeRegExp(heading)}\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
349
|
+
const match = text.match(regex);
|
|
350
|
+
return match?.[1]?.trim() ?? null;
|
|
351
|
+
}
|
|
352
|
+
function normalizeOptionalSection(value) {
|
|
353
|
+
if (!value) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return value.toUpperCase() === 'NONE' ? null : value;
|
|
357
|
+
}
|
|
358
|
+
function escapeRegExp(value) {
|
|
359
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
360
|
+
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
* Agent hierarchy configuration for the CTO + Engineer Wrapper architecture.
|
|
3
|
-
*
|
|
4
|
-
* CTO (cto) — pure orchestrator, spawns engineers, reviews diffs, commits
|
|
5
|
-
* Engineer Explore (engineer_explore) — manages a Claude Code session for read-only investigation
|
|
6
|
-
* Engineer Implement (engineer_implement) — manages a Claude Code session for implementation
|
|
7
|
-
* Claude Code session — the underlying AI session (prompt only, no OpenCode agent)
|
|
8
|
-
*/
|
|
9
|
-
import type { ManagerPromptRegistry } from '../types/contracts.js';
|
|
1
|
+
import type { EngineerName, ManagerPromptRegistry } from '../types/contracts.js';
|
|
10
2
|
export declare const AGENT_CTO = "cto";
|
|
11
|
-
export declare const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
export declare const ENGINEER_AGENT_IDS: {
|
|
4
|
+
readonly Tom: "tom";
|
|
5
|
+
readonly John: "john";
|
|
6
|
+
readonly Maya: "maya";
|
|
7
|
+
readonly Sara: "sara";
|
|
8
|
+
readonly Alex: "alex";
|
|
9
|
+
};
|
|
10
|
+
export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
|
|
11
|
+
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
|
|
16
12
|
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
17
13
|
type AgentPermission = {
|
|
18
14
|
'*'?: ToolPermission;
|
|
@@ -24,13 +20,9 @@ type AgentPermission = {
|
|
|
24
20
|
webfetch?: ToolPermission;
|
|
25
21
|
websearch?: ToolPermission;
|
|
26
22
|
lsp?: ToolPermission;
|
|
27
|
-
/** OpenCode built-in: manage session todo list */
|
|
28
23
|
todowrite?: ToolPermission;
|
|
29
|
-
/** OpenCode built-in: read session todo list */
|
|
30
24
|
todoread?: ToolPermission;
|
|
31
|
-
/** OpenCode built-in: ask the user structured questions with options */
|
|
32
25
|
question?: ToolPermission;
|
|
33
|
-
/** OpenCode built-in: launch subagents (matches subagent type, last-match-wins) */
|
|
34
26
|
task?: ToolPermission | Record<string, ToolPermission>;
|
|
35
27
|
bash?: ToolPermission | Record<string, ToolPermission>;
|
|
36
28
|
[tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
|
|
@@ -42,27 +34,13 @@ export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
|
|
|
42
34
|
permission: AgentPermission;
|
|
43
35
|
prompt: string;
|
|
44
36
|
};
|
|
45
|
-
export declare function
|
|
46
|
-
description: string;
|
|
47
|
-
mode: "subagent";
|
|
48
|
-
color: string;
|
|
49
|
-
permission: AgentPermission;
|
|
50
|
-
prompt: string;
|
|
51
|
-
};
|
|
52
|
-
export declare function buildEngineerImplementAgentConfig(prompts: ManagerPromptRegistry): {
|
|
53
|
-
description: string;
|
|
54
|
-
mode: "subagent";
|
|
55
|
-
color: string;
|
|
56
|
-
permission: AgentPermission;
|
|
57
|
-
prompt: string;
|
|
58
|
-
};
|
|
59
|
-
export declare function buildEngineerVerifyAgentConfig(prompts: ManagerPromptRegistry): {
|
|
37
|
+
export declare function buildEngineerAgentConfig(prompts: ManagerPromptRegistry, engineer: EngineerName): {
|
|
60
38
|
description: string;
|
|
61
39
|
mode: "subagent";
|
|
40
|
+
hidden: boolean;
|
|
62
41
|
color: string;
|
|
63
42
|
permission: AgentPermission;
|
|
64
43
|
prompt: string;
|
|
65
44
|
};
|
|
66
|
-
/** Deny all restricted tools at the global level so only designated agents can use them. */
|
|
67
45
|
export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
|
|
68
46
|
export {};
|
|
@@ -1,53 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
* Agent hierarchy configuration for the CTO + Engineer Wrapper architecture.
|
|
3
|
-
*
|
|
4
|
-
* CTO (cto) — pure orchestrator, spawns engineers, reviews diffs, commits
|
|
5
|
-
* Engineer Explore (engineer_explore) — manages a Claude Code session for read-only investigation
|
|
6
|
-
* Engineer Implement (engineer_implement) — manages a Claude Code session for implementation
|
|
7
|
-
* Claude Code session — the underlying AI session (prompt only, no OpenCode agent)
|
|
8
|
-
*/
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Agent names
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
1
|
+
import { TEAM_ENGINEERS } from '../team/roster.js';
|
|
12
2
|
export const AGENT_CTO = 'cto';
|
|
13
|
-
export const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
'session_health',
|
|
3
|
+
export const ENGINEER_AGENT_IDS = {
|
|
4
|
+
Tom: 'tom',
|
|
5
|
+
John: 'john',
|
|
6
|
+
Maya: 'maya',
|
|
7
|
+
Sara: 'sara',
|
|
8
|
+
Alex: 'alex',
|
|
9
|
+
};
|
|
10
|
+
export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
|
|
11
|
+
const CTO_ONLY_TOOL_IDS = [
|
|
12
|
+
'team_status',
|
|
24
13
|
'list_transcripts',
|
|
25
14
|
'list_history',
|
|
15
|
+
'git_diff',
|
|
16
|
+
'git_commit',
|
|
17
|
+
'git_reset',
|
|
18
|
+
'git_status',
|
|
19
|
+
'git_log',
|
|
20
|
+
'approval_policy',
|
|
21
|
+
'approval_decisions',
|
|
22
|
+
'approval_update',
|
|
26
23
|
];
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const ENGINEER_EXPLORE_TOOL_IDS = ['explore', ...ENGINEER_SHARED_TOOL_IDS];
|
|
31
|
-
/** Tools for the engineer_implement wrapper (implement-mode send + shared) */
|
|
32
|
-
const ENGINEER_IMPLEMENT_TOOL_IDS = ['implement', ...ENGINEER_SHARED_TOOL_IDS];
|
|
33
|
-
/** Tools for the engineer_verify wrapper (verify-mode send + shared) */
|
|
34
|
-
const ENGINEER_VERIFY_TOOL_IDS = ['verify', ...ENGINEER_SHARED_TOOL_IDS];
|
|
35
|
-
/** Git tools — owned by CTO */
|
|
36
|
-
const GIT_TOOL_IDS = ['git_diff', 'git_commit', 'git_reset', 'git_status', 'git_log'];
|
|
37
|
-
/** Approval tools — owned by CTO */
|
|
38
|
-
const APPROVAL_TOOL_IDS = ['approval_policy', 'approval_decisions', 'approval_update'];
|
|
39
|
-
/** All restricted tool IDs (union of all domain groups) */
|
|
40
|
-
export const ALL_RESTRICTED_TOOL_IDS = [
|
|
41
|
-
...ENGINEER_TOOL_IDS,
|
|
42
|
-
...GIT_TOOL_IDS,
|
|
43
|
-
...APPROVAL_TOOL_IDS,
|
|
44
|
-
];
|
|
45
|
-
/** Tools the CTO can use directly (git + approval only, NO engineer tools) */
|
|
46
|
-
const CTO_TOOL_IDS = [...GIT_TOOL_IDS, ...APPROVAL_TOOL_IDS];
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Shared read-only tool permissions
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
const READONLY_TOOLS = {
|
|
24
|
+
const ENGINEER_TOOL_IDS = ['claude'];
|
|
25
|
+
export const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
|
|
26
|
+
const CTO_READONLY_TOOLS = {
|
|
51
27
|
read: 'allow',
|
|
52
28
|
grep: 'allow',
|
|
53
29
|
glob: 'allow',
|
|
@@ -60,126 +36,57 @@ const READONLY_TOOLS = {
|
|
|
60
36
|
todoread: 'allow',
|
|
61
37
|
question: 'allow',
|
|
62
38
|
};
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Permission builders
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
/** CTO: pure orchestrator — read-only + git + approval + task (spawn engineers). No session tools. */
|
|
67
39
|
function buildCtoPermissions() {
|
|
68
40
|
const denied = {};
|
|
69
41
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
70
42
|
denied[toolId] = 'deny';
|
|
71
43
|
}
|
|
72
44
|
const allowed = {};
|
|
73
|
-
for (const toolId of
|
|
45
|
+
for (const toolId of CTO_ONLY_TOOL_IDS) {
|
|
74
46
|
allowed[toolId] = 'allow';
|
|
75
47
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
...denied,
|
|
80
|
-
...allowed,
|
|
81
|
-
task: {
|
|
82
|
-
'*': 'deny',
|
|
83
|
-
[AGENT_ENGINEER_EXPLORE]: 'allow',
|
|
84
|
-
[AGENT_ENGINEER_IMPLEMENT]: 'allow',
|
|
85
|
-
[AGENT_ENGINEER_VERIFY]: 'allow',
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
/** Engineer explore wrapper: read-only investigation + explore + shared session tools. */
|
|
90
|
-
function buildEngineerExplorePermissions() {
|
|
91
|
-
const denied = {};
|
|
92
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
93
|
-
denied[toolId] = 'deny';
|
|
94
|
-
}
|
|
95
|
-
const allowed = {};
|
|
96
|
-
for (const toolId of ENGINEER_EXPLORE_TOOL_IDS) {
|
|
97
|
-
allowed[toolId] = 'allow';
|
|
48
|
+
const taskPermissions = { '*': 'deny' };
|
|
49
|
+
for (const engineer of ENGINEER_AGENT_NAMES) {
|
|
50
|
+
taskPermissions[ENGINEER_AGENT_IDS[engineer]] = 'allow';
|
|
98
51
|
}
|
|
99
52
|
return {
|
|
100
53
|
'*': 'deny',
|
|
101
|
-
...
|
|
54
|
+
...CTO_READONLY_TOOLS,
|
|
102
55
|
...denied,
|
|
103
56
|
...allowed,
|
|
57
|
+
task: taskPermissions,
|
|
104
58
|
};
|
|
105
59
|
}
|
|
106
|
-
|
|
107
|
-
function buildEngineerImplementPermissions() {
|
|
60
|
+
function buildEngineerPermissions() {
|
|
108
61
|
const denied = {};
|
|
109
62
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
110
63
|
denied[toolId] = 'deny';
|
|
111
64
|
}
|
|
112
|
-
const allowed = {};
|
|
113
|
-
for (const toolId of ENGINEER_IMPLEMENT_TOOL_IDS) {
|
|
114
|
-
allowed[toolId] = 'allow';
|
|
115
|
-
}
|
|
116
65
|
return {
|
|
117
66
|
'*': 'deny',
|
|
118
|
-
...READONLY_TOOLS,
|
|
119
67
|
...denied,
|
|
120
|
-
|
|
68
|
+
claude: 'allow',
|
|
121
69
|
};
|
|
122
70
|
}
|
|
123
|
-
/** Engineer verify wrapper: read-only + verify + restricted bash for test/lint/typecheck/build. */
|
|
124
|
-
function buildEngineerVerifyPermissions() {
|
|
125
|
-
const denied = {};
|
|
126
|
-
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
127
|
-
denied[toolId] = 'deny';
|
|
128
|
-
}
|
|
129
|
-
const allowed = {};
|
|
130
|
-
for (const toolId of ENGINEER_VERIFY_TOOL_IDS) {
|
|
131
|
-
allowed[toolId] = 'allow';
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
'*': 'deny',
|
|
135
|
-
...READONLY_TOOLS,
|
|
136
|
-
...denied,
|
|
137
|
-
...allowed,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
// Agent config builders
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
71
|
export function buildCtoAgentConfig(prompts) {
|
|
144
72
|
return {
|
|
145
|
-
description: '
|
|
73
|
+
description: 'Technical orchestrator. Finds missing requirements, explicitly assigns engineers, compares plans, reviews diffs, and owns the final outcome.',
|
|
146
74
|
mode: 'primary',
|
|
147
75
|
color: '#D97757',
|
|
148
76
|
permission: buildCtoPermissions(),
|
|
149
77
|
prompt: prompts.ctoSystemPrompt,
|
|
150
78
|
};
|
|
151
79
|
}
|
|
152
|
-
export function
|
|
153
|
-
return {
|
|
154
|
-
description: 'Thin high-judgment wrapper that frames work quickly and dispatches to Claude Code in plan mode for read-only investigation.',
|
|
155
|
-
mode: 'subagent',
|
|
156
|
-
color: '#D97757',
|
|
157
|
-
permission: buildEngineerExplorePermissions(),
|
|
158
|
-
prompt: prompts.engineerExplorePrompt,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
export function buildEngineerImplementAgentConfig(prompts) {
|
|
162
|
-
return {
|
|
163
|
-
description: 'Thin high-judgment wrapper that frames work quickly and dispatches to Claude Code in free mode for implementation.',
|
|
164
|
-
mode: 'subagent',
|
|
165
|
-
color: '#D97757',
|
|
166
|
-
permission: buildEngineerImplementPermissions(),
|
|
167
|
-
prompt: prompts.engineerImplementPrompt,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
export function buildEngineerVerifyAgentConfig(prompts) {
|
|
80
|
+
export function buildEngineerAgentConfig(prompts, engineer) {
|
|
171
81
|
return {
|
|
172
|
-
description:
|
|
82
|
+
description: `${engineer} is a persistent engineer who works through one Claude Code session and remembers prior turns.`,
|
|
173
83
|
mode: 'subagent',
|
|
84
|
+
hidden: false,
|
|
174
85
|
color: '#D97757',
|
|
175
|
-
permission:
|
|
176
|
-
prompt: prompts.
|
|
86
|
+
permission: buildEngineerPermissions(),
|
|
87
|
+
prompt: `You are ${engineer}.\n\n${prompts.engineerAgentPrompt}`,
|
|
177
88
|
};
|
|
178
89
|
}
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
// Global permission helper
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
/** Deny all restricted tools at the global level so only designated agents can use them. */
|
|
183
90
|
export function denyRestrictedToolsGlobally(permissions) {
|
|
184
91
|
for (const toolId of ALL_RESTRICTED_TOOL_IDS) {
|
|
185
92
|
permissions[toolId] ??= 'deny';
|