@doingdev/opencode-claude-manager-plugin 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -129
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/plugin/orchestrator.plugin.d.ts +14 -0
- package/dist/plugin/orchestrator.plugin.js +108 -0
- package/dist/prompts/registry.d.ts +8 -2
- package/dist/prompts/registry.js +30 -159
- package/dist/safety/bash-safety.d.ts +21 -0
- package/dist/safety/bash-safety.js +62 -0
- package/package.json +3 -6
- package/dist/claude/claude-agent-sdk-adapter.d.ts +0 -27
- package/dist/claude/claude-agent-sdk-adapter.js +0 -520
- package/dist/claude/claude-session.service.d.ts +0 -15
- package/dist/claude/claude-session.service.js +0 -23
- package/dist/claude/delegated-can-use-tool.d.ts +0 -7
- package/dist/claude/delegated-can-use-tool.js +0 -178
- package/dist/claude/session-live-tailer.d.ts +0 -51
- package/dist/claude/session-live-tailer.js +0 -269
- package/dist/claude/tool-approval-manager.d.ts +0 -27
- package/dist/claude/tool-approval-manager.js +0 -238
- package/dist/manager/context-tracker.d.ts +0 -33
- package/dist/manager/context-tracker.js +0 -108
- package/dist/manager/git-operations.d.ts +0 -12
- package/dist/manager/git-operations.js +0 -76
- package/dist/manager/manager-orchestrator.d.ts +0 -17
- package/dist/manager/manager-orchestrator.js +0 -178
- package/dist/manager/parallel-session-job-manager.d.ts +0 -49
- package/dist/manager/parallel-session-job-manager.js +0 -177
- package/dist/manager/persistent-manager.d.ts +0 -74
- package/dist/manager/persistent-manager.js +0 -167
- package/dist/manager/session-controller.d.ts +0 -45
- package/dist/manager/session-controller.js +0 -147
- package/dist/manager/task-planner.d.ts +0 -5
- package/dist/manager/task-planner.js +0 -15
- package/dist/metadata/claude-metadata.service.d.ts +0 -12
- package/dist/metadata/claude-metadata.service.js +0 -38
- package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
- package/dist/metadata/repo-claude-config-reader.js +0 -154
- package/dist/plugin/claude-code-permission-bridge.d.ts +0 -15
- package/dist/plugin/claude-code-permission-bridge.js +0 -184
- package/dist/plugin/claude-manager.plugin.d.ts +0 -2
- package/dist/plugin/claude-manager.plugin.js +0 -627
- package/dist/plugin/service-factory.d.ts +0 -12
- package/dist/plugin/service-factory.js +0 -41
- package/dist/state/file-run-state-store.d.ts +0 -14
- package/dist/state/file-run-state-store.js +0 -87
- package/dist/state/transcript-store.d.ts +0 -15
- package/dist/state/transcript-store.js +0 -44
- package/dist/types/contracts.d.ts +0 -215
- package/dist/types/contracts.js +0 -1
- package/dist/util/fs-helpers.d.ts +0 -2
- package/dist/util/fs-helpers.js +0 -12
- package/dist/util/transcript-append.d.ts +0 -7
- package/dist/util/transcript-append.js +0 -29
- package/dist/worktree/worktree-coordinator.d.ts +0 -21
- package/dist/worktree/worktree-coordinator.js +0 -64
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
const DEFAULT_MAX_DECISIONS = 500;
|
|
2
|
-
const INPUT_PREVIEW_MAX = 300;
|
|
3
|
-
function getDefaultRules() {
|
|
4
|
-
return [
|
|
5
|
-
// Safe read-only tools
|
|
6
|
-
{
|
|
7
|
-
id: 'allow-read',
|
|
8
|
-
toolPattern: 'Read',
|
|
9
|
-
action: 'allow',
|
|
10
|
-
description: 'Allow reading files',
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
id: 'allow-grep',
|
|
14
|
-
toolPattern: 'Grep',
|
|
15
|
-
action: 'allow',
|
|
16
|
-
description: 'Allow grep searches',
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: 'allow-glob',
|
|
20
|
-
toolPattern: 'Glob',
|
|
21
|
-
action: 'allow',
|
|
22
|
-
description: 'Allow glob file searches',
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: 'allow-ls',
|
|
26
|
-
toolPattern: 'LS',
|
|
27
|
-
action: 'allow',
|
|
28
|
-
description: 'Allow directory listing',
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: 'allow-list',
|
|
32
|
-
toolPattern: 'ListDirectory',
|
|
33
|
-
action: 'allow',
|
|
34
|
-
description: 'Allow listing directories',
|
|
35
|
-
},
|
|
36
|
-
// Edit tools
|
|
37
|
-
{
|
|
38
|
-
id: 'allow-edit',
|
|
39
|
-
toolPattern: 'Edit',
|
|
40
|
-
action: 'allow',
|
|
41
|
-
description: 'Allow file edits',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 'allow-multiedit',
|
|
45
|
-
toolPattern: 'MultiEdit',
|
|
46
|
-
action: 'allow',
|
|
47
|
-
description: 'Allow multi-edits',
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: 'allow-write',
|
|
51
|
-
toolPattern: 'Write',
|
|
52
|
-
action: 'allow',
|
|
53
|
-
description: 'Allow file writes',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 'allow-notebook',
|
|
57
|
-
toolPattern: 'NotebookEdit',
|
|
58
|
-
action: 'allow',
|
|
59
|
-
description: 'Allow notebook edits',
|
|
60
|
-
},
|
|
61
|
-
// Bash - deny dangerous patterns first, then allow the rest
|
|
62
|
-
{
|
|
63
|
-
id: 'deny-bash-rm-rf-root',
|
|
64
|
-
toolPattern: 'Bash',
|
|
65
|
-
inputPattern: 'rm -rf /',
|
|
66
|
-
action: 'deny',
|
|
67
|
-
denyMessage: 'Destructive rm -rf on root path is not allowed.',
|
|
68
|
-
description: 'Block rm -rf on root',
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 'deny-bash-force-push',
|
|
72
|
-
toolPattern: 'Bash',
|
|
73
|
-
inputPattern: 'git push --force',
|
|
74
|
-
action: 'deny',
|
|
75
|
-
denyMessage: 'Force push is not allowed.',
|
|
76
|
-
description: 'Block git force push',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'deny-bash-reset-hard',
|
|
80
|
-
toolPattern: 'Bash',
|
|
81
|
-
inputPattern: 'git reset --hard',
|
|
82
|
-
action: 'deny',
|
|
83
|
-
denyMessage: 'git reset --hard is not allowed from Claude Code. Use the manager git_reset tool instead.',
|
|
84
|
-
description: 'Block git reset --hard',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: 'allow-bash',
|
|
88
|
-
toolPattern: 'Bash',
|
|
89
|
-
action: 'allow',
|
|
90
|
-
description: 'Allow bash commands (after dangerous patterns filtered)',
|
|
91
|
-
},
|
|
92
|
-
// Agent / misc
|
|
93
|
-
{
|
|
94
|
-
id: 'allow-skill',
|
|
95
|
-
toolPattern: 'Skill',
|
|
96
|
-
action: 'allow',
|
|
97
|
-
description: 'Allow Agent Skills (filesystem SKILL.md)',
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: 'allow-agent',
|
|
101
|
-
toolPattern: 'Agent',
|
|
102
|
-
action: 'allow',
|
|
103
|
-
description: 'Allow agent delegation',
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: 'allow-todowrite',
|
|
107
|
-
toolPattern: 'TodoWrite',
|
|
108
|
-
action: 'allow',
|
|
109
|
-
description: 'Allow todo tracking',
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
id: 'allow-todoread',
|
|
113
|
-
toolPattern: 'TodoRead',
|
|
114
|
-
action: 'allow',
|
|
115
|
-
description: 'Allow todo reading',
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
}
|
|
119
|
-
export class ToolApprovalManager {
|
|
120
|
-
policy;
|
|
121
|
-
decisions = [];
|
|
122
|
-
maxDecisions;
|
|
123
|
-
constructor(policy, maxDecisions) {
|
|
124
|
-
this.policy = {
|
|
125
|
-
rules: policy?.rules ?? getDefaultRules(),
|
|
126
|
-
defaultAction: policy?.defaultAction ?? 'allow',
|
|
127
|
-
defaultDenyMessage: policy?.defaultDenyMessage ?? 'Tool call denied by approval policy.',
|
|
128
|
-
enabled: policy?.enabled ?? true,
|
|
129
|
-
};
|
|
130
|
-
this.maxDecisions = maxDecisions ?? DEFAULT_MAX_DECISIONS;
|
|
131
|
-
}
|
|
132
|
-
evaluate(toolName, input, options) {
|
|
133
|
-
if (!this.policy.enabled) {
|
|
134
|
-
return { behavior: 'allow' };
|
|
135
|
-
}
|
|
136
|
-
const inputJson = safeJsonStringify(input);
|
|
137
|
-
const matchedRule = this.findMatchingRule(toolName, inputJson);
|
|
138
|
-
const action = matchedRule?.action ?? this.policy.defaultAction;
|
|
139
|
-
const denyMessage = action === 'deny'
|
|
140
|
-
? (matchedRule?.denyMessage ??
|
|
141
|
-
this.policy.defaultDenyMessage ??
|
|
142
|
-
'Denied by policy.')
|
|
143
|
-
: undefined;
|
|
144
|
-
this.recordDecision({
|
|
145
|
-
timestamp: new Date().toISOString(),
|
|
146
|
-
toolName,
|
|
147
|
-
inputPreview: inputJson.slice(0, INPUT_PREVIEW_MAX),
|
|
148
|
-
title: options?.title,
|
|
149
|
-
matchedRuleId: matchedRule?.id ?? 'default',
|
|
150
|
-
action,
|
|
151
|
-
denyMessage,
|
|
152
|
-
agentId: options?.agentID,
|
|
153
|
-
});
|
|
154
|
-
if (action === 'deny') {
|
|
155
|
-
return { behavior: 'deny', message: denyMessage };
|
|
156
|
-
}
|
|
157
|
-
return { behavior: 'allow' };
|
|
158
|
-
}
|
|
159
|
-
getDecisions(limit) {
|
|
160
|
-
const all = [...this.decisions].reverse();
|
|
161
|
-
return limit ? all.slice(0, limit) : all;
|
|
162
|
-
}
|
|
163
|
-
getDeniedDecisions(limit) {
|
|
164
|
-
const denied = this.decisions.filter((d) => d.action === 'deny').reverse();
|
|
165
|
-
return limit ? denied.slice(0, limit) : denied;
|
|
166
|
-
}
|
|
167
|
-
clearDecisions() {
|
|
168
|
-
this.decisions = [];
|
|
169
|
-
}
|
|
170
|
-
getPolicy() {
|
|
171
|
-
return { ...this.policy, rules: [...this.policy.rules] };
|
|
172
|
-
}
|
|
173
|
-
setPolicy(policy) {
|
|
174
|
-
this.policy = { ...policy, rules: [...policy.rules] };
|
|
175
|
-
}
|
|
176
|
-
addRule(rule, position) {
|
|
177
|
-
if (position !== undefined &&
|
|
178
|
-
position >= 0 &&
|
|
179
|
-
position < this.policy.rules.length) {
|
|
180
|
-
this.policy.rules.splice(position, 0, rule);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
this.policy.rules.push(rule);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
removeRule(ruleId) {
|
|
187
|
-
const index = this.policy.rules.findIndex((r) => r.id === ruleId);
|
|
188
|
-
if (index === -1) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
this.policy.rules.splice(index, 1);
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
setDefaultAction(action) {
|
|
195
|
-
this.policy.defaultAction = action;
|
|
196
|
-
}
|
|
197
|
-
setEnabled(enabled) {
|
|
198
|
-
this.policy.enabled = enabled;
|
|
199
|
-
}
|
|
200
|
-
findMatchingRule(toolName, inputJson) {
|
|
201
|
-
for (const rule of this.policy.rules) {
|
|
202
|
-
if (!matchesToolPattern(rule.toolPattern, toolName)) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (rule.inputPattern && !inputJson.includes(rule.inputPattern)) {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
return rule;
|
|
209
|
-
}
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
recordDecision(decision) {
|
|
213
|
-
this.decisions.push(decision);
|
|
214
|
-
if (this.decisions.length > this.maxDecisions) {
|
|
215
|
-
this.decisions = this.decisions.slice(-this.maxDecisions);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
function matchesToolPattern(pattern, toolName) {
|
|
220
|
-
if (pattern === '*') {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
if (!pattern.includes('*')) {
|
|
224
|
-
return pattern === toolName;
|
|
225
|
-
}
|
|
226
|
-
const regex = new RegExp('^' +
|
|
227
|
-
pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') +
|
|
228
|
-
'$');
|
|
229
|
-
return regex.test(toolName);
|
|
230
|
-
}
|
|
231
|
-
function safeJsonStringify(value) {
|
|
232
|
-
try {
|
|
233
|
-
return JSON.stringify(value);
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
return String(value);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { ContextWarningLevel, SessionContextSnapshot } from '../types/contracts.js';
|
|
2
|
-
export declare class ContextTracker {
|
|
3
|
-
private totalTurns;
|
|
4
|
-
private totalCostUsd;
|
|
5
|
-
private latestInputTokens;
|
|
6
|
-
private latestOutputTokens;
|
|
7
|
-
private contextWindowSize;
|
|
8
|
-
private compactionCount;
|
|
9
|
-
private sessionId;
|
|
10
|
-
recordResult(result: {
|
|
11
|
-
sessionId?: string;
|
|
12
|
-
turns?: number;
|
|
13
|
-
totalCostUsd?: number;
|
|
14
|
-
inputTokens?: number;
|
|
15
|
-
outputTokens?: number;
|
|
16
|
-
contextWindowSize?: number;
|
|
17
|
-
}): void;
|
|
18
|
-
recordCompaction(): void;
|
|
19
|
-
snapshot(): SessionContextSnapshot;
|
|
20
|
-
warningLevel(): ContextWarningLevel;
|
|
21
|
-
estimateContextPercent(): number | null;
|
|
22
|
-
isAboveTokenThreshold(thresholdTokens?: number): boolean;
|
|
23
|
-
reset(): void;
|
|
24
|
-
/** Restore from persisted active session state. */
|
|
25
|
-
restore(state: {
|
|
26
|
-
sessionId: string;
|
|
27
|
-
totalTurns: number;
|
|
28
|
-
totalCostUsd: number;
|
|
29
|
-
estimatedContextPercent: number | null;
|
|
30
|
-
contextWindowSize: number | null;
|
|
31
|
-
latestInputTokens: number | null;
|
|
32
|
-
}): void;
|
|
33
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
2
|
-
export class ContextTracker {
|
|
3
|
-
totalTurns = 0;
|
|
4
|
-
totalCostUsd = 0;
|
|
5
|
-
latestInputTokens = null;
|
|
6
|
-
latestOutputTokens = null;
|
|
7
|
-
contextWindowSize = null;
|
|
8
|
-
compactionCount = 0;
|
|
9
|
-
sessionId = null;
|
|
10
|
-
recordResult(result) {
|
|
11
|
-
if (result.sessionId) {
|
|
12
|
-
this.sessionId = result.sessionId;
|
|
13
|
-
}
|
|
14
|
-
if (result.turns !== undefined) {
|
|
15
|
-
this.totalTurns = result.turns;
|
|
16
|
-
}
|
|
17
|
-
if (result.totalCostUsd !== undefined) {
|
|
18
|
-
this.totalCostUsd = result.totalCostUsd;
|
|
19
|
-
}
|
|
20
|
-
if (result.inputTokens !== undefined) {
|
|
21
|
-
// If input tokens dropped significantly, compaction likely occurred
|
|
22
|
-
if (this.latestInputTokens !== null &&
|
|
23
|
-
result.inputTokens < this.latestInputTokens * 0.5) {
|
|
24
|
-
this.compactionCount++;
|
|
25
|
-
}
|
|
26
|
-
this.latestInputTokens = result.inputTokens;
|
|
27
|
-
}
|
|
28
|
-
if (result.outputTokens !== undefined) {
|
|
29
|
-
this.latestOutputTokens = result.outputTokens;
|
|
30
|
-
}
|
|
31
|
-
if (result.contextWindowSize !== undefined) {
|
|
32
|
-
this.contextWindowSize = result.contextWindowSize;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
recordCompaction() {
|
|
36
|
-
this.compactionCount++;
|
|
37
|
-
}
|
|
38
|
-
snapshot() {
|
|
39
|
-
return {
|
|
40
|
-
sessionId: this.sessionId,
|
|
41
|
-
totalTurns: this.totalTurns,
|
|
42
|
-
totalCostUsd: this.totalCostUsd,
|
|
43
|
-
latestInputTokens: this.latestInputTokens,
|
|
44
|
-
latestOutputTokens: this.latestOutputTokens,
|
|
45
|
-
contextWindowSize: this.contextWindowSize,
|
|
46
|
-
estimatedContextPercent: this.estimateContextPercent(),
|
|
47
|
-
warningLevel: this.warningLevel(),
|
|
48
|
-
compactionCount: this.compactionCount,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
warningLevel() {
|
|
52
|
-
const percent = this.estimateContextPercent();
|
|
53
|
-
if (percent === null) {
|
|
54
|
-
return 'ok';
|
|
55
|
-
}
|
|
56
|
-
if (percent >= 85) {
|
|
57
|
-
return 'critical';
|
|
58
|
-
}
|
|
59
|
-
if (percent >= 70) {
|
|
60
|
-
return 'high';
|
|
61
|
-
}
|
|
62
|
-
if (percent >= 50) {
|
|
63
|
-
return 'moderate';
|
|
64
|
-
}
|
|
65
|
-
return 'ok';
|
|
66
|
-
}
|
|
67
|
-
estimateContextPercent() {
|
|
68
|
-
// Tier 1: Token-based (most accurate)
|
|
69
|
-
if (this.latestInputTokens !== null) {
|
|
70
|
-
const window = this.contextWindowSize ?? DEFAULT_CONTEXT_WINDOW;
|
|
71
|
-
return Math.min(100, Math.round((this.latestInputTokens / window) * 100));
|
|
72
|
-
}
|
|
73
|
-
// Tier 2: Cost-based heuristic
|
|
74
|
-
if (this.totalCostUsd > 0) {
|
|
75
|
-
const estimatedTokens = this.totalCostUsd * 130_000;
|
|
76
|
-
const window = this.contextWindowSize ?? DEFAULT_CONTEXT_WINDOW;
|
|
77
|
-
return Math.min(100, Math.round((estimatedTokens / window) * 100));
|
|
78
|
-
}
|
|
79
|
-
// Tier 3: Turns-based fallback
|
|
80
|
-
if (this.totalTurns > 0) {
|
|
81
|
-
const estimatedTokens = this.totalTurns * 6_000;
|
|
82
|
-
const window = this.contextWindowSize ?? DEFAULT_CONTEXT_WINDOW;
|
|
83
|
-
return Math.min(100, Math.round((estimatedTokens / window) * 100));
|
|
84
|
-
}
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
isAboveTokenThreshold(thresholdTokens = 200_000) {
|
|
88
|
-
return (this.latestInputTokens !== null &&
|
|
89
|
-
this.latestInputTokens >= thresholdTokens);
|
|
90
|
-
}
|
|
91
|
-
reset() {
|
|
92
|
-
this.totalTurns = 0;
|
|
93
|
-
this.totalCostUsd = 0;
|
|
94
|
-
this.latestInputTokens = null;
|
|
95
|
-
this.latestOutputTokens = null;
|
|
96
|
-
this.contextWindowSize = null;
|
|
97
|
-
this.compactionCount = 0;
|
|
98
|
-
this.sessionId = null;
|
|
99
|
-
}
|
|
100
|
-
/** Restore from persisted active session state. */
|
|
101
|
-
restore(state) {
|
|
102
|
-
this.sessionId = state.sessionId;
|
|
103
|
-
this.totalTurns = state.totalTurns;
|
|
104
|
-
this.totalCostUsd = state.totalCostUsd;
|
|
105
|
-
this.contextWindowSize = state.contextWindowSize;
|
|
106
|
-
this.latestInputTokens = state.latestInputTokens;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { GitDiffResult, GitOperationResult } from '../types/contracts.js';
|
|
2
|
-
export declare class GitOperations {
|
|
3
|
-
private readonly cwd;
|
|
4
|
-
constructor(cwd: string);
|
|
5
|
-
diff(): Promise<GitDiffResult>;
|
|
6
|
-
diffStat(): Promise<string>;
|
|
7
|
-
commit(message: string): Promise<GitOperationResult>;
|
|
8
|
-
resetHard(): Promise<GitOperationResult>;
|
|
9
|
-
currentBranch(): Promise<string>;
|
|
10
|
-
recentCommits(count?: number): Promise<string>;
|
|
11
|
-
private git;
|
|
12
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
|
-
const execFileAsync = promisify(execFile);
|
|
4
|
-
export class GitOperations {
|
|
5
|
-
cwd;
|
|
6
|
-
constructor(cwd) {
|
|
7
|
-
this.cwd = cwd;
|
|
8
|
-
}
|
|
9
|
-
async diff() {
|
|
10
|
-
const [diffText, statOutput] = await Promise.all([
|
|
11
|
-
this.git(['diff', 'HEAD']),
|
|
12
|
-
this.git(['diff', 'HEAD', '--stat']),
|
|
13
|
-
]);
|
|
14
|
-
const stats = parseStatLine(statOutput);
|
|
15
|
-
return {
|
|
16
|
-
hasDiff: diffText.trim().length > 0,
|
|
17
|
-
diffText,
|
|
18
|
-
stats,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
async diffStat() {
|
|
22
|
-
return this.git(['diff', 'HEAD', '--stat']);
|
|
23
|
-
}
|
|
24
|
-
async commit(message) {
|
|
25
|
-
try {
|
|
26
|
-
await this.git(['add', '-A']);
|
|
27
|
-
const output = await this.git(['commit', '-m', message]);
|
|
28
|
-
return { success: true, output };
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
return {
|
|
32
|
-
success: false,
|
|
33
|
-
output: '',
|
|
34
|
-
error: error instanceof Error ? error.message : String(error),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async resetHard() {
|
|
39
|
-
try {
|
|
40
|
-
const output = await this.git(['reset', '--hard', 'HEAD']);
|
|
41
|
-
// Also clean untracked files
|
|
42
|
-
const cleanOutput = await this.git(['clean', '-fd']);
|
|
43
|
-
return { success: true, output: `${output}\n${cleanOutput}`.trim() };
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
return {
|
|
47
|
-
success: false,
|
|
48
|
-
output: '',
|
|
49
|
-
error: error instanceof Error ? error.message : String(error),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async currentBranch() {
|
|
54
|
-
const branch = await this.git(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
55
|
-
return branch.trim();
|
|
56
|
-
}
|
|
57
|
-
async recentCommits(count = 5) {
|
|
58
|
-
return this.git(['log', `--oneline`, `-${count}`]);
|
|
59
|
-
}
|
|
60
|
-
async git(args) {
|
|
61
|
-
const { stdout } = await execFileAsync('git', args, { cwd: this.cwd });
|
|
62
|
-
return stdout;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function parseStatLine(statOutput) {
|
|
66
|
-
// Parse the summary line like: "3 files changed, 10 insertions(+), 2 deletions(-)"
|
|
67
|
-
const summaryLine = statOutput.trim().split('\n').pop() ?? '';
|
|
68
|
-
const filesMatch = summaryLine.match(/(\d+)\s+files?\s+changed/);
|
|
69
|
-
const insertionsMatch = summaryLine.match(/(\d+)\s+insertions?\(\+\)/);
|
|
70
|
-
const deletionsMatch = summaryLine.match(/(\d+)\s+deletions?\(-\)/);
|
|
71
|
-
return {
|
|
72
|
-
filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
|
73
|
-
insertions: insertionsMatch ? parseInt(insertionsMatch[1], 10) : 0,
|
|
74
|
-
deletions: deletionsMatch ? parseInt(deletionsMatch[1], 10) : 0,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
2
|
-
import type { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
3
|
-
import type { ManagerRunRecord, ManagerRunResult, ManagerTaskRequest } from '../types/contracts.js';
|
|
4
|
-
import type { TaskPlanner } from './task-planner.js';
|
|
5
|
-
export type ManagerRunProgressHandler = (run: ManagerRunRecord) => void | Promise<void>;
|
|
6
|
-
export declare class ManagerOrchestrator {
|
|
7
|
-
private readonly sessionService;
|
|
8
|
-
private readonly stateStore;
|
|
9
|
-
private readonly taskPlanner;
|
|
10
|
-
constructor(sessionService: ClaudeSessionService, stateStore: FileRunStateStore, taskPlanner: TaskPlanner);
|
|
11
|
-
run(request: ManagerTaskRequest, onProgress?: ManagerRunProgressHandler): Promise<ManagerRunResult>;
|
|
12
|
-
listRuns(cwd: string): Promise<ManagerRunRecord[]>;
|
|
13
|
-
getRun(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
|
|
14
|
-
private executePlan;
|
|
15
|
-
private patchSession;
|
|
16
|
-
private updateRunAndNotify;
|
|
17
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
3
|
-
import { appendTranscriptEvents, stripTrailingPartials, } from '../util/transcript-append.js';
|
|
4
|
-
export class ManagerOrchestrator {
|
|
5
|
-
sessionService;
|
|
6
|
-
stateStore;
|
|
7
|
-
taskPlanner;
|
|
8
|
-
constructor(sessionService, stateStore, taskPlanner) {
|
|
9
|
-
this.sessionService = sessionService;
|
|
10
|
-
this.stateStore = stateStore;
|
|
11
|
-
this.taskPlanner = taskPlanner;
|
|
12
|
-
}
|
|
13
|
-
async run(request, onProgress) {
|
|
14
|
-
const maxSubagents = Math.max(1, request.maxSubagents ?? 3);
|
|
15
|
-
const includeProjectSettings = request.includeProjectSettings ?? true;
|
|
16
|
-
const metadata = await this.sessionService.inspectRepository(request.cwd);
|
|
17
|
-
const plans = this.taskPlanner.plan(request.task, maxSubagents);
|
|
18
|
-
const runId = randomUUID();
|
|
19
|
-
const createdAt = new Date().toISOString();
|
|
20
|
-
const plannedAssignments = plans.map((plan) => ({
|
|
21
|
-
plan,
|
|
22
|
-
cwd: request.cwd,
|
|
23
|
-
}));
|
|
24
|
-
const runRecord = {
|
|
25
|
-
id: runId,
|
|
26
|
-
cwd: request.cwd,
|
|
27
|
-
task: request.task,
|
|
28
|
-
includeProjectSettings,
|
|
29
|
-
status: 'running',
|
|
30
|
-
createdAt,
|
|
31
|
-
updatedAt: createdAt,
|
|
32
|
-
metadata,
|
|
33
|
-
sessions: plannedAssignments.map(({ plan, cwd }) => createManagedSessionRecord(plan, cwd)),
|
|
34
|
-
};
|
|
35
|
-
await this.stateStore.saveRun(runRecord);
|
|
36
|
-
await onProgress?.(runRecord);
|
|
37
|
-
const settledResults = plannedAssignments.length <= 1
|
|
38
|
-
? await Promise.allSettled(plannedAssignments.map(({ plan, cwd }) => this.executePlan({
|
|
39
|
-
request,
|
|
40
|
-
runId,
|
|
41
|
-
plan,
|
|
42
|
-
cwd,
|
|
43
|
-
includeProjectSettings,
|
|
44
|
-
onProgress,
|
|
45
|
-
})))
|
|
46
|
-
: await runSequentially(plannedAssignments.map(({ plan, cwd }) => () => this.executePlan({
|
|
47
|
-
request,
|
|
48
|
-
runId,
|
|
49
|
-
plan,
|
|
50
|
-
cwd,
|
|
51
|
-
includeProjectSettings,
|
|
52
|
-
onProgress,
|
|
53
|
-
})));
|
|
54
|
-
const failedResult = settledResults.find((result) => result.status === 'rejected');
|
|
55
|
-
const finalRun = await this.updateRunAndNotify(request.cwd, runId, (currentRun) => ({
|
|
56
|
-
...currentRun,
|
|
57
|
-
status: failedResult ? 'failed' : 'completed',
|
|
58
|
-
updatedAt: new Date().toISOString(),
|
|
59
|
-
finalSummary: summarizeRun(currentRun.sessions),
|
|
60
|
-
}), onProgress);
|
|
61
|
-
return { run: finalRun };
|
|
62
|
-
}
|
|
63
|
-
listRuns(cwd) {
|
|
64
|
-
return this.stateStore.listRuns(cwd);
|
|
65
|
-
}
|
|
66
|
-
getRun(cwd, runId) {
|
|
67
|
-
return this.stateStore.getRun(cwd, runId);
|
|
68
|
-
}
|
|
69
|
-
async executePlan(input) {
|
|
70
|
-
const { request, runId, plan, cwd, includeProjectSettings, onProgress } = input;
|
|
71
|
-
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
72
|
-
...session,
|
|
73
|
-
status: 'running',
|
|
74
|
-
}), onProgress);
|
|
75
|
-
try {
|
|
76
|
-
const sessionInput = {
|
|
77
|
-
cwd,
|
|
78
|
-
prompt: buildWorkerPrompt(plan.prompt, request.task),
|
|
79
|
-
systemPrompt: managerPromptRegistry.subagentSystemPrompt,
|
|
80
|
-
model: request.model,
|
|
81
|
-
includePartialMessages: true,
|
|
82
|
-
settingSources: includeProjectSettings ? ['project', 'local'] : [],
|
|
83
|
-
};
|
|
84
|
-
const sessionResult = await this.sessionService.runTask(sessionInput, async (event) => {
|
|
85
|
-
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
86
|
-
...session,
|
|
87
|
-
claudeSessionId: event.sessionId ?? session.claudeSessionId,
|
|
88
|
-
events: appendTranscriptEvents(session.events, [
|
|
89
|
-
compactEvent(event),
|
|
90
|
-
]),
|
|
91
|
-
}), onProgress);
|
|
92
|
-
});
|
|
93
|
-
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
94
|
-
...session,
|
|
95
|
-
status: 'completed',
|
|
96
|
-
claudeSessionId: sessionResult.sessionId,
|
|
97
|
-
finalText: sessionResult.finalText,
|
|
98
|
-
turns: sessionResult.turns,
|
|
99
|
-
totalCostUsd: sessionResult.totalCostUsd,
|
|
100
|
-
events: stripTrailingPartials(session.events),
|
|
101
|
-
}), onProgress);
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
105
|
-
...session,
|
|
106
|
-
status: 'failed',
|
|
107
|
-
error: error instanceof Error ? error.message : String(error),
|
|
108
|
-
events: stripTrailingPartials(session.events),
|
|
109
|
-
}), onProgress);
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
async patchSession(cwd, runId, sessionId, update, onProgress) {
|
|
114
|
-
await this.updateRunAndNotify(cwd, runId, (run) => ({
|
|
115
|
-
...run,
|
|
116
|
-
updatedAt: new Date().toISOString(),
|
|
117
|
-
sessions: run.sessions.map((session) => session.id === sessionId ? update(session) : session),
|
|
118
|
-
}), onProgress);
|
|
119
|
-
}
|
|
120
|
-
async updateRunAndNotify(cwd, runId, update, onProgress) {
|
|
121
|
-
const updatedRun = await this.stateStore.updateRun(cwd, runId, update);
|
|
122
|
-
await onProgress?.(updatedRun);
|
|
123
|
-
return updatedRun;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function createManagedSessionRecord(plan, cwd) {
|
|
127
|
-
return {
|
|
128
|
-
id: plan.id,
|
|
129
|
-
title: plan.title,
|
|
130
|
-
prompt: plan.prompt,
|
|
131
|
-
status: 'pending',
|
|
132
|
-
cwd,
|
|
133
|
-
events: [],
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
function buildWorkerPrompt(subtaskPrompt, parentTasks) {
|
|
137
|
-
const parentContext = parentTasks.length === 1
|
|
138
|
-
? `Parent task: ${parentTasks[0]}`
|
|
139
|
-
: `Parent tasks:\n${parentTasks.map((t, i) => `${i + 1}. ${t}`).join('\n')}`;
|
|
140
|
-
return [
|
|
141
|
-
'You are executing a delegated Claude Code subtask.',
|
|
142
|
-
parentContext,
|
|
143
|
-
`Assigned subtask: ${subtaskPrompt}`,
|
|
144
|
-
'Stay within scope, finish the requested work, and end with a concise verification summary.',
|
|
145
|
-
].join('\n\n');
|
|
146
|
-
}
|
|
147
|
-
function summarizeRun(sessions) {
|
|
148
|
-
return sessions
|
|
149
|
-
.map((session) => `${session.title}: ${session.finalText ?? session.error ?? session.status}`)
|
|
150
|
-
.join('\n');
|
|
151
|
-
}
|
|
152
|
-
function compactEvent(event) {
|
|
153
|
-
const compact = {
|
|
154
|
-
type: event.type,
|
|
155
|
-
sessionId: event.sessionId,
|
|
156
|
-
text: event.text.slice(0, 4000),
|
|
157
|
-
};
|
|
158
|
-
if (event.turns !== undefined) {
|
|
159
|
-
compact.turns = event.turns;
|
|
160
|
-
}
|
|
161
|
-
if (event.totalCostUsd !== undefined) {
|
|
162
|
-
compact.totalCostUsd = event.totalCostUsd;
|
|
163
|
-
}
|
|
164
|
-
return compact;
|
|
165
|
-
}
|
|
166
|
-
async function runSequentially(tasks) {
|
|
167
|
-
const results = [];
|
|
168
|
-
for (const task of tasks) {
|
|
169
|
-
try {
|
|
170
|
-
await task();
|
|
171
|
-
results.push({ status: 'fulfilled', value: undefined });
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
results.push({ status: 'rejected', reason: error });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return results;
|
|
178
|
-
}
|