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.
Files changed (119) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +81 -33
  3. package/docs/issue-29-analysis.md +189 -0
  4. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
  5. package/docs/ui-optimization-plan.md +447 -0
  6. package/package.json +2 -2
  7. package/src/config/config.ts +106 -15
  8. package/src/errors.ts +107 -0
  9. package/src/extension/async-notifier.ts +6 -2
  10. package/src/extension/crew-cleanup.ts +8 -5
  11. package/src/extension/management.ts +464 -109
  12. package/src/extension/register.ts +213 -35
  13. package/src/extension/registration/brief-tool-overrides.ts +400 -0
  14. package/src/extension/registration/commands.ts +27 -2
  15. package/src/extension/registration/subagent-helpers.ts +2 -2
  16. package/src/extension/registration/subagent-tools.ts +9 -4
  17. package/src/extension/registration/team-tool.ts +17 -10
  18. package/src/extension/registration/viewers.ts +2 -2
  19. package/src/extension/team-tool/api.ts +3 -3
  20. package/src/extension/team-tool/cancel.ts +3 -3
  21. package/src/extension/team-tool/explain.ts +1 -1
  22. package/src/extension/team-tool/handle-schedule.ts +40 -0
  23. package/src/extension/team-tool/inspect.ts +3 -3
  24. package/src/extension/team-tool/lifecycle-actions.ts +4 -4
  25. package/src/extension/team-tool/respond.ts +2 -2
  26. package/src/extension/team-tool/run.ts +64 -14
  27. package/src/extension/team-tool/status.ts +1 -1
  28. package/src/extension/team-tool-types.ts +2 -0
  29. package/src/extension/team-tool.ts +173 -46
  30. package/src/hooks/registry.ts +77 -13
  31. package/src/hooks/types.ts +9 -0
  32. package/src/plugins/plugin-define.ts +6 -0
  33. package/src/plugins/plugin-registry.ts +32 -0
  34. package/src/plugins/plugins/index.ts +3 -0
  35. package/src/plugins/plugins/nextjs.ts +19 -0
  36. package/src/plugins/plugins/vite.ts +14 -0
  37. package/src/plugins/plugins/vitest.ts +14 -0
  38. package/src/runtime/async-runner.ts +35 -7
  39. package/src/runtime/background-runner.ts +529 -144
  40. package/src/runtime/chain-parser.ts +5 -1
  41. package/src/runtime/chain-runner.ts +9 -3
  42. package/src/runtime/checkpoint.ts +262 -180
  43. package/src/runtime/child-pi.ts +164 -37
  44. package/src/runtime/crash-recovery.ts +25 -14
  45. package/src/runtime/crew-agent-records.ts +2 -0
  46. package/src/runtime/diagnostic-export.ts +1 -1
  47. package/src/runtime/dynamic-script-runner.ts +10 -7
  48. package/src/runtime/foreground-watchdog.ts +1 -1
  49. package/src/runtime/heartbeat-watcher.ts +19 -2
  50. package/src/runtime/intercom-bridge.ts +7 -0
  51. package/src/runtime/iteration-hooks.ts +1 -1
  52. package/src/runtime/manifest-cache.ts +4 -0
  53. package/src/runtime/orphan-worker-registry.ts +444 -0
  54. package/src/runtime/parent-guard.ts +70 -16
  55. package/src/runtime/pi-args.ts +437 -14
  56. package/src/runtime/pi-spawn.ts +1 -0
  57. package/src/runtime/post-checks.ts +1 -1
  58. package/src/runtime/retry-runner.ts +9 -3
  59. package/src/runtime/run-tracker.ts +38 -10
  60. package/src/runtime/sandbox.ts +17 -2
  61. package/src/runtime/scheduler.ts +25 -2
  62. package/src/runtime/skill-effectiveness.ts +105 -62
  63. package/src/runtime/skill-instructions.ts +110 -32
  64. package/src/runtime/stale-reconciler.ts +310 -69
  65. package/src/runtime/subagent-manager.ts +251 -57
  66. package/src/runtime/task-health.ts +76 -0
  67. package/src/runtime/task-id.ts +20 -0
  68. package/src/runtime/task-runner/live-executor.ts +1 -1
  69. package/src/runtime/task-runner/progress.ts +1 -1
  70. package/src/runtime/task-runner/state-helpers.ts +110 -6
  71. package/src/runtime/task-runner.ts +92 -70
  72. package/src/runtime/team-runner.ts +186 -20
  73. package/src/schema/team-tool-schema.ts +27 -9
  74. package/src/skills/discover-skills.ts +9 -3
  75. package/src/state/active-run-registry.ts +170 -38
  76. package/src/state/artifact-store.ts +25 -13
  77. package/src/state/atomic-write-v2.ts +86 -0
  78. package/src/state/atomic-write.ts +346 -55
  79. package/src/state/blob-store.ts +178 -10
  80. package/src/state/crew-init.ts +161 -28
  81. package/src/state/decision-ledger.ts +172 -111
  82. package/src/state/event-log-rotation.ts +82 -52
  83. package/src/state/event-log.ts +254 -70
  84. package/src/state/health-store.ts +71 -0
  85. package/src/state/locks.ts +102 -20
  86. package/src/state/mailbox.ts +45 -7
  87. package/src/state/observation-store.ts +4 -1
  88. package/src/state/run-graph.ts +141 -130
  89. package/src/state/run-metrics.ts +24 -8
  90. package/src/state/session-state-map.ts +51 -0
  91. package/src/state/state-store.ts +330 -43
  92. package/src/ui/live-run-sidebar.ts +1 -1
  93. package/src/ui/loaders.ts +4 -0
  94. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  95. package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
  96. package/src/ui/powerbar-publisher.ts +1 -1
  97. package/src/ui/run-action-dispatcher.ts +2 -2
  98. package/src/ui/run-snapshot-cache.ts +1 -1
  99. package/src/ui/status-colors.ts +5 -1
  100. package/src/ui/theme-adapter.ts +80 -1
  101. package/src/ui/tool-progress-formatter.ts +9 -5
  102. package/src/ui/tool-render.ts +4 -0
  103. package/src/ui/tool-renderers/brief-mode.ts +207 -0
  104. package/src/ui/tool-renderers/index.ts +627 -0
  105. package/src/ui/widget/index.ts +224 -0
  106. package/src/ui/widget/widget-formatters.ts +148 -0
  107. package/src/ui/widget/widget-model.ts +90 -0
  108. package/src/ui/widget/widget-renderer.ts +130 -0
  109. package/src/ui/widget/widget-types.ts +37 -0
  110. package/src/utils/env-filter.ts +66 -0
  111. package/src/utils/file-coalescer.ts +9 -1
  112. package/src/utils/guards.ts +110 -0
  113. package/src/utils/paths.ts +80 -5
  114. package/src/utils/redaction.ts +6 -1
  115. package/src/utils/safe-paths.ts +281 -10
  116. package/src/worktree/cleanup.ts +112 -24
  117. package/src/worktree/worktree-manager.ts +128 -15
  118. package/test-bugs-all.mjs +10 -6
  119. 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
