@doingdev/opencode-claude-manager-plugin 0.1.49 → 0.1.50
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/claude/claude-agent-sdk-adapter.d.ts +2 -3
- package/dist/claude/claude-agent-sdk-adapter.js +0 -44
- package/dist/claude/claude-session.service.d.ts +1 -2
- package/dist/claude/claude-session.service.js +0 -3
- package/dist/claude/tool-approval-manager.d.ts +9 -6
- package/dist/claude/tool-approval-manager.js +43 -6
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/manager/context-tracker.d.ts +0 -1
- package/dist/manager/context-tracker.js +0 -3
- package/dist/manager/git-operations.d.ts +1 -4
- package/dist/manager/git-operations.js +7 -12
- package/dist/manager/persistent-manager.d.ts +3 -53
- package/dist/manager/persistent-manager.js +3 -135
- package/dist/manager/team-orchestrator.d.ts +8 -1
- package/dist/manager/team-orchestrator.js +70 -11
- package/dist/plugin/agent-hierarchy.d.ts +1 -1
- package/dist/plugin/agent-hierarchy.js +3 -1
- package/dist/plugin/claude-manager.plugin.js +169 -21
- package/dist/plugin/service-factory.d.ts +0 -2
- package/dist/plugin/service-factory.js +4 -12
- package/dist/prompts/registry.js +42 -37
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +2 -3
- package/dist/src/claude/claude-agent-sdk-adapter.js +0 -44
- package/dist/src/claude/claude-session.service.d.ts +1 -2
- package/dist/src/claude/claude-session.service.js +0 -3
- package/dist/src/claude/tool-approval-manager.d.ts +9 -6
- package/dist/src/claude/tool-approval-manager.js +43 -6
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +0 -1
- package/dist/src/manager/context-tracker.d.ts +0 -1
- package/dist/src/manager/context-tracker.js +0 -3
- package/dist/src/manager/git-operations.d.ts +1 -4
- package/dist/src/manager/git-operations.js +7 -12
- package/dist/src/manager/persistent-manager.d.ts +3 -53
- package/dist/src/manager/persistent-manager.js +3 -135
- package/dist/src/manager/team-orchestrator.d.ts +8 -1
- package/dist/src/manager/team-orchestrator.js +70 -11
- package/dist/src/plugin/agent-hierarchy.d.ts +1 -1
- package/dist/src/plugin/agent-hierarchy.js +3 -1
- package/dist/src/plugin/claude-manager.plugin.js +169 -21
- package/dist/src/plugin/service-factory.d.ts +0 -2
- package/dist/src/plugin/service-factory.js +4 -12
- package/dist/src/prompts/registry.js +42 -37
- package/dist/src/state/team-state-store.js +4 -1
- package/dist/src/team/roster.js +1 -0
- package/dist/src/types/contracts.d.ts +9 -49
- package/dist/state/team-state-store.js +4 -1
- package/dist/team/roster.js +1 -0
- package/dist/test/claude-agent-sdk-adapter.test.js +0 -11
- package/dist/test/claude-manager.plugin.test.js +6 -1
- package/dist/test/context-tracker.test.js +0 -8
- package/dist/test/git-operations.test.js +0 -21
- package/dist/test/persistent-manager.test.js +4 -164
- package/dist/test/prompt-registry.test.js +4 -9
- package/dist/test/report-claude-event.test.js +2 -2
- package/dist/test/tool-approval-manager.test.js +17 -17
- package/dist/types/contracts.d.ts +9 -49
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createEmptyEngineerRecord, createEmptyTeamRecord } from '../team/roster.js';
|
|
2
2
|
import { ContextTracker } from './context-tracker.js';
|
|
3
|
+
const BUSY_LEASE_MS = 15 * 60 * 1000;
|
|
3
4
|
export class TeamOrchestrator {
|
|
4
5
|
sessions;
|
|
5
6
|
teamStore;
|
|
@@ -77,6 +78,16 @@ export class TeamOrchestrator {
|
|
|
77
78
|
}
|
|
78
79
|
return null;
|
|
79
80
|
}
|
|
81
|
+
async resetEngineer(cwd, teamId, engineer, options) {
|
|
82
|
+
await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
|
|
83
|
+
...entry,
|
|
84
|
+
busy: false,
|
|
85
|
+
busySince: null,
|
|
86
|
+
claudeSessionId: options?.clearSession ? null : entry.claudeSessionId,
|
|
87
|
+
wrapperHistory: options?.clearHistory ? [] : entry.wrapperHistory,
|
|
88
|
+
context: options?.clearSession ? createEmptyEngineerRecord(engineer).context : entry.context,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
80
91
|
async dispatchEngineer(input) {
|
|
81
92
|
const team = await this.getOrCreateTeam(input.cwd, input.teamId);
|
|
82
93
|
const engineerState = this.getEngineerState(team, input.engineer);
|
|
@@ -124,6 +135,7 @@ export class TeamOrchestrator {
|
|
|
124
135
|
...entry,
|
|
125
136
|
claudeSessionId: result.sessionId ?? engineerState.claudeSessionId,
|
|
126
137
|
busy: false,
|
|
138
|
+
busySince: null,
|
|
127
139
|
lastMode: input.mode,
|
|
128
140
|
lastTaskSummary: summarizeMessage(input.message),
|
|
129
141
|
lastUsedAt: new Date().toISOString(),
|
|
@@ -147,10 +159,38 @@ export class TeamOrchestrator {
|
|
|
147
159
|
await this.updateEngineer(input.cwd, input.teamId, input.engineer, (engineer) => ({
|
|
148
160
|
...engineer,
|
|
149
161
|
busy: false,
|
|
162
|
+
busySince: null,
|
|
150
163
|
}));
|
|
151
164
|
throw error;
|
|
152
165
|
}
|
|
153
166
|
}
|
|
167
|
+
static classifyError(error) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
let failureKind = 'unknown';
|
|
170
|
+
if (message.includes('already working on another assignment')) {
|
|
171
|
+
failureKind = 'engineerBusy';
|
|
172
|
+
}
|
|
173
|
+
else if (message.includes('context') || message.includes('token limit')) {
|
|
174
|
+
failureKind = 'contextExhausted';
|
|
175
|
+
}
|
|
176
|
+
else if (message.includes('denied') || message.includes('not allowed')) {
|
|
177
|
+
failureKind = 'toolDenied';
|
|
178
|
+
}
|
|
179
|
+
else if (message.includes('abort') || message.includes('cancel')) {
|
|
180
|
+
failureKind = 'aborted';
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
failureKind = 'sdkError';
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
teamId: '',
|
|
187
|
+
engineer: 'Tom',
|
|
188
|
+
mode: 'explore',
|
|
189
|
+
failureKind,
|
|
190
|
+
message,
|
|
191
|
+
cause: error,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
154
194
|
async planWithTeam(input) {
|
|
155
195
|
if (input.leadEngineer === input.challengerEngineer) {
|
|
156
196
|
throw new Error('Choose two different engineers for plan synthesis.');
|
|
@@ -221,15 +261,21 @@ export class TeamOrchestrator {
|
|
|
221
261
|
const normalized = this.normalizeTeamRecord(team);
|
|
222
262
|
const engineer = this.getEngineerState(normalized, engineerName);
|
|
223
263
|
if (engineer.busy) {
|
|
224
|
-
|
|
264
|
+
const leaseExpired = engineer.busySince !== null &&
|
|
265
|
+
Date.now() - new Date(engineer.busySince).getTime() > BUSY_LEASE_MS;
|
|
266
|
+
if (!leaseExpired) {
|
|
267
|
+
throw new Error(`${engineerName} is already working on another assignment.`);
|
|
268
|
+
}
|
|
225
269
|
}
|
|
270
|
+
const now = new Date().toISOString();
|
|
226
271
|
return {
|
|
227
272
|
...normalized,
|
|
228
|
-
updatedAt:
|
|
273
|
+
updatedAt: now,
|
|
229
274
|
engineers: normalized.engineers.map((entry) => entry.name === engineerName
|
|
230
275
|
? {
|
|
231
276
|
...entry,
|
|
232
277
|
busy: true,
|
|
278
|
+
busySince: now,
|
|
233
279
|
}
|
|
234
280
|
: entry),
|
|
235
281
|
};
|
|
@@ -272,11 +318,24 @@ export class TeamOrchestrator {
|
|
|
272
318
|
function buildModeInstruction(mode) {
|
|
273
319
|
switch (mode) {
|
|
274
320
|
case 'explore':
|
|
275
|
-
return
|
|
321
|
+
return [
|
|
322
|
+
'Investigation mode.',
|
|
323
|
+
'Read, search, and reason about the codebase.',
|
|
324
|
+
'Produce a concrete plan with specific file paths and an approach.',
|
|
325
|
+
'Do not create or edit files.',
|
|
326
|
+
].join(' ');
|
|
276
327
|
case 'implement':
|
|
277
|
-
return
|
|
328
|
+
return [
|
|
329
|
+
'Implementation mode.',
|
|
330
|
+
'Make the changes, run the most relevant verification (tests, lint, typecheck), and report what changed and what you verified.',
|
|
331
|
+
].join(' ');
|
|
278
332
|
case 'verify':
|
|
279
|
-
return
|
|
333
|
+
return [
|
|
334
|
+
'Verification mode.',
|
|
335
|
+
'Run targeted checks in order of relevance.',
|
|
336
|
+
'Report pass/fail with evidence.',
|
|
337
|
+
'Escalate failures with exact output.',
|
|
338
|
+
].join(' ');
|
|
280
339
|
}
|
|
281
340
|
}
|
|
282
341
|
function summarizeMessage(message) {
|
|
@@ -292,8 +351,8 @@ function appendWrapperHistoryEntries(existing, nextEntries) {
|
|
|
292
351
|
}
|
|
293
352
|
function buildPlanDraftRequest(perspective, request) {
|
|
294
353
|
const posture = perspective === 'lead'
|
|
295
|
-
? 'Propose the most direct workable plan.'
|
|
296
|
-
: '
|
|
354
|
+
? 'You are the lead planner. Propose the most direct workable plan with concrete file paths and clear next steps.'
|
|
355
|
+
: 'You are the challenger. Stress-test assumptions, surface missing decisions, and propose a stronger alternative when the lead plan is weak.';
|
|
297
356
|
return [
|
|
298
357
|
posture,
|
|
299
358
|
'',
|
|
@@ -310,10 +369,10 @@ function buildPlanDraftRequest(perspective, request) {
|
|
|
310
369
|
}
|
|
311
370
|
function buildSynthesisSystemPrompt() {
|
|
312
371
|
return [
|
|
313
|
-
'You are
|
|
314
|
-
'
|
|
315
|
-
'Prefer the
|
|
316
|
-
'If
|
|
372
|
+
'You are synthesizing two independent engineering plans into one stronger plan.',
|
|
373
|
+
'Compare them on clarity, feasibility, risk, and fit to the user request.',
|
|
374
|
+
'Prefer the simplest path that fully addresses the goal.',
|
|
375
|
+
'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
|
|
317
376
|
'Use this output format exactly:',
|
|
318
377
|
'## Synthesis',
|
|
319
378
|
'<combined plan>',
|
|
@@ -8,7 +8,7 @@ export declare const ENGINEER_AGENT_IDS: {
|
|
|
8
8
|
readonly Alex: "alex";
|
|
9
9
|
};
|
|
10
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"];
|
|
11
|
+
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "plan_with_team", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
|
|
12
12
|
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
13
13
|
type AgentPermission = {
|
|
14
14
|
'*'?: ToolPermission;
|
|
@@ -10,6 +10,8 @@ export const ENGINEER_AGENT_IDS = {
|
|
|
10
10
|
export const ENGINEER_AGENT_NAMES = TEAM_ENGINEERS;
|
|
11
11
|
const CTO_ONLY_TOOL_IDS = [
|
|
12
12
|
'team_status',
|
|
13
|
+
'plan_with_team',
|
|
14
|
+
'reset_engineer',
|
|
13
15
|
'list_transcripts',
|
|
14
16
|
'list_history',
|
|
15
17
|
'git_diff',
|
|
@@ -70,7 +72,7 @@ function buildEngineerPermissions() {
|
|
|
70
72
|
}
|
|
71
73
|
export function buildCtoAgentConfig(prompts) {
|
|
72
74
|
return {
|
|
73
|
-
description: '
|
|
75
|
+
description: 'Principal engineer who orchestrates AI-powered engineers. Decomposes work, asks clarifying questions, delegates precisely, reviews diffs, and owns the outcome.',
|
|
74
76
|
mode: 'primary',
|
|
75
77
|
color: '#D97757',
|
|
76
78
|
permission: buildCtoPermissions(),
|
|
@@ -1,6 +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
5
|
import { discoverProjectClaudeFiles } from '../util/project-context.js';
|
|
5
6
|
import { AGENT_CTO, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
|
|
6
7
|
import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
|
|
@@ -9,6 +10,7 @@ const MODE_ENUM = ['explore', 'implement', 'verify'];
|
|
|
9
10
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
10
11
|
const claudeFiles = await discoverProjectClaudeFiles(worktree);
|
|
11
12
|
const services = getOrCreatePluginServices(worktree, claudeFiles);
|
|
13
|
+
await services.approvalManager.loadPersistedPolicy();
|
|
12
14
|
return {
|
|
13
15
|
config: async (config) => {
|
|
14
16
|
config.agent ??= {};
|
|
@@ -103,6 +105,69 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
103
105
|
return JSON.stringify(team, null, 2);
|
|
104
106
|
},
|
|
105
107
|
}),
|
|
108
|
+
plan_with_team: tool({
|
|
109
|
+
description: 'Run dual-engineer plan synthesis. Two engineers explore in parallel (lead + challenger), then their plans are synthesized into one stronger plan.',
|
|
110
|
+
args: {
|
|
111
|
+
request: tool.schema.string().min(1),
|
|
112
|
+
leadEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
113
|
+
challengerEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
114
|
+
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
115
|
+
},
|
|
116
|
+
async execute(args, context) {
|
|
117
|
+
const teamId = getActiveTeamSession(context.worktree) ?? context.sessionID;
|
|
118
|
+
annotateToolRun(context, 'Running dual-engineer plan synthesis', {
|
|
119
|
+
teamId,
|
|
120
|
+
lead: args.leadEngineer,
|
|
121
|
+
challenger: args.challengerEngineer,
|
|
122
|
+
});
|
|
123
|
+
const result = await services.orchestrator.planWithTeam({
|
|
124
|
+
teamId,
|
|
125
|
+
cwd: context.worktree,
|
|
126
|
+
request: args.request,
|
|
127
|
+
leadEngineer: args.leadEngineer,
|
|
128
|
+
challengerEngineer: args.challengerEngineer,
|
|
129
|
+
model: args.model,
|
|
130
|
+
abortSignal: context.abort,
|
|
131
|
+
});
|
|
132
|
+
context.metadata({
|
|
133
|
+
title: '✅ Plan synthesis complete',
|
|
134
|
+
metadata: {
|
|
135
|
+
teamId: result.teamId,
|
|
136
|
+
lead: result.leadEngineer,
|
|
137
|
+
challenger: result.challengerEngineer,
|
|
138
|
+
hasQuestion: result.recommendedQuestion !== null,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
return JSON.stringify({
|
|
142
|
+
synthesis: result.synthesis,
|
|
143
|
+
recommendedQuestion: result.recommendedQuestion,
|
|
144
|
+
recommendedAnswer: result.recommendedAnswer,
|
|
145
|
+
}, null, 2);
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
reset_engineer: tool({
|
|
149
|
+
description: 'Reset a stuck or corrupted engineer. Clears the busy flag. Optionally clears the Claude session (starts fresh) and/or wrapper history.',
|
|
150
|
+
args: {
|
|
151
|
+
engineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
152
|
+
clearSession: tool.schema.boolean().optional(),
|
|
153
|
+
clearHistory: tool.schema.boolean().optional(),
|
|
154
|
+
},
|
|
155
|
+
async execute(args, context) {
|
|
156
|
+
const teamId = getActiveTeamSession(context.worktree) ?? context.sessionID;
|
|
157
|
+
annotateToolRun(context, `Resetting ${args.engineer}`, {
|
|
158
|
+
teamId,
|
|
159
|
+
clearSession: args.clearSession,
|
|
160
|
+
clearHistory: args.clearHistory,
|
|
161
|
+
});
|
|
162
|
+
await services.orchestrator.resetEngineer(context.worktree, teamId, args.engineer, {
|
|
163
|
+
clearSession: args.clearSession,
|
|
164
|
+
clearHistory: args.clearHistory,
|
|
165
|
+
});
|
|
166
|
+
const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
|
|
167
|
+
const engineer = team.engineers.find((e) => e.name === args.engineer);
|
|
168
|
+
return JSON.stringify({ reset: true, engineer: engineer ?? args.engineer }, null, 2);
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
106
171
|
git_diff: tool({
|
|
107
172
|
description: 'Show diff of uncommitted changes. Use paths to filter to specific files or use ref to compare against another branch, tag, or commit.',
|
|
108
173
|
args: {
|
|
@@ -125,13 +190,17 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
125
190
|
},
|
|
126
191
|
}),
|
|
127
192
|
git_commit: tool({
|
|
128
|
-
description: '
|
|
193
|
+
description: 'Create a commit. Stages all changes by default, or only the specified paths if provided.',
|
|
129
194
|
args: {
|
|
130
195
|
message: tool.schema.string().min(1),
|
|
196
|
+
paths: tool.schema.string().array().optional(),
|
|
131
197
|
},
|
|
132
198
|
async execute(args, context) {
|
|
133
|
-
annotateToolRun(context, 'Committing changes', {
|
|
134
|
-
|
|
199
|
+
annotateToolRun(context, 'Committing changes', {
|
|
200
|
+
message: args.message,
|
|
201
|
+
paths: args.paths,
|
|
202
|
+
});
|
|
203
|
+
const result = await services.manager.gitCommit(args.message, args.paths);
|
|
135
204
|
return JSON.stringify(result, null, 2);
|
|
136
205
|
},
|
|
137
206
|
}),
|
|
@@ -249,7 +318,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
249
318
|
error: 'addRule requires ruleId, toolPattern, and ruleAction',
|
|
250
319
|
});
|
|
251
320
|
}
|
|
252
|
-
services.approvalManager.addRule({
|
|
321
|
+
await services.approvalManager.addRule({
|
|
253
322
|
id: args.ruleId,
|
|
254
323
|
toolPattern: args.toolPattern,
|
|
255
324
|
inputPattern: args.inputPattern,
|
|
@@ -262,20 +331,20 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
262
331
|
if (!args.ruleId) {
|
|
263
332
|
return JSON.stringify({ error: 'removeRule requires ruleId' });
|
|
264
333
|
}
|
|
265
|
-
const removed = services.approvalManager.removeRule(args.ruleId);
|
|
334
|
+
const removed = await services.approvalManager.removeRule(args.ruleId);
|
|
266
335
|
return JSON.stringify({ removed }, null, 2);
|
|
267
336
|
}
|
|
268
337
|
else if (args.action === 'setDefault') {
|
|
269
338
|
if (!args.defaultAction) {
|
|
270
339
|
return JSON.stringify({ error: 'setDefault requires defaultAction' });
|
|
271
340
|
}
|
|
272
|
-
services.approvalManager.setDefaultAction(args.defaultAction);
|
|
341
|
+
await services.approvalManager.setDefaultAction(args.defaultAction);
|
|
273
342
|
}
|
|
274
343
|
else if (args.action === 'setEnabled') {
|
|
275
344
|
if (args.enabled === undefined) {
|
|
276
345
|
return JSON.stringify({ error: 'setEnabled requires enabled' });
|
|
277
346
|
}
|
|
278
|
-
services.approvalManager.setEnabled(args.enabled);
|
|
347
|
+
await services.approvalManager.setEnabled(args.enabled);
|
|
279
348
|
}
|
|
280
349
|
else if (args.action === 'clearDecisions') {
|
|
281
350
|
services.approvalManager.clearDecisions();
|
|
@@ -292,16 +361,35 @@ async function runEngineerAssignment(input, context) {
|
|
|
292
361
|
teamId: input.teamId,
|
|
293
362
|
mode: input.mode,
|
|
294
363
|
});
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
364
|
+
let result;
|
|
365
|
+
try {
|
|
366
|
+
result = await services.orchestrator.dispatchEngineer({
|
|
367
|
+
teamId: input.teamId,
|
|
368
|
+
cwd: context.worktree,
|
|
369
|
+
engineer: input.engineer,
|
|
370
|
+
mode: input.mode,
|
|
371
|
+
message: input.message,
|
|
372
|
+
model: input.model,
|
|
373
|
+
abortSignal: context.abort,
|
|
374
|
+
onEvent: (event) => reportClaudeEvent(context, input.engineer, event),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
const failure = TeamOrchestrator.classifyError(error);
|
|
379
|
+
failure.teamId = input.teamId;
|
|
380
|
+
failure.engineer = input.engineer;
|
|
381
|
+
failure.mode = input.mode;
|
|
382
|
+
context.metadata({
|
|
383
|
+
title: `❌ ${input.engineer} failed (${failure.failureKind})`,
|
|
384
|
+
metadata: {
|
|
385
|
+
teamId: failure.teamId,
|
|
386
|
+
engineer: failure.engineer,
|
|
387
|
+
failureKind: failure.failureKind,
|
|
388
|
+
message: failure.message.slice(0, 200),
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
305
393
|
await services.orchestrator.recordWrapperExchange(context.worktree, input.teamId, input.engineer, context.sessionID, input.mode, input.message, result.finalText);
|
|
306
394
|
context.metadata({
|
|
307
395
|
title: `✅ ${input.engineer} finished`,
|
|
@@ -335,6 +423,60 @@ function isEngineerAgent(agentId) {
|
|
|
335
423
|
async function resolveTeamId(worktree, sessionID) {
|
|
336
424
|
return (await getPersistedActiveTeam(worktree)) ?? getActiveTeamSession(worktree) ?? sessionID;
|
|
337
425
|
}
|
|
426
|
+
function formatToolDescription(toolName, toolArgs) {
|
|
427
|
+
if (!toolArgs || typeof toolArgs !== 'object')
|
|
428
|
+
return undefined;
|
|
429
|
+
const args = toolArgs;
|
|
430
|
+
switch (toolName) {
|
|
431
|
+
case 'Read':
|
|
432
|
+
case 'read': {
|
|
433
|
+
const filePath = args.file_path;
|
|
434
|
+
return typeof filePath === 'string' ? `Reading: ${filePath}` : undefined;
|
|
435
|
+
}
|
|
436
|
+
case 'Grep':
|
|
437
|
+
case 'grep': {
|
|
438
|
+
const pattern = args.pattern;
|
|
439
|
+
return typeof pattern === 'string' ? `Searching: ${pattern}` : undefined;
|
|
440
|
+
}
|
|
441
|
+
case 'Write':
|
|
442
|
+
case 'write': {
|
|
443
|
+
const filePath = args.file_path;
|
|
444
|
+
return typeof filePath === 'string' ? `Writing: ${filePath}` : undefined;
|
|
445
|
+
}
|
|
446
|
+
case 'Edit':
|
|
447
|
+
case 'edit': {
|
|
448
|
+
const filePath = args.file_path;
|
|
449
|
+
return typeof filePath === 'string' ? `Editing: ${filePath}` : undefined;
|
|
450
|
+
}
|
|
451
|
+
case 'Bash':
|
|
452
|
+
case 'bash':
|
|
453
|
+
case 'Run':
|
|
454
|
+
case 'run': {
|
|
455
|
+
const command = args.command;
|
|
456
|
+
return typeof command === 'string' ? `Running: ${command.slice(0, 80)}` : undefined;
|
|
457
|
+
}
|
|
458
|
+
case 'WebFetch':
|
|
459
|
+
case 'webfetch': {
|
|
460
|
+
const url = args.url;
|
|
461
|
+
return typeof url === 'string' ? `Fetching: ${url}` : undefined;
|
|
462
|
+
}
|
|
463
|
+
case 'Glob':
|
|
464
|
+
case 'glob': {
|
|
465
|
+
const pattern = args.pattern;
|
|
466
|
+
return typeof pattern === 'string' ? `Matching: ${pattern}` : undefined;
|
|
467
|
+
}
|
|
468
|
+
case 'TodoWrite':
|
|
469
|
+
case 'todowrite':
|
|
470
|
+
return args.content ? `Updating todos` : undefined;
|
|
471
|
+
case 'NotebookEdit':
|
|
472
|
+
case 'notebook_edit': {
|
|
473
|
+
const cellPath = args.notebook_cell_path;
|
|
474
|
+
return typeof cellPath === 'string' ? `Editing notebook: ${cellPath}` : undefined;
|
|
475
|
+
}
|
|
476
|
+
default:
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
338
480
|
function reportClaudeEvent(context, engineer, event) {
|
|
339
481
|
if (event.type === 'error') {
|
|
340
482
|
context.metadata({
|
|
@@ -382,10 +524,13 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
382
524
|
catch {
|
|
383
525
|
// event.text is not valid JSON — fall back to generic title
|
|
384
526
|
}
|
|
527
|
+
const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
|
|
385
528
|
context.metadata({
|
|
386
|
-
title:
|
|
387
|
-
? `⚡ ${engineer} → ${
|
|
388
|
-
:
|
|
529
|
+
title: toolDescription
|
|
530
|
+
? `⚡ ${engineer} → ${toolDescription}`
|
|
531
|
+
: toolName
|
|
532
|
+
? `⚡ ${engineer} → ${toolName}`
|
|
533
|
+
: `⚡ ${engineer} is using Claude Code tools`,
|
|
389
534
|
metadata: {
|
|
390
535
|
engineer,
|
|
391
536
|
sessionId: event.sessionId,
|
|
@@ -397,12 +542,15 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
397
542
|
return;
|
|
398
543
|
}
|
|
399
544
|
if (event.type === 'assistant' || event.type === 'partial') {
|
|
545
|
+
const isThinking = event.text.startsWith('<thinking>');
|
|
546
|
+
const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
|
|
400
547
|
context.metadata({
|
|
401
|
-
title: `⚡ ${engineer}
|
|
548
|
+
title: `⚡ ${engineer} ${stateLabel}`,
|
|
402
549
|
metadata: {
|
|
403
550
|
engineer,
|
|
404
551
|
sessionId: event.sessionId,
|
|
405
552
|
preview: event.text.slice(0, 160),
|
|
553
|
+
isThinking,
|
|
406
554
|
},
|
|
407
555
|
});
|
|
408
556
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
2
|
-
import { SessionLiveTailer } from '../claude/session-live-tailer.js';
|
|
3
2
|
import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
4
3
|
import { TeamStateStore } from '../state/team-state-store.js';
|
|
5
4
|
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
@@ -9,7 +8,6 @@ export interface ClaudeManagerPluginServices {
|
|
|
9
8
|
manager: PersistentManager;
|
|
10
9
|
sessions: ClaudeSessionService;
|
|
11
10
|
approvalManager: ToolApprovalManager;
|
|
12
|
-
liveTailer: SessionLiveTailer;
|
|
13
11
|
teamStore: TeamStateStore;
|
|
14
12
|
orchestrator: TeamOrchestrator;
|
|
15
13
|
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
2
3
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
3
|
-
import { SessionLiveTailer } from '../claude/session-live-tailer.js';
|
|
4
4
|
import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
5
|
-
import { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
6
5
|
import { TeamStateStore } from '../state/team-state-store.js';
|
|
7
6
|
import { TranscriptStore } from '../state/transcript-store.js';
|
|
8
|
-
import { ContextTracker } from '../manager/context-tracker.js';
|
|
9
7
|
import { GitOperations } from '../manager/git-operations.js';
|
|
10
|
-
import { SessionController } from '../manager/session-controller.js';
|
|
11
8
|
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
12
9
|
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
13
10
|
import { TeamOrchestrator } from '../manager/team-orchestrator.js';
|
|
@@ -19,24 +16,19 @@ export function getOrCreatePluginServices(worktree, projectClaudeFiles = []) {
|
|
|
19
16
|
if (existing) {
|
|
20
17
|
return existing;
|
|
21
18
|
}
|
|
22
|
-
const
|
|
19
|
+
const approvalPolicyPath = path.join(worktree, '.claude-manager', 'approval-policy.json');
|
|
20
|
+
const approvalManager = new ToolApprovalManager(undefined, undefined, approvalPolicyPath);
|
|
23
21
|
const sdkAdapter = new ClaudeAgentSdkAdapter(undefined, approvalManager);
|
|
24
22
|
const sessionService = new ClaudeSessionService(sdkAdapter);
|
|
25
|
-
const contextTracker = new ContextTracker();
|
|
26
|
-
const sessionController = new SessionController(sdkAdapter, contextTracker, undefined, // session prompt is now constructed dynamically by the wrapper
|
|
27
|
-
'default', worktree, managerPromptRegistry.modePrefixes);
|
|
28
23
|
const gitOps = new GitOperations(worktree);
|
|
29
|
-
const stateStore = new FileRunStateStore();
|
|
30
24
|
const teamStore = new TeamStateStore();
|
|
31
25
|
const transcriptStore = new TranscriptStore();
|
|
32
|
-
const manager = new PersistentManager(
|
|
33
|
-
const liveTailer = new SessionLiveTailer();
|
|
26
|
+
const manager = new PersistentManager(gitOps, transcriptStore);
|
|
34
27
|
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, projectClaudeFiles);
|
|
35
28
|
const services = {
|
|
36
29
|
manager,
|
|
37
30
|
sessions: sessionService,
|
|
38
31
|
approvalManager,
|
|
39
|
-
liveTailer,
|
|
40
32
|
teamStore,
|
|
41
33
|
orchestrator,
|
|
42
34
|
};
|
|
@@ -1,52 +1,57 @@
|
|
|
1
1
|
export const managerPromptRegistry = {
|
|
2
2
|
ctoSystemPrompt: [
|
|
3
|
-
'You are
|
|
3
|
+
'You are a principal engineer orchestrating a team of AI-powered engineers.',
|
|
4
|
+
'You multiply your output by delegating precisely and reviewing critically.',
|
|
5
|
+
'Every prompt you send to an engineer costs time and tokens. Make each one count.',
|
|
4
6
|
'',
|
|
5
|
-
'
|
|
6
|
-
'-
|
|
7
|
-
'-
|
|
8
|
-
'-
|
|
9
|
-
'-
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'-
|
|
7
|
+
'Understand first:',
|
|
8
|
+
'- Ask questions. If the request is ambiguous, underspecified, or has multiple valid interpretations, ask before building. Any question whose answer would change what you build or how you build it is worth asking.',
|
|
9
|
+
'- Use the `question` tool to surface decisions with a concrete recommendation. Prefer one precise question over many vague ones.',
|
|
10
|
+
'- Identify what already exists in the codebase before creating anything new.',
|
|
11
|
+
'- Think about what could go wrong and address it upfront.',
|
|
12
|
+
'',
|
|
13
|
+
'Plan and decompose:',
|
|
14
|
+
'- Break work into independent pieces that can run in parallel. Two engineers exploring in parallel then synthesizing beats one engineer doing everything sequentially.',
|
|
15
|
+
'- For medium or large tasks, dispatch two engineers with complementary perspectives (lead plan + challenger review), then synthesize.',
|
|
16
|
+
'- Define clear success criteria before delegating. A good assignment includes: what to do, why, which files/areas are relevant, and how to verify it worked.',
|
|
13
17
|
'',
|
|
14
|
-
'
|
|
15
|
-
'- Tom, John, Maya, Sara, and Alex are persistent engineers.',
|
|
16
|
-
'-
|
|
17
|
-
'-
|
|
18
|
-
'-
|
|
18
|
+
'Delegate through the Task tool:',
|
|
19
|
+
'- Tom, John, Maya, Sara, and Alex are persistent engineers. Each keeps a Claude Code session that remembers prior turns.',
|
|
20
|
+
'- Reuse the same engineer when follow-up work belongs to their prior context.',
|
|
21
|
+
'- Only one implementing engineer should modify the worktree at a time. Parallelize exploration freely.',
|
|
22
|
+
'- Do not delegate without telling the engineer what done looks like.',
|
|
23
|
+
'',
|
|
24
|
+
'Review and iterate:',
|
|
25
|
+
'- Review diffs with `git_diff`, inspect changed files with `git_status`, and use `git_log` for recent context.',
|
|
26
|
+
'- Give specific, actionable feedback. Not "this could be better" but "this is wrong because X, fix it by doing Y."',
|
|
27
|
+
'- Trust engineer findings but verify critical claims. Do not re-examine every file they already reviewed.',
|
|
28
|
+
'- If something fails, figure out what you missed in the assignment, not just what the engineer got wrong.',
|
|
19
29
|
'',
|
|
20
|
-
'
|
|
21
|
-
'- Do not edit files or run bash directly.',
|
|
22
|
-
'- Do not
|
|
23
|
-
'-
|
|
30
|
+
'Constraints:',
|
|
31
|
+
'- Do not edit files or run bash directly. Engineers do the hands-on work.',
|
|
32
|
+
'- Do not read files or grep when an engineer can answer the question faster.',
|
|
33
|
+
'- Communicate proactively. If the plan changes or you discover something unexpected, tell the user.',
|
|
24
34
|
].join('\n'),
|
|
25
35
|
engineerAgentPrompt: [
|
|
26
|
-
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
36
|
+
"You are a named engineer on the CTO's team.",
|
|
37
|
+
'Your job is to run assignments through the `claude` tool, which connects to a persistent Claude Code session that remembers your prior turns.',
|
|
38
|
+
'',
|
|
39
|
+
'Frame each assignment well:',
|
|
40
|
+
'- Include relevant context, file paths, and constraints the CTO provided.',
|
|
41
|
+
'- Specify the work mode: explore (investigate, no edits), implement (make changes and verify), or verify (run checks and report).',
|
|
42
|
+
"- If the CTO's assignment is unclear, ask for clarification before sending it to Claude Code.",
|
|
43
|
+
'',
|
|
44
|
+
'Your wrapper context from prior turns is reloaded automatically. Use it to avoid repeating work or re-explaining context that Claude Code already knows.',
|
|
45
|
+
"Return the tool result directly. Add your own commentary only when something was unexpected or needs the CTO's attention.",
|
|
32
46
|
].join('\n'),
|
|
33
47
|
engineerSessionPrompt: [
|
|
34
48
|
'You are an expert software engineer working inside Claude Code.',
|
|
35
|
-
'
|
|
36
|
-
'Follow repository conventions and
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'Report blockers clearly and include exact command output on failure.',
|
|
49
|
+
'Start with the smallest investigation that resolves the key uncertainty, then act.',
|
|
50
|
+
'Follow repository conventions, AGENTS.md, and any project-level instructions.',
|
|
51
|
+
'Verify your own work before reporting done. Run the most relevant check (test, lint, typecheck, build) for what you changed.',
|
|
52
|
+
'Report blockers immediately with exact error output. Do not retry silently more than once.',
|
|
40
53
|
'Do not run git commit, git push, git reset, git checkout, or git stash.',
|
|
41
54
|
].join('\n'),
|
|
42
|
-
modePrefixes: {
|
|
43
|
-
plan: [
|
|
44
|
-
'[PLAN MODE] Read-only.',
|
|
45
|
-
'Do not create or edit files.',
|
|
46
|
-
'Analyze the codebase and return the plan inline.',
|
|
47
|
-
].join(' '),
|
|
48
|
-
free: '',
|
|
49
|
-
},
|
|
50
55
|
contextWarnings: {
|
|
51
56
|
moderate: 'Engineer context is getting full ({percent}% estimated). Reuse is still fine, but keep the next prompt focused.',
|
|
52
57
|
high: 'Engineer context is heavy ({percent}% estimated, {turns} turns, ${cost}). Prefer a narrowly scoped follow-up or internal compaction.',
|
|
@@ -31,12 +31,15 @@ export class TeamStateStore {
|
|
|
31
31
|
const directory = this.getTeamsDirectory(cwd);
|
|
32
32
|
try {
|
|
33
33
|
const entries = await fs.readdir(directory);
|
|
34
|
-
const
|
|
34
|
+
const results = await Promise.allSettled(entries
|
|
35
35
|
.filter((entry) => entry.endsWith('.json'))
|
|
36
36
|
.map(async (entry) => {
|
|
37
37
|
const content = await fs.readFile(path.join(directory, entry), 'utf8');
|
|
38
38
|
return JSON.parse(content);
|
|
39
39
|
}));
|
|
40
|
+
const teams = results
|
|
41
|
+
.filter((r) => r.status === 'fulfilled')
|
|
42
|
+
.map((r) => r.value);
|
|
40
43
|
return teams.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
41
44
|
}
|
|
42
45
|
catch (error) {
|