@doingdev/opencode-claude-manager-plugin 0.1.47 → 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 +218 -25
- package/dist/plugin/service-factory.d.ts +2 -2
- package/dist/plugin/service-factory.js +18 -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 +218 -25
- package/dist/src/plugin/service-factory.d.ts +2 -2
- package/dist/src/plugin/service-factory.js +18 -12
- package/dist/src/prompts/registry.js +42 -37
- package/dist/src/state/team-state-store.d.ts +3 -0
- package/dist/src/state/team-state-store.js +26 -1
- package/dist/src/team/roster.js +1 -0
- package/dist/src/types/contracts.d.ts +9 -49
- package/dist/state/team-state-store.d.ts +3 -0
- package/dist/state/team-state-store.js +26 -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/cto-active-team.test.d.ts +1 -0
- package/dist/test/cto-active-team.test.js +52 -0
- 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.d.ts +1 -0
- package/dist/test/report-claude-event.test.js +246 -0
- package/dist/test/team-state-store.test.js +18 -0
- 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,14 +1,16 @@
|
|
|
1
1
|
import { tool } from '@opencode-ai/plugin';
|
|
2
2
|
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
3
|
+
import { isEngineerName } from '../team/roster.js';
|
|
4
|
+
import { TeamOrchestrator } from '../manager/team-orchestrator.js';
|
|
3
5
|
import { discoverProjectClaudeFiles } from '../util/project-context.js';
|
|
4
6
|
import { AGENT_CTO, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, buildCtoAgentConfig, buildEngineerAgentConfig, denyRestrictedToolsGlobally, } from './agent-hierarchy.js';
|
|
5
|
-
import { getActiveTeamSession, getOrCreatePluginServices, getWrapperSessionMapping, setActiveTeamSession, setWrapperSessionMapping, } from './service-factory.js';
|
|
6
|
-
import { isEngineerName } from '../team/roster.js';
|
|
7
|
+
import { getActiveTeamSession, getOrCreatePluginServices, getPersistedActiveTeam, getWrapperSessionMapping, setActiveTeamSession, setPersistedActiveTeam, setWrapperSessionMapping, } from './service-factory.js';
|
|
7
8
|
const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
|
|
8
9
|
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 ??= {};
|
|
@@ -21,7 +23,15 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
21
23
|
},
|
|
22
24
|
'chat.message': async (input) => {
|
|
23
25
|
if (input.agent === AGENT_CTO) {
|
|
24
|
-
|
|
26
|
+
// Adopt the persisted active team if one exists, so a new CTO session
|
|
27
|
+
// does not orphan previously created engineers and wrapper memory.
|
|
28
|
+
const persistedTeamId = await getPersistedActiveTeam(worktree);
|
|
29
|
+
const activeTeamId = persistedTeamId ?? input.sessionID;
|
|
30
|
+
setActiveTeamSession(worktree, activeTeamId);
|
|
31
|
+
if (!persistedTeamId) {
|
|
32
|
+
// First CTO session for this worktree — persist this session as active team.
|
|
33
|
+
await setPersistedActiveTeam(worktree, activeTeamId);
|
|
34
|
+
}
|
|
25
35
|
return;
|
|
26
36
|
}
|
|
27
37
|
if (input.agent && isEngineerAgent(input.agent)) {
|
|
@@ -29,7 +39,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
29
39
|
const existing = getWrapperSessionMapping(worktree, input.sessionID);
|
|
30
40
|
const persisted = existing ??
|
|
31
41
|
(await services.orchestrator.findTeamByWrapperSession(worktree, input.sessionID));
|
|
32
|
-
const teamId = persisted?.teamId ??
|
|
42
|
+
const teamId = persisted?.teamId ?? (await resolveTeamId(worktree, input.sessionID));
|
|
33
43
|
setWrapperSessionMapping(worktree, input.sessionID, {
|
|
34
44
|
teamId,
|
|
35
45
|
engineer,
|
|
@@ -65,7 +75,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
65
75
|
const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
|
|
66
76
|
const persisted = existing ??
|
|
67
77
|
(await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
|
|
68
|
-
const teamId = persisted?.teamId ??
|
|
78
|
+
const teamId = persisted?.teamId ?? (await resolveTeamId(context.worktree, context.sessionID));
|
|
69
79
|
setWrapperSessionMapping(context.worktree, context.sessionID, {
|
|
70
80
|
teamId,
|
|
71
81
|
engineer,
|
|
@@ -78,7 +88,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
78
88
|
message: args.message,
|
|
79
89
|
model: args.model,
|
|
80
90
|
}, context);
|
|
81
|
-
return
|
|
91
|
+
return result.finalText;
|
|
82
92
|
},
|
|
83
93
|
}),
|
|
84
94
|
team_status: tool({
|
|
@@ -95,6 +105,69 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
95
105
|
return JSON.stringify(team, null, 2);
|
|
96
106
|
},
|
|
97
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
|
+
}),
|
|
98
171
|
git_diff: tool({
|
|
99
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.',
|
|
100
173
|
args: {
|
|
@@ -117,13 +190,17 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
117
190
|
},
|
|
118
191
|
}),
|
|
119
192
|
git_commit: tool({
|
|
120
|
-
description: '
|
|
193
|
+
description: 'Create a commit. Stages all changes by default, or only the specified paths if provided.',
|
|
121
194
|
args: {
|
|
122
195
|
message: tool.schema.string().min(1),
|
|
196
|
+
paths: tool.schema.string().array().optional(),
|
|
123
197
|
},
|
|
124
198
|
async execute(args, context) {
|
|
125
|
-
annotateToolRun(context, 'Committing changes', {
|
|
126
|
-
|
|
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);
|
|
127
204
|
return JSON.stringify(result, null, 2);
|
|
128
205
|
},
|
|
129
206
|
}),
|
|
@@ -241,7 +318,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
241
318
|
error: 'addRule requires ruleId, toolPattern, and ruleAction',
|
|
242
319
|
});
|
|
243
320
|
}
|
|
244
|
-
services.approvalManager.addRule({
|
|
321
|
+
await services.approvalManager.addRule({
|
|
245
322
|
id: args.ruleId,
|
|
246
323
|
toolPattern: args.toolPattern,
|
|
247
324
|
inputPattern: args.inputPattern,
|
|
@@ -254,20 +331,20 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
254
331
|
if (!args.ruleId) {
|
|
255
332
|
return JSON.stringify({ error: 'removeRule requires ruleId' });
|
|
256
333
|
}
|
|
257
|
-
const removed = services.approvalManager.removeRule(args.ruleId);
|
|
334
|
+
const removed = await services.approvalManager.removeRule(args.ruleId);
|
|
258
335
|
return JSON.stringify({ removed }, null, 2);
|
|
259
336
|
}
|
|
260
337
|
else if (args.action === 'setDefault') {
|
|
261
338
|
if (!args.defaultAction) {
|
|
262
339
|
return JSON.stringify({ error: 'setDefault requires defaultAction' });
|
|
263
340
|
}
|
|
264
|
-
services.approvalManager.setDefaultAction(args.defaultAction);
|
|
341
|
+
await services.approvalManager.setDefaultAction(args.defaultAction);
|
|
265
342
|
}
|
|
266
343
|
else if (args.action === 'setEnabled') {
|
|
267
344
|
if (args.enabled === undefined) {
|
|
268
345
|
return JSON.stringify({ error: 'setEnabled requires enabled' });
|
|
269
346
|
}
|
|
270
|
-
services.approvalManager.setEnabled(args.enabled);
|
|
347
|
+
await services.approvalManager.setEnabled(args.enabled);
|
|
271
348
|
}
|
|
272
349
|
else if (args.action === 'clearDecisions') {
|
|
273
350
|
services.approvalManager.clearDecisions();
|
|
@@ -284,16 +361,35 @@ async function runEngineerAssignment(input, context) {
|
|
|
284
361
|
teamId: input.teamId,
|
|
285
362
|
mode: input.mode,
|
|
286
363
|
});
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
}
|
|
297
393
|
await services.orchestrator.recordWrapperExchange(context.worktree, input.teamId, input.engineer, context.sessionID, input.mode, input.message, result.finalText);
|
|
298
394
|
context.metadata({
|
|
299
395
|
title: `✅ ${input.engineer} finished`,
|
|
@@ -319,6 +415,68 @@ function engineerFromAgent(agentId) {
|
|
|
319
415
|
function isEngineerAgent(agentId) {
|
|
320
416
|
return Object.values(ENGINEER_AGENT_IDS).includes(agentId);
|
|
321
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Resolves the team ID for an engineer session.
|
|
420
|
+
* Reads the persisted active team first (survives process restarts), then
|
|
421
|
+
* falls back to the in-memory registry, then to the raw session ID as a last resort.
|
|
422
|
+
*/
|
|
423
|
+
async function resolveTeamId(worktree, sessionID) {
|
|
424
|
+
return (await getPersistedActiveTeam(worktree)) ?? getActiveTeamSession(worktree) ?? sessionID;
|
|
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
|
+
}
|
|
322
480
|
function reportClaudeEvent(context, engineer, event) {
|
|
323
481
|
if (event.type === 'error') {
|
|
324
482
|
context.metadata({
|
|
@@ -342,22 +500,57 @@ function reportClaudeEvent(context, engineer, event) {
|
|
|
342
500
|
return;
|
|
343
501
|
}
|
|
344
502
|
if (event.type === 'tool_call') {
|
|
503
|
+
let toolName;
|
|
504
|
+
let toolId;
|
|
505
|
+
let toolArgs;
|
|
506
|
+
try {
|
|
507
|
+
const parsed = JSON.parse(event.text);
|
|
508
|
+
toolName = parsed.name;
|
|
509
|
+
toolId = parsed.id;
|
|
510
|
+
// Some SDK versions serialize the input object as a JSON string inside the outer JSON.
|
|
511
|
+
// Try to double-decode it so callers always receive a plain object.
|
|
512
|
+
if (typeof parsed.input === 'string') {
|
|
513
|
+
try {
|
|
514
|
+
toolArgs = JSON.parse(parsed.input);
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
toolArgs = parsed.input;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
toolArgs = parsed.input;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// event.text is not valid JSON — fall back to generic title
|
|
526
|
+
}
|
|
527
|
+
const toolDescription = formatToolDescription(toolName ?? '', toolArgs);
|
|
345
528
|
context.metadata({
|
|
346
|
-
title:
|
|
529
|
+
title: toolDescription
|
|
530
|
+
? `⚡ ${engineer} → ${toolDescription}`
|
|
531
|
+
: toolName
|
|
532
|
+
? `⚡ ${engineer} → ${toolName}`
|
|
533
|
+
: `⚡ ${engineer} is using Claude Code tools`,
|
|
347
534
|
metadata: {
|
|
348
535
|
engineer,
|
|
349
536
|
sessionId: event.sessionId,
|
|
537
|
+
...(toolName !== undefined && { toolName }),
|
|
538
|
+
...(toolId !== undefined && { toolId }),
|
|
539
|
+
...(toolArgs !== undefined && { toolArgs }),
|
|
350
540
|
},
|
|
351
541
|
});
|
|
352
542
|
return;
|
|
353
543
|
}
|
|
354
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';
|
|
355
547
|
context.metadata({
|
|
356
|
-
title: `⚡ ${engineer}
|
|
548
|
+
title: `⚡ ${engineer} ${stateLabel}`,
|
|
357
549
|
metadata: {
|
|
358
550
|
engineer,
|
|
359
551
|
sessionId: event.sessionId,
|
|
360
552
|
preview: event.text.slice(0, 160),
|
|
553
|
+
isThinking,
|
|
361
554
|
},
|
|
362
555
|
});
|
|
363
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
|
}
|
|
@@ -17,6 +15,8 @@ export declare function getOrCreatePluginServices(worktree: string, projectClaud
|
|
|
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;
|
|
18
|
+
export declare function getPersistedActiveTeam(worktree: string): Promise<string | null>;
|
|
19
|
+
export declare function setPersistedActiveTeam(worktree: string, teamId: string): Promise<void>;
|
|
20
20
|
export declare function setWrapperSessionMapping(worktree: string, wrapperSessionId: string, mapping: {
|
|
21
21
|
teamId: string;
|
|
22
22
|
engineer: EngineerName;
|
|
@@ -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
|
};
|
|
@@ -54,6 +46,20 @@ export function setActiveTeamSession(worktree, teamId) {
|
|
|
54
46
|
export function getActiveTeamSession(worktree) {
|
|
55
47
|
return activeTeamRegistry.get(worktree) ?? null;
|
|
56
48
|
}
|
|
49
|
+
export async function getPersistedActiveTeam(worktree) {
|
|
50
|
+
const services = serviceRegistry.get(worktree);
|
|
51
|
+
if (!services) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return services.teamStore.getActiveTeam(worktree);
|
|
55
|
+
}
|
|
56
|
+
export async function setPersistedActiveTeam(worktree, teamId) {
|
|
57
|
+
const services = serviceRegistry.get(worktree);
|
|
58
|
+
if (!services) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await services.teamStore.setActiveTeam(worktree, teamId);
|
|
62
|
+
}
|
|
57
63
|
export function setWrapperSessionMapping(worktree, wrapperSessionId, mapping) {
|
|
58
64
|
wrapperSessionRegistry.set(`${worktree}:${wrapperSessionId}`, mapping);
|
|
59
65
|
}
|