pi-crew 0.6.1 → 0.6.4
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/CHANGELOG.md +194 -0
- package/README.md +81 -33
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/docs/ui-optimization-plan.md +447 -0
- package/package.json +2 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +213 -35
- package/src/extension/registration/brief-tool-overrides.ts +400 -0
- package/src/extension/registration/commands.ts +27 -2
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +9 -4
- package/src/extension/registration/team-tool.ts +17 -10
- package/src/extension/registration/viewers.ts +2 -2
- package/src/extension/team-tool/api.ts +3 -3
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +64 -14
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +173 -46
- package/src/hooks/registry.ts +77 -13
- package/src/hooks/types.ts +9 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/async-runner.ts +35 -7
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +9 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +164 -37
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +2 -0
- package/src/runtime/diagnostic-export.ts +1 -1
- package/src/runtime/dynamic-script-runner.ts +10 -7
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +1 -1
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +17 -2
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/skill-effectiveness.ts +105 -62
- package/src/runtime/skill-instructions.ts +110 -32
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +251 -57
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +92 -70
- package/src/runtime/team-runner.ts +186 -20
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +254 -70
- package/src/state/health-store.ts +71 -0
- package/src/state/locks.ts +102 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/session-state-map.ts +51 -0
- package/src/state/state-store.ts +330 -43
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +2 -2
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/ui/status-colors.ts +5 -1
- package/src/ui/theme-adapter.ts +80 -1
- package/src/ui/tool-progress-formatter.ts +9 -5
- package/src/ui/tool-render.ts +4 -0
- package/src/ui/tool-renderers/brief-mode.ts +207 -0
- package/src/ui/tool-renderers/index.ts +627 -0
- package/src/ui/widget/index.ts +224 -0
- package/src/ui/widget/widget-formatters.ts +148 -0
- package/src/ui/widget/widget-model.ts +90 -0
- package/src/ui/widget/widget-renderer.ts +130 -0
- package/src/ui/widget/widget-types.ts +37 -0
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/guards.ts +110 -0
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +281 -10
- package/src/worktree/cleanup.ts +112 -24
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- package/test-integration-check.ts +4 -0
|
@@ -114,7 +114,11 @@ function tokenize(input: string): Token[] {
|
|
|
114
114
|
class ChainParser {
|
|
115
115
|
private pos = 0;
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
private tokens: Token[];
|
|
118
|
+
|
|
119
|
+
constructor(tokens: Token[]) {
|
|
120
|
+
this.tokens = tokens;
|
|
121
|
+
}
|
|
118
122
|
|
|
119
123
|
parse(): ChainStep[] {
|
|
120
124
|
const steps: ChainStep[] = [];
|
|
@@ -106,10 +106,16 @@ export class ChainRunner {
|
|
|
106
106
|
/** Maximum size per handoff entry to prevent memory issues from large artifacts */
|
|
107
107
|
private static readonly MAX_HANDOFF_ENTRY_SIZE = 5000; // bytes per entry
|
|
108
108
|
|
|
109
|
+
private taskRunner: ChainTaskRunner;
|
|
110
|
+
private handoffManager: HandoffManager;
|
|
111
|
+
|
|
109
112
|
constructor(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
) {
|
|
113
|
+
taskRunner: ChainTaskRunner,
|
|
114
|
+
handoffManager: HandoffManager,
|
|
115
|
+
) {
|
|
116
|
+
this.taskRunner = taskRunner;
|
|
117
|
+
this.handoffManager = handoffManager;
|
|
118
|
+
}
|
|
113
119
|
|
|
114
120
|
/**
|
|
115
121
|
* Parse chain syntax: step1 -> step2 -> step3
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
3
4
|
import { assertSafePathId } from "../utils/safe-paths.ts";
|
|
5
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
4
6
|
|
|
5
7
|
export interface Checkpoint {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
runId: string;
|
|
9
|
+
taskId: string;
|
|
10
|
+
step: number;
|
|
11
|
+
context: string;
|
|
12
|
+
progress: string;
|
|
13
|
+
savedAt: number;
|
|
14
|
+
agentId: string;
|
|
15
|
+
agentModel?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface CheckpointStore {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
save(checkpoint: Checkpoint): void;
|
|
20
|
+
load(runId: string, taskId: string): Checkpoint | null;
|
|
21
|
+
delete(runId: string, taskId: string): void;
|
|
22
|
+
list(runId: string): Checkpoint[];
|
|
23
|
+
hasCheckpoint(runId: string, taskId: string): boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
interface CheckpointEntry {
|
|
25
|
-
|
|
27
|
+
checkpoints: Record<string, Checkpoint>;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -30,222 +32,302 @@ interface CheckpointEntry {
|
|
|
30
32
|
* Saves checkpoints as JSON files in .crew/state/runs/<runId>/checkpoints/
|
|
31
33
|
*/
|
|
32
34
|
export class FileCheckpointStore implements CheckpointStore {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
35
|
+
private readonly stateRoot: string;
|
|
36
|
+
|
|
37
|
+
constructor(stateRoot: string) {
|
|
38
|
+
this.stateRoot = stateRoot;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private checkpointDir(): string {
|
|
42
|
+
return path.join(this.stateRoot, "checkpoints");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private checkpointPath(taskId: string): string {
|
|
46
|
+
return path.join(this.checkpointDir(), `${taskId}.json`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private ensureDir(): void {
|
|
50
|
+
const dir = this.checkpointDir();
|
|
51
|
+
if (!fs.existsSync(dir)) {
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
save(checkpoint: Checkpoint): void {
|
|
57
|
+
// Validate taskId to prevent path traversal: the taskId is used to
|
|
58
|
+
// build a file path under this.checkpointDir(). Without validation, a
|
|
59
|
+
// malicious or buggy taskId like "../../../etc/passwd" could escape
|
|
60
|
+
// the checkpoints directory.
|
|
61
|
+
assertSafePathId("taskId", checkpoint.taskId);
|
|
62
|
+
this.ensureDir();
|
|
63
|
+
const p = this.checkpointPath(checkpoint.taskId);
|
|
64
|
+
// Atomic write: write to temp file first, then rename, then fsync parent.
|
|
65
|
+
// This guarantees either the old file or the new file, never a partial
|
|
66
|
+
// write, even on network filesystems or certain journal modes.
|
|
67
|
+
const tmp = path.join(this.checkpointDir(), ".tmp.checkpoint");
|
|
68
|
+
fs.writeFileSync(tmp, JSON.stringify(checkpoint, null, 2), "utf-8");
|
|
69
|
+
fs.renameSync(tmp, p);
|
|
70
|
+
// fsync parent directory to ensure the rename is durable
|
|
71
|
+
const dirFd = fs.openSync(this.checkpointDir(), "r");
|
|
72
|
+
try {
|
|
73
|
+
fs.fsyncSync(dirFd);
|
|
74
|
+
} catch {
|
|
75
|
+
// EPERM on Windows: opening a directory and fsync-ing it is not supported
|
|
76
|
+
// on all Windows configurations. Best-effort — the rename is still atomic.
|
|
77
|
+
} finally {
|
|
78
|
+
fs.closeSync(dirFd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
load(runId: string, taskId: string): Checkpoint | null {
|
|
83
|
+
assertSafePathId("taskId", taskId);
|
|
84
|
+
const p = this.checkpointPath(taskId);
|
|
85
|
+
if (!fs.existsSync(p)) return null;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8")) as Checkpoint;
|
|
89
|
+
// Verify it's for the correct run
|
|
90
|
+
if (data.runId !== runId) return null;
|
|
91
|
+
return data;
|
|
92
|
+
} catch {
|
|
93
|
+
// File existed but JSON was corrupt — log and rename for later inspection
|
|
94
|
+
logInternalError("checkpoint-load", new Error("JSON parse failed"), `file=${p}`);
|
|
95
|
+
try {
|
|
96
|
+
fs.renameSync(p, `${p}.corrupt.${Date.now()}`);
|
|
97
|
+
} catch {
|
|
98
|
+
// Best effort — ignore rename failure
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
delete(runId: string, taskId: string): void {
|
|
105
|
+
assertSafePathId("taskId", taskId);
|
|
106
|
+
const p = this.checkpointPath(taskId);
|
|
107
|
+
if (fs.existsSync(p)) {
|
|
108
|
+
try {
|
|
109
|
+
const data = JSON.parse(
|
|
110
|
+
fs.readFileSync(p, "utf-8"),
|
|
111
|
+
) as Checkpoint;
|
|
112
|
+
if (data.runId === runId) {
|
|
113
|
+
fs.unlinkSync(p);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// File existed but couldn't read — delete it anyway
|
|
117
|
+
try {
|
|
118
|
+
fs.unlinkSync(p);
|
|
119
|
+
} catch {
|
|
120
|
+
/* ignore */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
list(runId: string): Checkpoint[] {
|
|
127
|
+
const dir = this.checkpointDir();
|
|
128
|
+
if (!fs.existsSync(dir)) return [];
|
|
129
|
+
|
|
130
|
+
return fs
|
|
131
|
+
.readdirSync(dir)
|
|
132
|
+
.filter((f) => f.endsWith(".json"))
|
|
133
|
+
.map((f) => {
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(
|
|
136
|
+
fs.readFileSync(path.join(dir, f), "utf-8"),
|
|
137
|
+
) as Checkpoint;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
.filter((c): c is Checkpoint => c !== null && c.runId === runId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
hasCheckpoint(runId: string, taskId: string): boolean {
|
|
146
|
+
return this.load(runId, taskId) !== null;
|
|
147
|
+
}
|
|
117
148
|
}
|
|
118
149
|
|
|
150
|
+
const MAX_STORES = 100;
|
|
119
151
|
const _stores = new Map<string, FileCheckpointStore>();
|
|
120
152
|
|
|
121
153
|
/**
|
|
122
154
|
* Get checkpoint store for a run's state root.
|
|
155
|
+
* Uses LRU eviction when the store exceeds MAX_STORES entries.
|
|
123
156
|
*/
|
|
124
157
|
export function getCheckpointStore(stateRoot: string): CheckpointStore {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
158
|
+
if (!_stores.has(stateRoot)) {
|
|
159
|
+
if (_stores.size >= MAX_STORES) {
|
|
160
|
+
// Evict the oldest entry (first in insertion order)
|
|
161
|
+
const oldestKey = _stores.keys().next().value;
|
|
162
|
+
if (oldestKey !== undefined) {
|
|
163
|
+
_stores.delete(oldestKey);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
_stores.set(stateRoot, new FileCheckpointStore(stateRoot));
|
|
167
|
+
}
|
|
168
|
+
return _stores.get(stateRoot)!;
|
|
129
169
|
}
|
|
130
170
|
|
|
131
171
|
/**
|
|
132
172
|
* Clear all checkpoint stores (for testing).
|
|
133
173
|
*/
|
|
134
174
|
export function clearCheckpointStores(): void {
|
|
135
|
-
|
|
175
|
+
_stores.clear();
|
|
136
176
|
}
|
|
137
177
|
|
|
138
178
|
/**
|
|
139
179
|
* Save a checkpoint during agent execution.
|
|
140
180
|
*/
|
|
141
181
|
export function saveCheckpoint(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
182
|
+
runId: string,
|
|
183
|
+
taskId: string,
|
|
184
|
+
step: number,
|
|
185
|
+
context: string,
|
|
186
|
+
progress: string,
|
|
187
|
+
agentId: string,
|
|
188
|
+
agentModel?: string,
|
|
189
|
+
cwd?: string,
|
|
149
190
|
): void {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
191
|
+
// Validate both runId and taskId to prevent path traversal: these are
|
|
192
|
+
// used to build the file path under <crewRoot>/state/runs/<runId>/checkpoints/<taskId>.json.
|
|
193
|
+
assertSafePathId("runId", runId);
|
|
194
|
+
assertSafePathId("taskId", taskId);
|
|
195
|
+
const checkpoint: Checkpoint = {
|
|
196
|
+
runId,
|
|
197
|
+
taskId,
|
|
198
|
+
step,
|
|
199
|
+
context,
|
|
200
|
+
progress,
|
|
201
|
+
savedAt: Date.now(),
|
|
202
|
+
agentId,
|
|
203
|
+
agentModel,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// State root is parent of checkpoints dir. Use projectCrewRoot() so the
|
|
207
|
+
// path lands in .pi/teams/state/runs/ for .pi-based projects (issue #29).
|
|
208
|
+
const stateRoot = path.join(
|
|
209
|
+
projectCrewRoot(cwd ?? process.cwd()),
|
|
210
|
+
"state",
|
|
211
|
+
"runs",
|
|
212
|
+
runId,
|
|
213
|
+
);
|
|
214
|
+
const store = getCheckpointStore(stateRoot);
|
|
215
|
+
store.save(checkpoint);
|
|
169
216
|
}
|
|
170
217
|
|
|
171
218
|
/**
|
|
172
219
|
* Load a checkpoint for resuming.
|
|
173
220
|
*/
|
|
174
|
-
export function loadCheckpoint(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
221
|
+
export function loadCheckpoint(
|
|
222
|
+
runId: string,
|
|
223
|
+
taskId: string,
|
|
224
|
+
cwd?: string,
|
|
225
|
+
): Checkpoint | null {
|
|
226
|
+
assertSafePathId("runId", runId);
|
|
227
|
+
assertSafePathId("taskId", taskId);
|
|
228
|
+
const stateRoot = path.join(
|
|
229
|
+
projectCrewRoot(cwd ?? process.cwd()),
|
|
230
|
+
"state",
|
|
231
|
+
"runs",
|
|
232
|
+
runId,
|
|
233
|
+
);
|
|
234
|
+
const store = getCheckpointStore(stateRoot);
|
|
235
|
+
return store.load(runId, taskId);
|
|
180
236
|
}
|
|
181
237
|
|
|
182
238
|
/**
|
|
183
239
|
* Delete a checkpoint after successful completion.
|
|
184
240
|
*/
|
|
185
|
-
export function clearCheckpoint(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
241
|
+
export function clearCheckpoint(
|
|
242
|
+
runId: string,
|
|
243
|
+
taskId: string,
|
|
244
|
+
cwd?: string,
|
|
245
|
+
): void {
|
|
246
|
+
assertSafePathId("runId", runId);
|
|
247
|
+
assertSafePathId("taskId", taskId);
|
|
248
|
+
const stateRoot = path.join(
|
|
249
|
+
projectCrewRoot(cwd ?? process.cwd()),
|
|
250
|
+
"state",
|
|
251
|
+
"runs",
|
|
252
|
+
runId,
|
|
253
|
+
);
|
|
254
|
+
const store = getCheckpointStore(stateRoot);
|
|
255
|
+
store.delete(runId, taskId);
|
|
191
256
|
}
|
|
192
257
|
|
|
193
258
|
/**
|
|
194
259
|
* Check if a checkpoint exists for a task.
|
|
195
260
|
*/
|
|
196
|
-
export function hasCheckpoint(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
261
|
+
export function hasCheckpoint(
|
|
262
|
+
runId: string,
|
|
263
|
+
taskId: string,
|
|
264
|
+
cwd?: string,
|
|
265
|
+
): boolean {
|
|
266
|
+
assertSafePathId("runId", runId);
|
|
267
|
+
assertSafePathId("taskId", taskId);
|
|
268
|
+
const stateRoot = path.join(
|
|
269
|
+
projectCrewRoot(cwd ?? process.cwd()),
|
|
270
|
+
"state",
|
|
271
|
+
"runs",
|
|
272
|
+
runId,
|
|
273
|
+
);
|
|
274
|
+
const store = getCheckpointStore(stateRoot);
|
|
275
|
+
return store.hasCheckpoint(runId, taskId);
|
|
202
276
|
}
|
|
203
277
|
|
|
204
278
|
/**
|
|
205
279
|
* List all checkpoints for a run.
|
|
206
280
|
*/
|
|
207
|
-
export function listCheckpoints(runId: string): Checkpoint[] {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
281
|
+
export function listCheckpoints(runId: string, cwd?: string): Checkpoint[] {
|
|
282
|
+
assertSafePathId("runId", runId);
|
|
283
|
+
const stateRoot = path.join(
|
|
284
|
+
projectCrewRoot(cwd ?? process.cwd()),
|
|
285
|
+
"state",
|
|
286
|
+
"runs",
|
|
287
|
+
runId,
|
|
288
|
+
);
|
|
289
|
+
const store = getCheckpointStore(stateRoot);
|
|
290
|
+
return store.list(runId);
|
|
212
291
|
}
|
|
213
292
|
|
|
214
293
|
/**
|
|
215
294
|
* Format a checkpoint for display.
|
|
216
295
|
*/
|
|
217
296
|
export function formatCheckpoint(checkpoint: Checkpoint): string {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
297
|
+
return [
|
|
298
|
+
`## Checkpoint: ${checkpoint.taskId}`,
|
|
299
|
+
"",
|
|
300
|
+
`**Agent:** ${checkpoint.agentId}`,
|
|
301
|
+
checkpoint.agentModel ? `**Model:** ${checkpoint.agentModel}` : "",
|
|
302
|
+
"",
|
|
303
|
+
`**Progress:** ${checkpoint.progress}`,
|
|
304
|
+
"",
|
|
305
|
+
`**Step:** ${checkpoint.step}`,
|
|
306
|
+
`**Saved:** ${new Date(checkpoint.savedAt).toISOString()}`,
|
|
307
|
+
"",
|
|
308
|
+
`**Context:** ${checkpoint.context.slice(0, 300)}${checkpoint.context.length > 300 ? "..." : ""}`,
|
|
309
|
+
]
|
|
310
|
+
.filter(Boolean)
|
|
311
|
+
.join("\n");
|
|
231
312
|
}
|
|
232
313
|
|
|
233
314
|
/**
|
|
234
315
|
* Format all checkpoints for a run.
|
|
235
316
|
*/
|
|
236
|
-
export function formatAllCheckpoints(runId: string): string {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
317
|
+
export function formatAllCheckpoints(runId: string, cwd?: string): string {
|
|
318
|
+
const checkpoints = listCheckpoints(runId, cwd);
|
|
319
|
+
if (checkpoints.length === 0) {
|
|
320
|
+
return `No checkpoints found for run ${runId}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return [
|
|
324
|
+
`# Checkpoints: ${runId}`,
|
|
325
|
+
"",
|
|
326
|
+
...checkpoints.map(
|
|
327
|
+
(cp, i) =>
|
|
328
|
+
`${i + 1}. **${cp.taskId}** — ${cp.progress} (${new Date(cp.savedAt).toLocaleString()})`,
|
|
329
|
+
),
|
|
330
|
+
"",
|
|
331
|
+
`Use \`team action='resume' runId=${runId} taskId=<taskId>\` to resume.`,
|
|
332
|
+
].join("\n");
|
|
251
333
|
}
|