@doingdev/opencode-claude-manager-plugin 0.1.9 → 0.1.11
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 +26 -29
- package/dist/claude/claude-agent-sdk-adapter.js +249 -62
- package/dist/claude/delegated-can-use-tool.d.ts +7 -0
- package/dist/claude/delegated-can-use-tool.js +178 -0
- package/dist/index.d.ts +1 -1
- package/dist/manager/context-tracker.d.ts +33 -0
- package/dist/manager/context-tracker.js +108 -0
- package/dist/manager/git-operations.d.ts +12 -0
- package/dist/manager/git-operations.js +76 -0
- package/dist/manager/manager-orchestrator.d.ts +1 -4
- package/dist/manager/manager-orchestrator.js +37 -53
- package/dist/manager/persistent-manager.d.ts +64 -0
- package/dist/manager/persistent-manager.js +152 -0
- package/dist/manager/session-controller.d.ts +38 -0
- package/dist/manager/session-controller.js +135 -0
- package/dist/manager/task-planner.d.ts +2 -2
- package/dist/manager/task-planner.js +4 -31
- package/dist/plugin/claude-code-permission-bridge.d.ts +15 -0
- package/dist/plugin/claude-code-permission-bridge.js +184 -0
- package/dist/plugin/claude-manager.plugin.d.ts +2 -2
- package/dist/plugin/claude-manager.plugin.js +150 -192
- package/dist/plugin/service-factory.d.ts +2 -2
- package/dist/plugin/service-factory.js +12 -4
- package/dist/prompts/registry.js +42 -8
- package/dist/state/file-run-state-store.d.ts +5 -5
- package/dist/types/contracts.d.ts +68 -45
- package/dist/util/transcript-append.d.ts +7 -0
- package/dist/util/transcript-append.js +29 -0
- package/package.json +10 -10
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from '@opencode-ai/plugin';
|
|
2
2
|
import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
|
|
3
|
-
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage,
|
|
3
|
+
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, } from './types/contracts.js';
|
|
4
4
|
export { ClaudeManagerPlugin };
|
|
5
5
|
export declare const plugin: Plugin;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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,19 +1,16 @@
|
|
|
1
1
|
import type { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
2
2
|
import type { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
3
3
|
import type { ManagerRunRecord, ManagerRunResult, ManagerTaskRequest } from '../types/contracts.js';
|
|
4
|
-
import type { WorktreeCoordinator } from '../worktree/worktree-coordinator.js';
|
|
5
4
|
import type { TaskPlanner } from './task-planner.js';
|
|
6
5
|
export type ManagerRunProgressHandler = (run: ManagerRunRecord) => void | Promise<void>;
|
|
7
6
|
export declare class ManagerOrchestrator {
|
|
8
7
|
private readonly sessionService;
|
|
9
8
|
private readonly stateStore;
|
|
10
|
-
private readonly worktreeCoordinator;
|
|
11
9
|
private readonly taskPlanner;
|
|
12
|
-
constructor(sessionService: ClaudeSessionService, stateStore: FileRunStateStore,
|
|
10
|
+
constructor(sessionService: ClaudeSessionService, stateStore: FileRunStateStore, taskPlanner: TaskPlanner);
|
|
13
11
|
run(request: ManagerTaskRequest, onProgress?: ManagerRunProgressHandler): Promise<ManagerRunResult>;
|
|
14
12
|
listRuns(cwd: string): Promise<ManagerRunRecord[]>;
|
|
15
13
|
getRun(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
|
|
16
|
-
cleanupRunWorktrees(cwd: string, runId: string): Promise<ManagerRunRecord | null>;
|
|
17
14
|
private executePlan;
|
|
18
15
|
private patchSession;
|
|
19
16
|
private updateRunAndNotify;
|
|
@@ -1,65 +1,53 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { managerPromptRegistry } from '../prompts/registry.js';
|
|
3
|
+
import { appendTranscriptEvents, stripTrailingPartials, } from '../util/transcript-append.js';
|
|
3
4
|
export class ManagerOrchestrator {
|
|
4
5
|
sessionService;
|
|
5
6
|
stateStore;
|
|
6
|
-
worktreeCoordinator;
|
|
7
7
|
taskPlanner;
|
|
8
|
-
constructor(sessionService, stateStore,
|
|
8
|
+
constructor(sessionService, stateStore, taskPlanner) {
|
|
9
9
|
this.sessionService = sessionService;
|
|
10
10
|
this.stateStore = stateStore;
|
|
11
|
-
this.worktreeCoordinator = worktreeCoordinator;
|
|
12
11
|
this.taskPlanner = taskPlanner;
|
|
13
12
|
}
|
|
14
13
|
async run(request, onProgress) {
|
|
15
|
-
const mode = request.mode ?? 'auto';
|
|
16
14
|
const maxSubagents = Math.max(1, request.maxSubagents ?? 3);
|
|
17
|
-
const useWorktrees = request.useWorktrees ?? maxSubagents > 1;
|
|
18
15
|
const includeProjectSettings = request.includeProjectSettings ?? true;
|
|
19
16
|
const metadata = await this.sessionService.inspectRepository(request.cwd);
|
|
20
|
-
const plans = this.taskPlanner.plan(request.task,
|
|
17
|
+
const plans = this.taskPlanner.plan(request.task, maxSubagents);
|
|
21
18
|
const runId = randomUUID();
|
|
22
19
|
const createdAt = new Date().toISOString();
|
|
23
|
-
const
|
|
24
|
-
cwd: request.cwd,
|
|
25
|
-
runId,
|
|
26
|
-
title: plan.title,
|
|
27
|
-
useWorktree: useWorktrees && plans.length > 1,
|
|
28
|
-
})));
|
|
29
|
-
const plannedAssignments = plans.map((plan, index) => ({
|
|
20
|
+
const plannedAssignments = plans.map((plan) => ({
|
|
30
21
|
plan,
|
|
31
|
-
|
|
22
|
+
cwd: request.cwd,
|
|
32
23
|
}));
|
|
33
24
|
const runRecord = {
|
|
34
25
|
id: runId,
|
|
35
26
|
cwd: request.cwd,
|
|
36
27
|
task: request.task,
|
|
37
|
-
mode,
|
|
38
|
-
useWorktrees,
|
|
39
28
|
includeProjectSettings,
|
|
40
29
|
status: 'running',
|
|
41
30
|
createdAt,
|
|
42
31
|
updatedAt: createdAt,
|
|
43
32
|
metadata,
|
|
44
|
-
sessions: plannedAssignments.map(({ plan,
|
|
33
|
+
sessions: plannedAssignments.map(({ plan, cwd }) => createManagedSessionRecord(plan, cwd)),
|
|
45
34
|
};
|
|
46
35
|
await this.stateStore.saveRun(runRecord);
|
|
47
36
|
await onProgress?.(runRecord);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
? await Promise.allSettled(plannedAssignments.map(({ plan, assignment }) => this.executePlan({
|
|
37
|
+
const settledResults = plannedAssignments.length <= 1
|
|
38
|
+
? await Promise.allSettled(plannedAssignments.map(({ plan, cwd }) => this.executePlan({
|
|
51
39
|
request,
|
|
52
40
|
runId,
|
|
53
41
|
plan,
|
|
54
|
-
|
|
42
|
+
cwd,
|
|
55
43
|
includeProjectSettings,
|
|
56
44
|
onProgress,
|
|
57
45
|
})))
|
|
58
|
-
: await runSequentially(plannedAssignments.map(({ plan,
|
|
46
|
+
: await runSequentially(plannedAssignments.map(({ plan, cwd }) => () => this.executePlan({
|
|
59
47
|
request,
|
|
60
48
|
runId,
|
|
61
49
|
plan,
|
|
62
|
-
|
|
50
|
+
cwd,
|
|
63
51
|
includeProjectSettings,
|
|
64
52
|
onProgress,
|
|
65
53
|
})));
|
|
@@ -78,43 +66,28 @@ export class ManagerOrchestrator {
|
|
|
78
66
|
getRun(cwd, runId) {
|
|
79
67
|
return this.stateStore.getRun(cwd, runId);
|
|
80
68
|
}
|
|
81
|
-
async cleanupRunWorktrees(cwd, runId) {
|
|
82
|
-
const run = await this.stateStore.getRun(cwd, runId);
|
|
83
|
-
if (!run) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
for (const session of run.sessions) {
|
|
87
|
-
if (session.worktreeMode !== 'git-worktree') {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
await this.worktreeCoordinator.cleanupAssignment({
|
|
91
|
-
mode: 'git-worktree',
|
|
92
|
-
cwd: session.cwd,
|
|
93
|
-
rootCwd: run.cwd,
|
|
94
|
-
branchName: session.branchName,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return run;
|
|
98
|
-
}
|
|
99
69
|
async executePlan(input) {
|
|
100
|
-
const { request, runId, plan,
|
|
70
|
+
const { request, runId, plan, cwd, includeProjectSettings, onProgress } = input;
|
|
101
71
|
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
102
72
|
...session,
|
|
103
73
|
status: 'running',
|
|
104
74
|
}), onProgress);
|
|
105
75
|
try {
|
|
106
|
-
const
|
|
107
|
-
cwd
|
|
76
|
+
const sessionInput = {
|
|
77
|
+
cwd,
|
|
108
78
|
prompt: buildWorkerPrompt(plan.prompt, request.task),
|
|
109
79
|
systemPrompt: managerPromptRegistry.subagentSystemPrompt,
|
|
110
80
|
model: request.model,
|
|
111
81
|
includePartialMessages: true,
|
|
112
82
|
settingSources: includeProjectSettings ? ['project', 'local'] : [],
|
|
113
|
-
}
|
|
83
|
+
};
|
|
84
|
+
const sessionResult = await this.sessionService.runTask(sessionInput, async (event) => {
|
|
114
85
|
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
115
86
|
...session,
|
|
116
87
|
claudeSessionId: event.sessionId ?? session.claudeSessionId,
|
|
117
|
-
events:
|
|
88
|
+
events: appendTranscriptEvents(session.events, [
|
|
89
|
+
compactEvent(event),
|
|
90
|
+
]),
|
|
118
91
|
}), onProgress);
|
|
119
92
|
});
|
|
120
93
|
await this.patchSession(request.cwd, runId, plan.id, (session) => ({
|
|
@@ -124,6 +97,7 @@ export class ManagerOrchestrator {
|
|
|
124
97
|
finalText: sessionResult.finalText,
|
|
125
98
|
turns: sessionResult.turns,
|
|
126
99
|
totalCostUsd: sessionResult.totalCostUsd,
|
|
100
|
+
events: stripTrailingPartials(session.events),
|
|
127
101
|
}), onProgress);
|
|
128
102
|
}
|
|
129
103
|
catch (error) {
|
|
@@ -131,6 +105,7 @@ export class ManagerOrchestrator {
|
|
|
131
105
|
...session,
|
|
132
106
|
status: 'failed',
|
|
133
107
|
error: error instanceof Error ? error.message : String(error),
|
|
108
|
+
events: stripTrailingPartials(session.events),
|
|
134
109
|
}), onProgress);
|
|
135
110
|
throw error;
|
|
136
111
|
}
|
|
@@ -148,22 +123,23 @@ export class ManagerOrchestrator {
|
|
|
148
123
|
return updatedRun;
|
|
149
124
|
}
|
|
150
125
|
}
|
|
151
|
-
function createManagedSessionRecord(plan, cwd
|
|
126
|
+
function createManagedSessionRecord(plan, cwd) {
|
|
152
127
|
return {
|
|
153
128
|
id: plan.id,
|
|
154
129
|
title: plan.title,
|
|
155
130
|
prompt: plan.prompt,
|
|
156
131
|
status: 'pending',
|
|
157
132
|
cwd,
|
|
158
|
-
worktreeMode: assignment.mode,
|
|
159
|
-
branchName: assignment.branchName,
|
|
160
133
|
events: [],
|
|
161
134
|
};
|
|
162
135
|
}
|
|
163
|
-
function buildWorkerPrompt(subtaskPrompt,
|
|
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')}`;
|
|
164
140
|
return [
|
|
165
141
|
'You are executing a delegated Claude Code subtask.',
|
|
166
|
-
|
|
142
|
+
parentContext,
|
|
167
143
|
`Assigned subtask: ${subtaskPrompt}`,
|
|
168
144
|
'Stay within scope, finish the requested work, and end with a concise verification summary.',
|
|
169
145
|
].join('\n\n');
|
|
@@ -174,10 +150,18 @@ function summarizeRun(sessions) {
|
|
|
174
150
|
.join('\n');
|
|
175
151
|
}
|
|
176
152
|
function compactEvent(event) {
|
|
177
|
-
|
|
178
|
-
|
|
153
|
+
const compact = {
|
|
154
|
+
type: event.type,
|
|
155
|
+
sessionId: event.sessionId,
|
|
179
156
|
text: event.text.slice(0, 4000),
|
|
180
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;
|
|
181
165
|
}
|
|
182
166
|
async function runSequentially(tasks) {
|
|
183
167
|
const results = [];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ClaudeSessionRunResult, PersistentRunRecord, PersistentRunResult, SessionContextSnapshot, GitDiffResult, GitOperationResult } from '../types/contracts.js';
|
|
2
|
+
import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
|
|
3
|
+
import type { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
4
|
+
import type { SessionController } from './session-controller.js';
|
|
5
|
+
import type { GitOperations } from './git-operations.js';
|
|
6
|
+
import type { ContextTracker } from './context-tracker.js';
|
|
7
|
+
export type PersistentManagerProgressHandler = (run: PersistentRunRecord) => void | Promise<void>;
|
|
8
|
+
export declare class PersistentManager {
|
|
9
|
+
private readonly sessionController;
|
|
10
|
+
private readonly gitOps;
|
|
11
|
+
private readonly stateStore;
|
|
12
|
+
private readonly contextTracker;
|
|
13
|
+
constructor(sessionController: SessionController, gitOps: GitOperations, stateStore: FileRunStateStore, contextTracker: ContextTracker);
|
|
14
|
+
/**
|
|
15
|
+
* Send a message to the persistent Claude Code session.
|
|
16
|
+
* Creates a new session if none exists.
|
|
17
|
+
*/
|
|
18
|
+
sendMessage(cwd: string, message: string, options?: {
|
|
19
|
+
model?: string;
|
|
20
|
+
}, onEvent?: ClaudeSessionEventHandler): Promise<{
|
|
21
|
+
sessionId: string | undefined;
|
|
22
|
+
finalText: string;
|
|
23
|
+
turns?: number;
|
|
24
|
+
totalCostUsd?: number;
|
|
25
|
+
context: SessionContextSnapshot;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the current git diff.
|
|
29
|
+
*/
|
|
30
|
+
gitDiff(): Promise<GitDiffResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Commit all current changes.
|
|
33
|
+
*/
|
|
34
|
+
gitCommit(message: string): Promise<GitOperationResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Hard reset to discard all uncommitted changes.
|
|
37
|
+
*/
|
|
38
|
+
gitReset(): Promise<GitOperationResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Get current session status and context health.
|
|
41
|
+
*/
|
|
42
|
+
getStatus(): SessionContextSnapshot;
|
|
43
|
+
/**
|
|
44
|
+
* Clear the active session. Next send creates a fresh one.
|
|
45
|
+
*/
|
|
46
|
+
clearSession(cwd: string): Promise<string | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Compact the current session to free context.
|
|
49
|
+
*/
|
|
50
|
+
compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Execute a full task with run tracking.
|
|
53
|
+
* Creates a run record, sends the message, and persists the result.
|
|
54
|
+
*/
|
|
55
|
+
executeTask(cwd: string, task: string, options?: {
|
|
56
|
+
model?: string;
|
|
57
|
+
}, onProgress?: PersistentManagerProgressHandler): Promise<PersistentRunResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Try to restore session state from disk on startup.
|
|
60
|
+
*/
|
|
61
|
+
tryRestore(cwd: string): Promise<boolean>;
|
|
62
|
+
listRuns(cwd: string): Promise<PersistentRunRecord[]>;
|
|
63
|
+
getRun(cwd: string, runId: string): Promise<PersistentRunRecord | null>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export class PersistentManager {
|
|
3
|
+
sessionController;
|
|
4
|
+
gitOps;
|
|
5
|
+
stateStore;
|
|
6
|
+
contextTracker;
|
|
7
|
+
constructor(sessionController, gitOps, stateStore, contextTracker) {
|
|
8
|
+
this.sessionController = sessionController;
|
|
9
|
+
this.gitOps = gitOps;
|
|
10
|
+
this.stateStore = stateStore;
|
|
11
|
+
this.contextTracker = contextTracker;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Send a message to the persistent Claude Code session.
|
|
15
|
+
* Creates a new session if none exists.
|
|
16
|
+
*/
|
|
17
|
+
async sendMessage(cwd, message, options, onEvent) {
|
|
18
|
+
const result = await this.sessionController.sendMessage(cwd, message, options, onEvent);
|
|
19
|
+
return {
|
|
20
|
+
sessionId: result.sessionId,
|
|
21
|
+
finalText: result.finalText,
|
|
22
|
+
turns: result.turns,
|
|
23
|
+
totalCostUsd: result.totalCostUsd,
|
|
24
|
+
context: this.sessionController.getContextSnapshot(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get the current git diff.
|
|
29
|
+
*/
|
|
30
|
+
async gitDiff() {
|
|
31
|
+
return this.gitOps.diff();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Commit all current changes.
|
|
35
|
+
*/
|
|
36
|
+
async gitCommit(message) {
|
|
37
|
+
return this.gitOps.commit(message);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Hard reset to discard all uncommitted changes.
|
|
41
|
+
*/
|
|
42
|
+
async gitReset() {
|
|
43
|
+
return this.gitOps.resetHard();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get current session status and context health.
|
|
47
|
+
*/
|
|
48
|
+
getStatus() {
|
|
49
|
+
return this.sessionController.getContextSnapshot();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Clear the active session. Next send creates a fresh one.
|
|
53
|
+
*/
|
|
54
|
+
async clearSession(cwd) {
|
|
55
|
+
return this.sessionController.clearSession(cwd);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Compact the current session to free context.
|
|
59
|
+
*/
|
|
60
|
+
async compactSession(cwd, onEvent) {
|
|
61
|
+
return this.sessionController.compactSession(cwd, onEvent);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Execute a full task with run tracking.
|
|
65
|
+
* Creates a run record, sends the message, and persists the result.
|
|
66
|
+
*/
|
|
67
|
+
async executeTask(cwd, task, options, onProgress) {
|
|
68
|
+
const runId = randomUUID();
|
|
69
|
+
const createdAt = new Date().toISOString();
|
|
70
|
+
const runRecord = {
|
|
71
|
+
id: runId,
|
|
72
|
+
cwd,
|
|
73
|
+
task,
|
|
74
|
+
status: 'running',
|
|
75
|
+
createdAt,
|
|
76
|
+
updatedAt: createdAt,
|
|
77
|
+
sessionId: this.sessionController.sessionId,
|
|
78
|
+
sessionHistory: [],
|
|
79
|
+
messages: [
|
|
80
|
+
{
|
|
81
|
+
timestamp: createdAt,
|
|
82
|
+
direction: 'sent',
|
|
83
|
+
text: task,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
actions: [],
|
|
87
|
+
commits: [],
|
|
88
|
+
context: this.sessionController.getContextSnapshot(),
|
|
89
|
+
};
|
|
90
|
+
await this.stateStore.saveRun(runRecord);
|
|
91
|
+
await onProgress?.(runRecord);
|
|
92
|
+
try {
|
|
93
|
+
const result = await this.sessionController.sendMessage(cwd, task, options, async (event) => {
|
|
94
|
+
// Update run record with progress events
|
|
95
|
+
const currentRun = await this.stateStore.getRun(cwd, runId);
|
|
96
|
+
if (currentRun) {
|
|
97
|
+
const updated = {
|
|
98
|
+
...currentRun,
|
|
99
|
+
updatedAt: new Date().toISOString(),
|
|
100
|
+
sessionId: event.sessionId ?? currentRun.sessionId,
|
|
101
|
+
context: this.sessionController.getContextSnapshot(),
|
|
102
|
+
};
|
|
103
|
+
await this.stateStore.saveRun(updated);
|
|
104
|
+
await onProgress?.(updated);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const completedRun = await this.stateStore.updateRun(cwd, runId, (run) => ({
|
|
108
|
+
...run,
|
|
109
|
+
status: 'completed',
|
|
110
|
+
updatedAt: new Date().toISOString(),
|
|
111
|
+
sessionId: result.sessionId ?? run.sessionId,
|
|
112
|
+
messages: [
|
|
113
|
+
...run.messages,
|
|
114
|
+
{
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
direction: 'received',
|
|
117
|
+
text: result.finalText,
|
|
118
|
+
turns: result.turns,
|
|
119
|
+
totalCostUsd: result.totalCostUsd,
|
|
120
|
+
inputTokens: result.inputTokens,
|
|
121
|
+
outputTokens: result.outputTokens,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
context: this.sessionController.getContextSnapshot(),
|
|
125
|
+
finalSummary: result.finalText,
|
|
126
|
+
}));
|
|
127
|
+
return { run: completedRun };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const failedRun = await this.stateStore.updateRun(cwd, runId, (run) => ({
|
|
131
|
+
...run,
|
|
132
|
+
status: 'failed',
|
|
133
|
+
updatedAt: new Date().toISOString(),
|
|
134
|
+
context: this.sessionController.getContextSnapshot(),
|
|
135
|
+
finalSummary: error instanceof Error ? error.message : String(error),
|
|
136
|
+
}));
|
|
137
|
+
return { run: failedRun };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Try to restore session state from disk on startup.
|
|
142
|
+
*/
|
|
143
|
+
async tryRestore(cwd) {
|
|
144
|
+
return this.sessionController.tryRestore(cwd);
|
|
145
|
+
}
|
|
146
|
+
listRuns(cwd) {
|
|
147
|
+
return this.stateStore.listRuns(cwd);
|
|
148
|
+
}
|
|
149
|
+
getRun(cwd, runId) {
|
|
150
|
+
return this.stateStore.getRun(cwd, runId);
|
|
151
|
+
}
|
|
152
|
+
}
|