@doingdev/opencode-claude-manager-plugin 0.1.49 → 0.1.51
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 +38 -48
- 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 +9 -4
- package/dist/manager/team-orchestrator.js +84 -31
- package/dist/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/plugin/agent-hierarchy.js +4 -2
- package/dist/plugin/claude-manager.plugin.js +170 -24
- package/dist/plugin/service-factory.d.ts +5 -6
- package/dist/plugin/service-factory.js +9 -17
- package/dist/prompts/registry.js +58 -39
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +2 -3
- package/dist/src/claude/claude-agent-sdk-adapter.js +38 -48
- 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 +9 -4
- package/dist/src/manager/team-orchestrator.js +84 -31
- package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/src/plugin/agent-hierarchy.js +4 -2
- package/dist/src/plugin/claude-manager.plugin.js +170 -24
- package/dist/src/plugin/service-factory.d.ts +5 -6
- package/dist/src/plugin/service-factory.js +9 -17
- package/dist/src/prompts/registry.js +58 -39
- 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 +18 -57
- 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 +103 -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 +4 -4
- package/dist/test/team-orchestrator.test.js +7 -5
- package/dist/test/tool-approval-manager.test.js +17 -17
- package/dist/types/contracts.d.ts +18 -57
- package/package.json +1 -1
|
@@ -1,17 +1,16 @@
|
|
|
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;
|
|
6
7
|
transcriptStore;
|
|
7
8
|
engineerSessionPrompt;
|
|
8
|
-
|
|
9
|
-
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt, projectClaudeFiles) {
|
|
9
|
+
constructor(sessions, teamStore, transcriptStore, engineerSessionPrompt) {
|
|
10
10
|
this.sessions = sessions;
|
|
11
11
|
this.teamStore = teamStore;
|
|
12
12
|
this.transcriptStore = transcriptStore;
|
|
13
13
|
this.engineerSessionPrompt = engineerSessionPrompt;
|
|
14
|
-
this.projectClaudeFiles = projectClaudeFiles;
|
|
15
14
|
}
|
|
16
15
|
async getOrCreateTeam(cwd, teamId) {
|
|
17
16
|
const existing = await this.teamStore.getTeam(cwd, teamId);
|
|
@@ -77,6 +76,16 @@ export class TeamOrchestrator {
|
|
|
77
76
|
}
|
|
78
77
|
return null;
|
|
79
78
|
}
|
|
79
|
+
async resetEngineer(cwd, teamId, engineer, options) {
|
|
80
|
+
await this.updateEngineer(cwd, teamId, engineer, (entry) => ({
|
|
81
|
+
...entry,
|
|
82
|
+
busy: false,
|
|
83
|
+
busySince: null,
|
|
84
|
+
claudeSessionId: options?.clearSession ? null : entry.claudeSessionId,
|
|
85
|
+
wrapperHistory: options?.clearHistory ? [] : entry.wrapperHistory,
|
|
86
|
+
context: options?.clearSession ? createEmptyEngineerRecord(engineer).context : entry.context,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
80
89
|
async dispatchEngineer(input) {
|
|
81
90
|
const team = await this.getOrCreateTeam(input.cwd, input.teamId);
|
|
82
91
|
const engineerState = this.getEngineerState(team, input.engineer);
|
|
@@ -102,10 +111,11 @@ export class TeamOrchestrator {
|
|
|
102
111
|
resumeSessionId: engineerState.claudeSessionId ?? undefined,
|
|
103
112
|
persistSession: true,
|
|
104
113
|
includePartialMessages: true,
|
|
105
|
-
permissionMode:
|
|
114
|
+
permissionMode: 'acceptEdits',
|
|
115
|
+
restrictWriteTools: input.mode === 'explore',
|
|
106
116
|
model: input.model,
|
|
107
117
|
effort: input.mode === 'implement' ? 'high' : 'medium',
|
|
108
|
-
settingSources: ['user'],
|
|
118
|
+
settingSources: ['user', 'project', 'local'],
|
|
109
119
|
abortSignal: input.abortSignal,
|
|
110
120
|
}, input.onEvent);
|
|
111
121
|
tracker.recordResult({
|
|
@@ -124,6 +134,7 @@ export class TeamOrchestrator {
|
|
|
124
134
|
...entry,
|
|
125
135
|
claudeSessionId: result.sessionId ?? engineerState.claudeSessionId,
|
|
126
136
|
busy: false,
|
|
137
|
+
busySince: null,
|
|
127
138
|
lastMode: input.mode,
|
|
128
139
|
lastTaskSummary: summarizeMessage(input.message),
|
|
129
140
|
lastUsedAt: new Date().toISOString(),
|
|
@@ -147,10 +158,38 @@ export class TeamOrchestrator {
|
|
|
147
158
|
await this.updateEngineer(input.cwd, input.teamId, input.engineer, (engineer) => ({
|
|
148
159
|
...engineer,
|
|
149
160
|
busy: false,
|
|
161
|
+
busySince: null,
|
|
150
162
|
}));
|
|
151
163
|
throw error;
|
|
152
164
|
}
|
|
153
165
|
}
|
|
166
|
+
static classifyError(error) {
|
|
167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
+
let failureKind = 'unknown';
|
|
169
|
+
if (message.includes('already working on another assignment')) {
|
|
170
|
+
failureKind = 'engineerBusy';
|
|
171
|
+
}
|
|
172
|
+
else if (message.includes('context') || message.includes('token limit')) {
|
|
173
|
+
failureKind = 'contextExhausted';
|
|
174
|
+
}
|
|
175
|
+
else if (message.includes('denied') || message.includes('not allowed')) {
|
|
176
|
+
failureKind = 'toolDenied';
|
|
177
|
+
}
|
|
178
|
+
else if (message.includes('abort') || message.includes('cancel')) {
|
|
179
|
+
failureKind = 'aborted';
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
failureKind = 'sdkError';
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
teamId: '',
|
|
186
|
+
engineer: 'Tom',
|
|
187
|
+
mode: 'explore',
|
|
188
|
+
failureKind,
|
|
189
|
+
message,
|
|
190
|
+
cause: error,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
154
193
|
async planWithTeam(input) {
|
|
155
194
|
if (input.leadEngineer === input.challengerEngineer) {
|
|
156
195
|
throw new Error('Choose two different engineers for plan synthesis.');
|
|
@@ -185,10 +224,11 @@ export class TeamOrchestrator {
|
|
|
185
224
|
systemPrompt: buildSynthesisSystemPrompt(),
|
|
186
225
|
persistSession: false,
|
|
187
226
|
includePartialMessages: false,
|
|
188
|
-
permissionMode: '
|
|
227
|
+
permissionMode: 'acceptEdits',
|
|
228
|
+
restrictWriteTools: true,
|
|
189
229
|
model: input.model,
|
|
190
230
|
effort: 'high',
|
|
191
|
-
settingSources: ['user'],
|
|
231
|
+
settingSources: ['user', 'project', 'local'],
|
|
192
232
|
abortSignal: input.abortSignal,
|
|
193
233
|
});
|
|
194
234
|
const parsedSynthesis = parseSynthesisResult(synthesisResult.finalText);
|
|
@@ -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
|
};
|
|
@@ -247,17 +293,11 @@ export class TeamOrchestrator {
|
|
|
247
293
|
};
|
|
248
294
|
}
|
|
249
295
|
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
296
|
return [
|
|
256
297
|
this.engineerSessionPrompt,
|
|
257
298
|
'',
|
|
258
299
|
`Assigned engineer: ${engineer}.`,
|
|
259
300
|
`Current work mode: ${mode}.`,
|
|
260
|
-
claudeFileSection,
|
|
261
301
|
]
|
|
262
302
|
.join('\n')
|
|
263
303
|
.trim();
|
|
@@ -265,18 +305,30 @@ export class TeamOrchestrator {
|
|
|
265
305
|
buildEngineerPrompt(mode, message) {
|
|
266
306
|
return `${buildModeInstruction(mode)}\n\n${message}`;
|
|
267
307
|
}
|
|
268
|
-
mapWorkModeToSessionMode(mode) {
|
|
269
|
-
return mode === 'explore' ? 'plan' : 'free';
|
|
270
|
-
}
|
|
271
308
|
}
|
|
272
309
|
function buildModeInstruction(mode) {
|
|
273
310
|
switch (mode) {
|
|
274
311
|
case 'explore':
|
|
275
|
-
return
|
|
312
|
+
return [
|
|
313
|
+
'Investigation mode.',
|
|
314
|
+
'Read, search, and reason about the codebase.',
|
|
315
|
+
'Produce a concrete plan with specific file paths and an approach.',
|
|
316
|
+
'Do not create or edit files.',
|
|
317
|
+
].join(' ');
|
|
276
318
|
case 'implement':
|
|
277
|
-
return
|
|
319
|
+
return [
|
|
320
|
+
'Implementation mode.',
|
|
321
|
+
'Make the changes, run the most relevant verification (tests, lint, typecheck), and report what changed and what you verified.',
|
|
322
|
+
'Before reporting done, review your own diff for issues that pass tests but break in production.',
|
|
323
|
+
].join(' ');
|
|
278
324
|
case 'verify':
|
|
279
|
-
return
|
|
325
|
+
return [
|
|
326
|
+
'Verification mode.',
|
|
327
|
+
'Run targeted checks in order of relevance: tests, lint, typecheck, build.',
|
|
328
|
+
'Check that changed code paths have test coverage.',
|
|
329
|
+
'Report pass/fail with evidence.',
|
|
330
|
+
'Escalate failures with exact output.',
|
|
331
|
+
].join(' ');
|
|
280
332
|
}
|
|
281
333
|
}
|
|
282
334
|
function summarizeMessage(message) {
|
|
@@ -292,28 +344,29 @@ function appendWrapperHistoryEntries(existing, nextEntries) {
|
|
|
292
344
|
}
|
|
293
345
|
function buildPlanDraftRequest(perspective, request) {
|
|
294
346
|
const posture = perspective === 'lead'
|
|
295
|
-
? 'Propose the most direct workable plan.'
|
|
296
|
-
: '
|
|
347
|
+
? 'You are the lead planner. Propose the most direct workable plan with concrete file paths and clear next steps. Think about failure modes and edge cases — what can break at each boundary?'
|
|
348
|
+
: 'You are the challenger. Stress-test assumptions, surface missing decisions, and propose a stronger alternative when the lead plan is weak. Think about failure modes and edge cases — what can break at each boundary?';
|
|
297
349
|
return [
|
|
298
350
|
posture,
|
|
299
351
|
'',
|
|
300
352
|
'Return exactly these sections:',
|
|
301
353
|
'1. Objective',
|
|
302
|
-
'2. Proposed approach',
|
|
354
|
+
'2. Proposed approach (include system boundaries and data flow)',
|
|
303
355
|
'3. Files or systems likely involved',
|
|
304
|
-
'4.
|
|
305
|
-
'5.
|
|
306
|
-
'6.
|
|
356
|
+
'4. Failure modes and edge cases (what happens when things go wrong?)',
|
|
357
|
+
'5. Risks and open questions',
|
|
358
|
+
'6. Verification (how to prove it works, including what tests to add)',
|
|
359
|
+
'7. Step-by-step plan',
|
|
307
360
|
'',
|
|
308
361
|
`User request: ${request}`,
|
|
309
362
|
].join('\n');
|
|
310
363
|
}
|
|
311
364
|
function buildSynthesisSystemPrompt() {
|
|
312
365
|
return [
|
|
313
|
-
'You are
|
|
314
|
-
'
|
|
315
|
-
'Prefer the
|
|
316
|
-
'If
|
|
366
|
+
'You are synthesizing two independent engineering plans into one stronger plan.',
|
|
367
|
+
'Compare them on clarity, feasibility, risk, and fit to the user request.',
|
|
368
|
+
'Prefer the simplest path that fully addresses the goal.',
|
|
369
|
+
'If the plans disagree on something only the user can decide, surface exactly one recommended question and one recommended answer.',
|
|
317
370
|
'Use this output format exactly:',
|
|
318
371
|
'## Synthesis',
|
|
319
372
|
'<combined plan>',
|
|
@@ -8,7 +8,6 @@ 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"];
|
|
12
11
|
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
13
12
|
type AgentPermission = {
|
|
14
13
|
'*'?: 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',
|
|
@@ -22,7 +24,7 @@ const CTO_ONLY_TOOL_IDS = [
|
|
|
22
24
|
'approval_update',
|
|
23
25
|
];
|
|
24
26
|
const ENGINEER_TOOL_IDS = ['claude'];
|
|
25
|
-
|
|
27
|
+
const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
|
|
26
28
|
const CTO_READONLY_TOOLS = {
|
|
27
29
|
read: 'allow',
|
|
28
30
|
grep: 'allow',
|
|
@@ -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,14 +1,14 @@
|
|
|
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 {
|
|
4
|
+
import { TeamOrchestrator } from '../manager/team-orchestrator.js';
|
|
5
5
|
import { AGENT_CTO, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
|
|
6
6
|
import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
|
|
7
7
|
const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
|
|
8
8
|
const MODE_ENUM = ['explore', 'implement', 'verify'];
|
|
9
9
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const services = getOrCreatePluginServices(worktree);
|
|
11
|
+
await services.approvalManager.loadPersistedPolicy();
|
|
12
12
|
return {
|
|
13
13
|
config: async (config) => {
|
|
14
14
|
config.agent ??= {};
|
|
@@ -103,6 +103,69 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
103
103
|
return JSON.stringify(team, null, 2);
|
|
104
104
|
},
|
|
105
105
|
}),
|
|
106
|
+
plan_with_team: tool({
|
|
107
|
+
description: 'Run dual-engineer plan synthesis. Two engineers explore in parallel (lead + challenger), then their plans are synthesized into one stronger plan.',
|
|
108
|
+
args: {
|
|
109
|
+
request: tool.schema.string().min(1),
|
|
110
|
+
leadEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
111
|
+
challengerEngineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
112
|
+
model: tool.schema.enum(MODEL_ENUM).optional(),
|
|
113
|
+
},
|
|
114
|
+
async execute(args, context) {
|
|
115
|
+
const teamId = getActiveTeamSession(context.worktree) ?? context.sessionID;
|
|
116
|
+
annotateToolRun(context, 'Running dual-engineer plan synthesis', {
|
|
117
|
+
teamId,
|
|
118
|
+
lead: args.leadEngineer,
|
|
119
|
+
challenger: args.challengerEngineer,
|
|
120
|
+
});
|
|
121
|
+
const result = await services.orchestrator.planWithTeam({
|
|
122
|
+
teamId,
|
|
123
|
+
cwd: context.worktree,
|
|
124
|
+
request: args.request,
|
|
125
|
+
leadEngineer: args.leadEngineer,
|
|
126
|
+
challengerEngineer: args.challengerEngineer,
|
|
127
|
+
model: args.model,
|
|
128
|
+
abortSignal: context.abort,
|
|
129
|
+
});
|
|
130
|
+
context.metadata({
|
|
131
|
+
title: '✅ Plan synthesis complete',
|
|
132
|
+
metadata: {
|
|
133
|
+
teamId: result.teamId,
|
|
134
|
+
lead: result.leadEngineer,
|
|
135
|
+
challenger: result.challengerEngineer,
|
|
136
|
+
hasQuestion: result.recommendedQuestion !== null,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return JSON.stringify({
|
|
140
|
+
synthesis: result.synthesis,
|
|
141
|
+
recommendedQuestion: result.recommendedQuestion,
|
|
142
|
+
recommendedAnswer: result.recommendedAnswer,
|
|
143
|
+
}, null, 2);
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
reset_engineer: tool({
|
|
147
|
+
description: 'Reset a stuck or corrupted engineer. Clears the busy flag. Optionally clears the Claude session (starts fresh) and/or wrapper history.',
|
|
148
|
+
args: {
|
|
149
|
+
engineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex']),
|
|
150
|
+
clearSession: tool.schema.boolean().optional(),
|
|
151
|
+
clearHistory: tool.schema.boolean().optional(),
|
|
152
|
+
},
|
|
153
|
+
async execute(args, context) {
|
|
154
|
+
const teamId = getActiveTeamSession(context.worktree) ?? context.sessionID;
|
|
155
|
+
annotateToolRun(context, `Resetting ${args.engineer}`, {
|
|
156
|
+
teamId,
|
|
157
|
+
clearSession: args.clearSession,
|
|
158
|
+
clearHistory: args.clearHistory,
|
|
159
|
+
});
|
|
160
|
+
await services.orchestrator.resetEngineer(context.worktree, teamId, args.engineer, {
|
|
161
|
+
clearSession: args.clearSession,
|
|
162
|
+
clearHistory: args.clearHistory,
|
|
163
|
+
});
|
|
164
|
+
const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
|
|
165
|
+
const engineer = team.engineers.find((e) => e.name === args.engineer);
|
|
166
|
+
return JSON.stringify({ reset: true, engineer: engineer ?? args.engineer }, null, 2);
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
106
169
|
git_diff: tool({
|
|
107
170
|
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
171
|
args: {
|
|
@@ -125,13 +188,17 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
125
188
|
},
|
|
126
189
|
}),
|
|
127
190
|
git_commit: tool({
|
|
128
|
-
description: '
|
|
191
|
+
description: 'Create a commit. Stages all changes by default, or only the specified paths if provided.',
|
|
129
192
|
args: {
|
|
130
193
|
message: tool.schema.string().min(1),
|
|
194
|
+
paths: tool.schema.string().array().optional(),
|
|
131
195
|
},
|
|
132
196
|
async execute(args, context) {
|
|
133
|
-
annotateToolRun(context, 'Committing changes', {
|
|
134
|
-
|
|
197
|
+
annotateToolRun(context, 'Committing changes', {
|
|
198
|
+
message: args.message,
|
|
199
|
+
paths: args.paths,
|
|
200
|
+
});
|
|
201
|
+
const result = await services.manager.gitCommit(args.message, args.paths);
|
|
135
202
|
return JSON.stringify(result, null, 2);
|
|
136
203
|
},
|
|
137
204
|
}),
|
|
@@ -249,7 +316,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
249
316
|
error: 'addRule requires ruleId, toolPattern, and ruleAction',
|
|
250
317
|
});
|
|
251
318
|
}
|
|
252
|
-
services.approvalManager.addRule({
|
|
319
|
+
await services.approvalManager.addRule({
|
|
253
320
|
id: args.ruleId,
|
|
254
321
|
toolPattern: args.toolPattern,
|
|
255
322
|
inputPattern: args.inputPattern,
|
|
@@ -262,20 +329,20 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
262
329
|
if (!args.ruleId) {
|
|
263
330
|
return JSON.stringify({ error: 'removeRule requires ruleId' });
|
|
264
331
|
}
|
|
265
|
-
const removed = services.approvalManager.removeRule(args.ruleId);
|
|
332
|
+
const removed = await services.approvalManager.removeRule(args.ruleId);
|
|
266
333
|
return JSON.stringify({ removed }, null, 2);
|
|
267
334
|
}
|
|
268
335
|
else if (args.action === 'setDefault') {
|
|
269
336
|
if (!args.defaultAction) {
|
|
270
337
|
return JSON.stringify({ error: 'setDefault requires defaultAction' });
|
|
271
338
|
}
|
|
272
|
-
services.approvalManager.setDefaultAction(args.defaultAction);
|
|
339
|
+
await services.approvalManager.setDefaultAction(args.defaultAction);
|
|
273
340
|
}
|
|
274
341
|
else if (args.action === 'setEnabled') {
|
|
275
342
|
if (args.enabled === undefined) {
|
|
276
343
|
return JSON.stringify({ error: 'setEnabled requires enabled' });
|
|
277
344
|
}
|
|
278
|
-
services.approvalManager.setEnabled(args.enabled);
|
|
345
|
+
await services.approvalManager.setEnabled(args.enabled);
|
|
279
346
|
}
|
|
280
347
|
else if (args.action === 'clearDecisions') {
|
|
281
348
|
services.approvalManager.clearDecisions();
|
|
@@ -292,16 +359,35 @@ async function runEngineerAssignment(input, context) {
|
|
|
292
359
|
teamId: input.teamId,
|
|
293
360
|
mode: input.mode,
|
|
294
361
|
});
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
362
|
+
let result;
|
|
363
|
+
try {
|
|
364
|
+
result = await services.orchestrator.dispatchEngineer({
|
|
365
|
+
teamId: input.teamId,
|
|
366
|
+
cwd: context.worktree,
|
|
367
|
+
engineer: input.engineer,
|
|
368
|
+
mode: input.mode,
|
|
369
|
+
message: input.message,
|
|
370
|
+
model: input.model,
|
|
371
|
+
abortSignal: context.abort,
|
|
372
|
+
onEvent: (event) => reportClaudeEvent(context, input.engineer, event),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const failure = TeamOrchestrator.classifyError(error);
|
|
377
|
+
failure.teamId = input.teamId;
|
|
378
|
+
failure.engineer = input.engineer;
|
|
379
|
+
failure.mode = input.mode;
|
|
380
|
+
context.metadata({
|
|
381
|
+
title: `❌ ${input.engineer} failed (${failure.failureKind})`,
|
|
382
|
+
metadata: {
|
|
383
|
+
teamId: failure.teamId,
|
|
384
|
+
engineer: failure.engineer,
|
|
385
|
+
failureKind: failure.failureKind,
|
|
386
|
+
message: failure.message.slice(0, 200),
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
305
391
|
await services.orchestrator.recordWrapperExchange(context.worktree, input.teamId, input.engineer, context.sessionID, input.mode, input.message, result.finalText);
|
|
306
392
|
context.metadata({
|
|
307
393
|
title: `✅ ${input.engineer} finished`,
|
|
@@ -335,6 +421,60 @@ function isEngineerAgent(agentId) {
|
|
|
335
421
|
async function resolveTeamId(worktree, sessionID) {
|
|
336
422
|
return (await getPersistedActiveTeam(worktree)) ?? getActiveTeamSession(worktree) ?? sessionID;
|
|
337
423
|
}
|
|
424
|
+
function formatToolDescription(toolName, toolArgs) {
|
|
425
|
+
if (!toolArgs || typeof toolArgs !== 'object')
|
|
426
|
+
return undefined;
|
|
427
|
+
const args = toolArgs;
|
|
428
|
+
switch (toolName) {
|
|
429
|
+
case 'Read':
|
|
430
|
+
case 'read': {
|
|
431
|
+
const filePath = args.file_path;
|
|
432
|
+
return typeof filePath === 'string' ? `Reading: ${filePath}` : undefined;
|
|
433
|
+
}
|
|
434
|
+
case 'Grep':
|
|
435
|
+
case 'grep': {
|
|
436
|
+
const pattern = args.pattern;
|
|
437
|
+
return typeof pattern === 'string' ? `Searching: ${pattern}` : undefined;
|
|
438
|
+
}
|
|
439
|
+
case 'Write':
|
|
440
|
+
case 'write': {
|
|
441
|
+
const filePath = args.file_path;
|
|
442
|
+
return typeof filePath === 'string' ? `Writing: ${filePath}` : undefined;
|
|
443
|
+
}
|
|
444
|
+
case 'Edit':
|
|
445
|
+
case 'edit': {
|
|
446
|
+
const filePath = args.file_path;
|
|
447
|
+
return typeof filePath === 'string' ? `Editing: ${filePath}` : undefined;
|
|
448
|
+
}
|
|
449
|
+
case 'Bash':
|
|
450
|
+
case 'bash':
|
|
451
|
+
case 'Run':
|
|
452
|
+
case 'run': {
|
|
453
|
+
const command = args.command;
|
|
454
|
+
return typeof command === 'string' ? `Running: ${command.slice(0, 80)}` : undefined;
|
|
455
|
+
}
|
|
456
|
+
case 'WebFetch':
|
|
457
|
+
case 'webfetch': {
|
|
458
|
+
const url = args.url;
|
|
459
|
+
return typeof url === 'string' ? `Fetching: ${url}` : undefined;
|
|
460
|
+
}
|
|
461
|
+
case 'Glob':
|
|
462
|
+
case 'glob': {
|
|
463
|
+
const pattern = args.pattern;
|
|
464
|
+
return typeof pattern === 'string' ? `Matching: ${pattern}` : undefined;
|
|
465
|
+
}
|
|
466
|
+
case 'TodoWrite':
|
|
467
|
+
case 'todowrite':
|
|
468
|
+
return args.content ? `Updating todos` : undefined;
|
|
469
|
+
case 'NotebookEdit':
|
|
470
|
+
case 'notebook_edit': {
|
|
471
|
+
const cellPath = args.notebook_cell_path;
|
|
472
|
+
return typeof cellPath === 'string' ? `Editing notebook: ${cellPath}` : undefined;
|
|
473
|
+
}
|
|
474
|
+
default:
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
338
478
|
function reportClaudeEvent(context, engineer, event) {
|
|
339
479
|
if (event.type === 'error') {
|
|
340
480
|
context.metadata({
|
|
@@ -382,10 +522,13 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
382
522
|
catch {
|
|
383
523
|
// event.text is not valid JSON — fall back to generic title
|
|
384
524
|
}
|
|
525
|
+
const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
|
|
385
526
|
context.metadata({
|
|
386
|
-
title:
|
|
387
|
-
? `⚡ ${engineer} → ${
|
|
388
|
-
:
|
|
527
|
+
title: toolDescription
|
|
528
|
+
? `⚡ ${engineer} → ${toolDescription}`
|
|
529
|
+
: toolName
|
|
530
|
+
? `⚡ ${engineer} → ${toolName}`
|
|
531
|
+
: `⚡ ${engineer} is using Claude Code tools`,
|
|
389
532
|
metadata: {
|
|
390
533
|
engineer,
|
|
391
534
|
sessionId: event.sessionId,
|
|
@@ -397,12 +540,15 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
397
540
|
return;
|
|
398
541
|
}
|
|
399
542
|
if (event.type === 'assistant' || event.type === 'partial') {
|
|
543
|
+
const isThinking = event.text.startsWith('<thinking>');
|
|
544
|
+
const stateLabel = event.type === 'partial' && isThinking ? 'is thinking' : 'is working';
|
|
400
545
|
context.metadata({
|
|
401
|
-
title: `⚡ ${engineer}
|
|
546
|
+
title: `⚡ ${engineer} ${stateLabel}`,
|
|
402
547
|
metadata: {
|
|
403
548
|
engineer,
|
|
404
549
|
sessionId: event.sessionId,
|
|
405
550
|
preview: event.text.slice(0, 160),
|
|
551
|
+
isThinking,
|
|
406
552
|
},
|
|
407
553
|
});
|
|
408
554
|
}
|
|
@@ -1,19 +1,17 @@
|
|
|
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
|
-
import { TeamStateStore } from '../state/team-state-store.js';
|
|
5
3
|
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
6
4
|
import { TeamOrchestrator } from '../manager/team-orchestrator.js';
|
|
7
|
-
import
|
|
8
|
-
|
|
5
|
+
import { TeamStateStore } from '../state/team-state-store.js';
|
|
6
|
+
import type { EngineerName } from '../types/contracts.js';
|
|
7
|
+
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
|
}
|
|
16
|
-
export declare function getOrCreatePluginServices(worktree: string
|
|
14
|
+
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
17
15
|
export declare function clearPluginServices(): void;
|
|
18
16
|
export declare function setActiveTeamSession(worktree: string, teamId: string): void;
|
|
19
17
|
export declare function getActiveTeamSession(worktree: string): string | null;
|
|
@@ -27,3 +25,4 @@ export declare function getWrapperSessionMapping(worktree: string, wrapperSessio
|
|
|
27
25
|
teamId: string;
|
|
28
26
|
engineer: EngineerName;
|
|
29
27
|
} | null;
|
|
28
|
+
export {};
|
|
@@ -1,42 +1,34 @@
|
|
|
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
|
-
import { TeamStateStore } from '../state/team-state-store.js';
|
|
7
|
-
import { TranscriptStore } from '../state/transcript-store.js';
|
|
8
|
-
import { ContextTracker } from '../manager/context-tracker.js';
|
|
9
5
|
import { GitOperations } from '../manager/git-operations.js';
|
|
10
|
-
import { SessionController } from '../manager/session-controller.js';
|
|
11
6
|
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
12
|
-
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
13
7
|
import { TeamOrchestrator } from '../manager/team-orchestrator.js';
|
|
8
|
+
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
9
|
+
import { TeamStateStore } from '../state/team-state-store.js';
|
|
10
|
+
import { TranscriptStore } from '../state/transcript-store.js';
|
|
14
11
|
const serviceRegistry = new Map();
|
|
15
12
|
const activeTeamRegistry = new Map();
|
|
16
13
|
const wrapperSessionRegistry = new Map();
|
|
17
|
-
export function getOrCreatePluginServices(worktree
|
|
14
|
+
export function getOrCreatePluginServices(worktree) {
|
|
18
15
|
const existing = serviceRegistry.get(worktree);
|
|
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
|
|
34
|
-
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt, projectClaudeFiles);
|
|
26
|
+
const manager = new PersistentManager(gitOps, transcriptStore);
|
|
27
|
+
const orchestrator = new TeamOrchestrator(sessionService, teamStore, transcriptStore, managerPromptRegistry.engineerSessionPrompt);
|
|
35
28
|
const services = {
|
|
36
29
|
manager,
|
|
37
30
|
sessions: sessionService,
|
|
38
31
|
approvalManager,
|
|
39
|
-
liveTailer,
|
|
40
32
|
teamStore,
|
|
41
33
|
orchestrator,
|
|
42
34
|
};
|