- constructor(private tokens: Token[]) {}
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
- private taskRunner: ChainTaskRunner,
111
- private handoffManager: HandoffManager,
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
- runId: string;
7
- taskId: string;
8
- step: number;
9
- context: string;
10
- progress: string;
11
- savedAt: number;
12
- agentId: string;
13
- agentModel?: string;
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
- save(checkpoint: Checkpoint): void;
18
- load(runId: string, taskId: string): Checkpoint | null;
19
- delete(runId: string, taskId: string): void;
20
- list(runId: string): Checkpoint[];
21
- hasCheckpoint(runId: string, taskId: string): boolean;
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
- checkpoints: Record<string, Checkpoint>;
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
- private readonly stateRoot: string;
34
-
35
- constructor(stateRoot: string) {
36
- this.stateRoot = stateRoot;
37
- }
38
-
39
- private checkpointDir(): string {
40
- return path.join(this.stateRoot, "checkpoints");
41
- }
42
-
43
- private checkpointPath(taskId: string): string {
44
- return path.join(this.checkpointDir(), `${taskId}.json`);
45
- }
46
-
47
- private ensureDir(): void {
48
- const dir = this.checkpointDir();
49
- if (!fs.existsSync(dir)) {
50
- fs.mkdirSync(dir, { recursive: true });
51
- }
52
- }
53
-
54
- save(checkpoint: Checkpoint): void {
55
- // Validate taskId to prevent path traversal: the taskId is used to
56
- // build a file path under this.checkpointDir(). Without validation, a
57
- // malicious or buggy taskId like "../../../etc/passwd" could escape
58
- // the checkpoints directory.
59
- assertSafePathId("taskId", checkpoint.taskId);
60
- this.ensureDir();
61
- const p = this.checkpointPath(checkpoint.taskId);
62
- fs.writeFileSync(p, JSON.stringify(checkpoint, null, 2), "utf-8");
63
- }
64
-
65
- load(runId: string, taskId: string): Checkpoint | null {
66
- assertSafePathId("taskId", taskId);
67
- const p = this.checkpointPath(taskId);
68
- if (!fs.existsSync(p)) return null;
69
-
70
- try {
71
- const data = JSON.parse(fs.readFileSync(p, "utf-8")) as Checkpoint;
72
- // Verify it's for the correct run
73
- if (data.runId !== runId) return null;
74
- return data;
75
- } catch {
76
- return null;
77
- }
78
- }
79
-
80
- delete(runId: string, taskId: string): void {
81
- assertSafePathId("taskId", taskId);
82
- const p = this.checkpointPath(taskId);
83
- if (fs.existsSync(p)) {
84
- try {
85
- const data = JSON.parse(fs.readFileSync(p, "utf-8")) as Checkpoint;
86
- if (data.runId === runId) {
87
- fs.unlinkSync(p);
88
- }
89
- } catch {
90
- // File existed but couldn't read — delete it anyway
91
- try {
92
- fs.unlinkSync(p);
93
- } catch { /* ignore */ }
94
- }
95
- }
96
- }
97
-
98
- list(runId: string): Checkpoint[] {
99
- const dir = this.checkpointDir();
100
- if (!fs.existsSync(dir)) return [];
101
-
102
- return fs.readdirSync(dir)
103
- .filter((f) => f.endsWith(".json"))
104
- .map((f) => {
105
- try {
106
- return JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8")) as Checkpoint;
107
- } catch {
108
- return null;
109
- }
110
- })
111
- .filter((c): c is Checkpoint => c !== null && c.runId === runId);
112
- }
113
-
114
- hasCheckpoint(runId: string, taskId: string): boolean {
115
- return this.load(runId, taskId) !== null;
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
- if (!_stores.has(stateRoot)) {
126
- _stores.set(stateRoot, new FileCheckpointStore(stateRoot));
127
- }
128
- return _stores.get(stateRoot)!;
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
- _stores.clear();
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
- runId: string,
143
- taskId: string,
144
- step: number,
145
- context: string,
146
- progress: string,
147
- agentId: string,
148
- agentModel?: string,
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
- // Validate both runId and taskId to prevent path traversal: these are
151
- // used to build the file path under .crew/state/runs/<runId>/checkpoints/<taskId>.json.
152
- assertSafePathId("runId", runId);
153
- assertSafePathId("taskId", taskId);
154
- const checkpoint: Checkpoint = {
155
- runId,
156
- taskId,
157
- step,
158
- context,
159
- progress,
160
- savedAt: Date.now(),
161
- agentId,
162
- agentModel,
163
- };
164
-
165
- // State root is parent of checkpoints dir
166
- const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
167
- const store = getCheckpointStore(stateRoot);
168
- store.save(checkpoint);
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(runId: string, taskId: string): Checkpoint | null {
175
- assertSafePathId("runId", runId);
176
- assertSafePathId("taskId", taskId);
177
- const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
178
- const store = getCheckpointStore(stateRoot);
179
- return store.load(runId, taskId);
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(runId: string, taskId: string): void {
186
- assertSafePathId("runId", runId);
187
- assertSafePathId("taskId", taskId);
188
- const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
189
- const store = getCheckpointStore(stateRoot);
190
- store.delete(runId, taskId);
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(runId: string, taskId: string): boolean {
197
- assertSafePathId("runId", runId);
198
- assertSafePathId("taskId", taskId);
199
- const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
200
- const store = getCheckpointStore(stateRoot);
201
- return store.hasCheckpoint(runId, taskId);
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
- assertSafePathId("runId", runId);
209
- const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
210
- const store = getCheckpointStore(stateRoot);
211
- return store.list(runId);
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
- return [
219
- `## Checkpoint: ${checkpoint.taskId}`,
220
- "",
221
- `**Agent:** ${checkpoint.agentId}`,
222
- checkpoint.agentModel ? `**Model:** ${checkpoint.agentModel}` : "",
223
- "",
224
- `**Progress:** ${checkpoint.progress}`,
225
- "",
226
- `**Step:** ${checkpoint.step}`,
227
- `**Saved:** ${new Date(checkpoint.savedAt).toISOString()}`,
228
- "",
229
- `**Context:** ${checkpoint.context.slice(0, 300)}${checkpoint.context.length > 300 ? "..." : ""}`,
230
- ].filter(Boolean).join("\n");
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
- const checkpoints = listCheckpoints(runId);
238
- if (checkpoints.length === 0) {
239
- return `No checkpoints found for run ${runId}`;
240
- }
241
-
242
- return [
243
- `# Checkpoints: ${runId}`,
244
- "",
245
- ...checkpoints.map((cp, i) =>
246
- `${i + 1}. **${cp.taskId}** ${cp.progress} (${new Date(cp.savedAt).toLocaleString()})`,
247
- ),
248
- "",
249
- `Use \`team action='resume' runId=${runId} taskId=<taskId>\` to resume.`,
250
- ].join("\n");
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
  }