pi-crew 0.1.49 → 0.2.0

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 (249) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +176 -781
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +70 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/actions-reference.md +595 -0
  14. package/docs/commands-reference.md +347 -0
  15. package/docs/runtime-flow.md +148 -148
  16. package/index.ts +6 -6
  17. package/package.json +99 -99
  18. package/skills/async-worker-recovery/SKILL.md +42 -42
  19. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  20. package/skills/delegation-patterns/SKILL.md +54 -54
  21. package/skills/mailbox-interactive/SKILL.md +40 -40
  22. package/skills/model-routing-context/SKILL.md +39 -39
  23. package/skills/multi-perspective-review/SKILL.md +58 -58
  24. package/skills/observability-reliability/SKILL.md +41 -41
  25. package/skills/orchestration/SKILL.md +157 -157
  26. package/skills/ownership-session-security/SKILL.md +41 -41
  27. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  28. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  29. package/skills/resource-discovery-config/SKILL.md +41 -41
  30. package/skills/runtime-state-reader/SKILL.md +44 -44
  31. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  32. package/skills/state-mutation-locking/SKILL.md +42 -42
  33. package/skills/systematic-debugging/SKILL.md +67 -67
  34. package/skills/ui-render-performance/SKILL.md +39 -39
  35. package/skills/verification-before-done/SKILL.md +57 -57
  36. package/skills/worktree-isolation/SKILL.md +39 -39
  37. package/src/adapters/claude-adapter.ts +25 -0
  38. package/src/adapters/codex-adapter.ts +21 -0
  39. package/src/adapters/cursor-adapter.ts +17 -0
  40. package/src/adapters/export-util.ts +137 -0
  41. package/src/adapters/index.ts +15 -0
  42. package/src/adapters/registry.ts +18 -0
  43. package/src/adapters/types.ts +23 -0
  44. package/src/agents/agent-config.ts +2 -0
  45. package/src/agents/agent-search.ts +98 -98
  46. package/src/agents/discover-agents.ts +2 -1
  47. package/src/config/config.ts +14 -1
  48. package/src/config/defaults.ts +5 -5
  49. package/src/config/drift-detector.ts +211 -0
  50. package/src/config/markers.ts +327 -0
  51. package/src/config/resilient-parser.ts +108 -0
  52. package/src/config/suggestions.ts +74 -0
  53. package/src/extension/cross-extension-rpc.ts +103 -82
  54. package/src/extension/project-init.ts +36 -4
  55. package/src/extension/register.ts +67 -22
  56. package/src/extension/registration/commands.ts +77 -8
  57. package/src/extension/registration/subagent-tools.ts +10 -1
  58. package/src/extension/registration/team-tool.ts +10 -1
  59. package/src/extension/registration/viewers.ts +48 -34
  60. package/src/extension/run-bundle-schema.ts +89 -89
  61. package/src/extension/run-export.ts +26 -12
  62. package/src/extension/run-import.ts +25 -1
  63. package/src/extension/run-index.ts +5 -1
  64. package/src/extension/run-maintenance.ts +142 -68
  65. package/src/extension/team-manager-command.ts +10 -1
  66. package/src/extension/team-tool/context.ts +1 -1
  67. package/src/extension/team-tool/doctor.ts +28 -3
  68. package/src/extension/team-tool/handle-settings.ts +195 -188
  69. package/src/extension/team-tool/inspect.ts +41 -41
  70. package/src/extension/team-tool/intent-policy.ts +42 -42
  71. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  72. package/src/extension/team-tool/plan.ts +19 -19
  73. package/src/extension/team-tool/run.ts +12 -1
  74. package/src/extension/team-tool.ts +14 -3
  75. package/src/i18n.ts +184 -184
  76. package/src/observability/exporters/otlp-exporter.ts +92 -77
  77. package/src/prompt/prompt-runtime.ts +72 -72
  78. package/src/runtime/agent-memory.ts +72 -72
  79. package/src/runtime/agent-observability.ts +114 -114
  80. package/src/runtime/async-marker.ts +26 -26
  81. package/src/runtime/attention-events.ts +28 -28
  82. package/src/runtime/auto-resume.ts +100 -0
  83. package/src/runtime/background-runner.ts +11 -1
  84. package/src/runtime/cancellation-token.ts +89 -89
  85. package/src/runtime/cancellation.ts +61 -61
  86. package/src/runtime/capability-inventory.ts +116 -116
  87. package/src/runtime/child-pi.ts +7 -2
  88. package/src/runtime/compaction-summary.ts +271 -0
  89. package/src/runtime/completion-guard.ts +190 -190
  90. package/src/runtime/concurrency.ts +3 -1
  91. package/src/runtime/crash-recovery.ts +33 -0
  92. package/src/runtime/delta-conflict.ts +360 -0
  93. package/src/runtime/diagnostic-export.ts +3 -1
  94. package/src/runtime/direct-run.ts +35 -35
  95. package/src/runtime/event-stream-bridge.ts +3 -1
  96. package/src/runtime/foreground-control.ts +82 -82
  97. package/src/runtime/green-contract.ts +46 -46
  98. package/src/runtime/group-join.ts +106 -106
  99. package/src/runtime/heartbeat-gradient.ts +28 -28
  100. package/src/runtime/heartbeat-watcher.ts +124 -124
  101. package/src/runtime/iteration-hooks.ts +262 -0
  102. package/src/runtime/live-agent-control.ts +88 -88
  103. package/src/runtime/live-control-realtime.ts +36 -36
  104. package/src/runtime/live-extension-bridge.ts +150 -150
  105. package/src/runtime/live-irc.ts +92 -92
  106. package/src/runtime/live-session-health.ts +100 -100
  107. package/src/runtime/loop-gates.ts +129 -0
  108. package/src/runtime/metric-parser.ts +40 -0
  109. package/src/runtime/notebook-helpers.ts +90 -90
  110. package/src/runtime/orphan-sentinel.ts +7 -7
  111. package/src/runtime/parallel-research.ts +44 -44
  112. package/src/runtime/phase-progress.ts +217 -0
  113. package/src/runtime/pi-args.ts +38 -2
  114. package/src/runtime/pi-json-output.ts +111 -111
  115. package/src/runtime/pi-spawn.ts +74 -6
  116. package/src/runtime/policy-engine.ts +79 -79
  117. package/src/runtime/post-checks.ts +122 -0
  118. package/src/runtime/process-status.ts +14 -1
  119. package/src/runtime/progress-event-coalescer.ts +43 -43
  120. package/src/runtime/prose-compressor.ts +164 -164
  121. package/src/runtime/recovery-recipes.ts +74 -74
  122. package/src/runtime/result-extractor.ts +121 -121
  123. package/src/runtime/role-permission.ts +39 -39
  124. package/src/runtime/sensitive-paths.ts +3 -3
  125. package/src/runtime/session-resources.ts +25 -25
  126. package/src/runtime/session-snapshot.ts +59 -59
  127. package/src/runtime/session-usage.ts +79 -79
  128. package/src/runtime/sidechain-output.ts +29 -29
  129. package/src/runtime/stream-preview.ts +177 -177
  130. package/src/runtime/supervisor-contact.ts +59 -59
  131. package/src/runtime/task-display.ts +38 -38
  132. package/src/runtime/task-graph.ts +207 -0
  133. package/src/runtime/task-quality.ts +207 -0
  134. package/src/runtime/task-runner/capabilities.ts +78 -78
  135. package/src/runtime/task-runner/live-executor.ts +7 -1
  136. package/src/runtime/task-runner/progress.ts +119 -119
  137. package/src/runtime/task-runner/prompt-builder.ts +1 -1
  138. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  139. package/src/runtime/task-runner/result-utils.ts +14 -14
  140. package/src/runtime/task-runner/run-projection.ts +103 -103
  141. package/src/runtime/task-runner/state-helpers.ts +22 -22
  142. package/src/runtime/team-runner.ts +126 -7
  143. package/src/runtime/worker-heartbeat.ts +21 -21
  144. package/src/runtime/worker-startup.ts +57 -57
  145. package/src/runtime/workflow-state.ts +187 -0
  146. package/src/runtime/workspace-tree.ts +298 -298
  147. package/src/schema/config-schema.ts +12 -0
  148. package/src/schema/validation-types.ts +148 -0
  149. package/src/skills/skill-templates.ts +374 -0
  150. package/src/state/active-run-registry.ts +35 -11
  151. package/src/state/atomic-write.ts +33 -26
  152. package/src/state/contracts.ts +1 -0
  153. package/src/state/event-reconstructor.ts +217 -0
  154. package/src/state/locks.ts +2 -11
  155. package/src/state/mailbox.ts +4 -3
  156. package/src/state/state-store.ts +32 -14
  157. package/src/state/task-claims.ts +44 -44
  158. package/src/state/types.ts +9 -0
  159. package/src/state/usage.ts +29 -29
  160. package/src/subagents/async-entry.ts +1 -1
  161. package/src/subagents/index.ts +3 -3
  162. package/src/subagents/live/control.ts +1 -1
  163. package/src/subagents/live/manager.ts +1 -1
  164. package/src/subagents/live/realtime.ts +1 -1
  165. package/src/subagents/live/session-runtime.ts +1 -1
  166. package/src/subagents/manager.ts +1 -1
  167. package/src/subagents/spawn.ts +1 -1
  168. package/src/teams/team-serializer.ts +38 -38
  169. package/src/types/diff.d.ts +18 -18
  170. package/src/ui/crew-footer.ts +101 -101
  171. package/src/ui/crew-select-list.ts +111 -111
  172. package/src/ui/crew-widget.ts +9 -4
  173. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  174. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  175. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  176. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  177. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  178. package/src/ui/dynamic-border.ts +25 -25
  179. package/src/ui/layout-primitives.ts +106 -106
  180. package/src/ui/loaders.ts +158 -158
  181. package/src/ui/powerbar-publisher.ts +6 -0
  182. package/src/ui/render-coalescer.ts +51 -51
  183. package/src/ui/render-diff.ts +119 -119
  184. package/src/ui/render-scheduler.ts +143 -143
  185. package/src/ui/run-action-dispatcher.ts +10 -1
  186. package/src/ui/spinner.ts +17 -17
  187. package/src/ui/status-colors.ts +58 -58
  188. package/src/ui/syntax-highlight.ts +116 -116
  189. package/src/ui/transcript-entries.ts +258 -258
  190. package/src/utils/completion-dedupe.ts +63 -63
  191. package/src/utils/frontmatter.ts +68 -68
  192. package/src/utils/git.ts +262 -262
  193. package/src/utils/ids.ts +17 -17
  194. package/src/utils/incremental-reader.ts +104 -104
  195. package/src/utils/names.ts +27 -27
  196. package/src/utils/redaction.ts +44 -44
  197. package/src/utils/safe-paths.ts +47 -47
  198. package/src/utils/scan-cache.ts +136 -136
  199. package/src/utils/sleep.ts +40 -26
  200. package/src/utils/task-name-generator.ts +337 -337
  201. package/src/workflows/validate-workflow.ts +40 -40
  202. package/src/worktree/branch-freshness.ts +45 -45
  203. package/src/worktree/worktree-manager.ts +11 -3
  204. package/teams/default.team.md +12 -12
  205. package/teams/fast-fix.team.md +11 -11
  206. package/teams/implementation.team.md +18 -18
  207. package/teams/parallel-research.team.md +14 -14
  208. package/teams/research.team.md +11 -11
  209. package/teams/review.team.md +12 -12
  210. package/workflows/default.workflow.md +30 -29
  211. package/workflows/fast-fix.workflow.md +23 -22
  212. package/workflows/implementation.workflow.md +43 -38
  213. package/workflows/parallel-research.workflow.md +46 -46
  214. package/workflows/research.workflow.md +22 -22
  215. package/workflows/review.workflow.md +30 -30
  216. package/docs/refactor-tasks-phase3.md +0 -394
  217. package/docs/refactor-tasks-phase4.md +0 -564
  218. package/docs/refactor-tasks-phase5.md +0 -402
  219. package/docs/refactor-tasks-phase6.md +0 -662
  220. package/docs/refactor-tasks.md +0 -1484
  221. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  222. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  223. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  224. package/docs/research/AUDIT_PI_CREW.md +0 -457
  225. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  226. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  227. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  228. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  229. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  230. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  231. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  232. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  233. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  234. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  235. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  236. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  237. package/docs/research-extension-examples.md +0 -297
  238. package/docs/research-extension-system.md +0 -324
  239. package/docs/research-oh-my-pi-distillation.md +0 -369
  240. package/docs/research-optimization-plan.md +0 -548
  241. package/docs/research-phase10-distillation.md +0 -199
  242. package/docs/research-phase11-distillation.md +0 -201
  243. package/docs/research-phase8-operator-experience-plan.md +0 -819
  244. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  245. package/docs/research-pi-coding-agent.md +0 -357
  246. package/docs/research-source-pi-crew-reference.md +0 -174
  247. package/docs/research-ui-optimization-plan.md +0 -480
  248. package/docs/source-runtime-refactor-map.md +0 -107
  249. package/src/utils/atomic-write.ts +0 -33
@@ -1,34 +1,48 @@
1
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
- import { loadRunManifestById } from "../../state/state-store.ts";
3
- import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
4
- import { loadConfig } from "../../config/config.ts";
5
- import { DurableTranscriptViewer } from "../../ui/transcript-viewer.ts";
6
-
7
- export async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
8
- if (!runId) return undefined;
9
- if (taskId) return { runId, taskId };
10
- const loaded = loadRunManifestById(ctx.cwd, runId);
11
- if (!loaded) return { runId };
12
- const agents = readCrewAgents(loaded.manifest);
13
- if (ctx.hasUI && agents.length > 1) {
14
- const choice = await ctx.ui.select("Select pi-crew agent", agents.map((agent) => `${agent.taskId} ${agent.role}→${agent.agent} [${agent.status}]`));
15
- return { runId, taskId: choice?.split(" ")[0] };
16
- }
17
- return { runId, taskId: agents[0]?.taskId };
18
- }
19
-
20
- export async function openTranscriptViewer(ctx: ExtensionCommandContext, initialRunId: string | undefined, initialTaskId?: string): Promise<boolean> {
21
- const selected = await selectAgentTask(ctx, initialRunId, initialTaskId);
22
- if (!selected) return false;
23
- const runId = selected.runId;
24
- const taskId = selected.taskId;
25
- if (!runId || !ctx.hasUI) return false;
26
- const loaded = loadRunManifestById(ctx.cwd, runId);
27
- if (!loaded) return false;
28
- const uiConfig = loadConfig(ctx.cwd).config.ui;
29
- await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTranscriptViewer(loaded.manifest, theme, done, taskId, { maxTailBytes: uiConfig?.transcriptTailBytes }), {
30
- overlay: true,
31
- overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" },
32
- });
33
- return true;
34
- }
1
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { loadRunManifestById } from "../../state/state-store.ts";
3
+ import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
4
+ import { loadConfig } from "../../config/config.ts";
5
+ // Lazy-loaded: DurableTranscriptViewer is 658ms — only needed for /crew transcript command
6
+ import type { DurableTranscriptViewer as DurableTranscriptViewerType } from "../../ui/transcript-viewer.ts";
7
+ let _cachedViewer: typeof DurableTranscriptViewerType | undefined;
8
+ let _viewerPromise: Promise<typeof DurableTranscriptViewerType> | undefined;
9
+ async function getViewer(): Promise<typeof DurableTranscriptViewerType> {
10
+ if (_cachedViewer) return _cachedViewer;
11
+ if (!_viewerPromise) {
12
+ _viewerPromise = import("../../ui/transcript-viewer.ts").then((mod) => {
13
+ _cachedViewer = mod.DurableTranscriptViewer;
14
+ return mod.DurableTranscriptViewer;
15
+ });
16
+ }
17
+ return _viewerPromise;
18
+ }
19
+
20
+ export async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
21
+ if (!runId) return undefined;
22
+ if (taskId) return { runId, taskId };
23
+ const loaded = loadRunManifestById(ctx.cwd, runId);
24
+ if (!loaded) return { runId };
25
+ const agents = readCrewAgents(loaded.manifest);
26
+ if (ctx.hasUI && agents.length > 1) {
27
+ const choice = await ctx.ui.select("Select pi-crew agent", agents.map((agent) => `${agent.taskId} ${agent.role}→${agent.agent} [${agent.status}]`));
28
+ return { runId, taskId: choice?.split(" ")[0] };
29
+ }
30
+ return { runId, taskId: agents[0]?.taskId };
31
+ }
32
+
33
+ export async function openTranscriptViewer(ctx: ExtensionCommandContext, initialRunId: string | undefined, initialTaskId?: string): Promise<boolean> {
34
+ const selected = await selectAgentTask(ctx, initialRunId, initialTaskId);
35
+ if (!selected) return false;
36
+ const runId = selected.runId;
37
+ const taskId = selected.taskId;
38
+ if (!runId || !ctx.hasUI) return false;
39
+ const loaded = loadRunManifestById(ctx.cwd, runId);
40
+ if (!loaded) return false;
41
+ const uiConfig = loadConfig(ctx.cwd).config.ui;
42
+ const DurableTranscriptViewer = await getViewer();
43
+ await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTranscriptViewer(loaded.manifest, theme, done, taskId, { maxTailBytes: uiConfig?.transcriptTailBytes }), {
44
+ overlay: true,
45
+ overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" },
46
+ });
47
+ return true;
48
+ }
@@ -1,89 +1,89 @@
1
- import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
2
- import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
3
- import type { TeamEvent } from "../state/event-log.ts";
4
- import type { ExportedRunBundle } from "./run-export.ts";
5
-
6
- export interface BundleValidationResult {
7
- ok: boolean;
8
- errors: string[];
9
- }
10
-
11
- function isRecord(value: unknown): value is Record<string, unknown> {
12
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
- }
14
-
15
- function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
16
- if (!isRecord(value)) {
17
- errors.push(`manifest.artifacts[${index}] must be an object.`);
18
- return false;
19
- }
20
- const before = errors.length;
21
- if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
22
- if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
23
- if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
24
- if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
25
- if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
26
- return errors.length === before;
27
- }
28
-
29
- function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
30
- if (!isRecord(value)) {
31
- errors.push("manifest must be an object.");
32
- return false;
33
- }
34
- const before = errors.length;
35
- if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
36
- for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
37
- if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
38
- }
39
- if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
40
- if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
41
- if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
42
- else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
43
- return errors.length === before;
44
- }
45
-
46
- function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
47
- if (!isRecord(value)) {
48
- errors.push(`tasks[${index}] must be an object.`);
49
- return false;
50
- }
51
- const before = errors.length;
52
- for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
53
- if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
54
- }
55
- if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
56
- if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
57
- return errors.length === before;
58
- }
59
-
60
- function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
61
- if (!isRecord(value)) {
62
- errors.push(`events[${index}] must be an object.`);
63
- return false;
64
- }
65
- const before = errors.length;
66
- for (const field of ["time", "type", "runId"] as const) {
67
- if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
68
- }
69
- return errors.length === before;
70
- }
71
-
72
- export function validateRunBundle(value: unknown): BundleValidationResult {
73
- const errors: string[] = [];
74
- if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
75
- if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
76
- if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
77
- validateManifest(value.manifest, errors);
78
- if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
79
- else value.tasks.forEach((task, index) => validateTask(task, index, errors));
80
- if (!Array.isArray(value.events)) errors.push("events must be an array.");
81
- else value.events.forEach((event, index) => validateEvent(event, index, errors));
82
- if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
83
- return { ok: errors.length === 0, errors };
84
- }
85
-
86
- export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
87
- const validation = validateRunBundle(value);
88
- if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
89
- }
1
+ import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
2
+ import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
3
+ import type { TeamEvent } from "../state/event-log.ts";
4
+ import type { ExportedRunBundle } from "./run-export.ts";
5
+
6
+ export interface BundleValidationResult {
7
+ ok: boolean;
8
+ errors: string[];
9
+ }
10
+
11
+ function isRecord(value: unknown): value is Record<string, unknown> {
12
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+
15
+ function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
16
+ if (!isRecord(value)) {
17
+ errors.push(`manifest.artifacts[${index}] must be an object.`);
18
+ return false;
19
+ }
20
+ const before = errors.length;
21
+ if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
22
+ if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
23
+ if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
24
+ if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
25
+ if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
26
+ return errors.length === before;
27
+ }
28
+
29
+ function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
30
+ if (!isRecord(value)) {
31
+ errors.push("manifest must be an object.");
32
+ return false;
33
+ }
34
+ const before = errors.length;
35
+ if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
36
+ for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
37
+ if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
38
+ }
39
+ if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
40
+ if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
41
+ if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
42
+ else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
43
+ return errors.length === before;
44
+ }
45
+
46
+ function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
47
+ if (!isRecord(value)) {
48
+ errors.push(`tasks[${index}] must be an object.`);
49
+ return false;
50
+ }
51
+ const before = errors.length;
52
+ for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
53
+ if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
54
+ }
55
+ if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
56
+ if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
57
+ return errors.length === before;
58
+ }
59
+
60
+ function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
61
+ if (!isRecord(value)) {
62
+ errors.push(`events[${index}] must be an object.`);
63
+ return false;
64
+ }
65
+ const before = errors.length;
66
+ for (const field of ["time", "type", "runId"] as const) {
67
+ if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
68
+ }
69
+ return errors.length === before;
70
+ }
71
+
72
+ export function validateRunBundle(value: unknown): BundleValidationResult {
73
+ const errors: string[] = [];
74
+ if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
75
+ if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
76
+ if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
77
+ validateManifest(value.manifest, errors);
78
+ if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
79
+ else value.tasks.forEach((task, index) => validateTask(task, index, errors));
80
+ if (!Array.isArray(value.events)) errors.push("events must be an array.");
81
+ else value.events.forEach((event, index) => validateEvent(event, index, errors));
82
+ if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
83
+ return { ok: errors.length === 0, errors };
84
+ }
85
+
86
+ export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
87
+ const validation = validateRunBundle(value);
88
+ if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
89
+ }
@@ -1,8 +1,19 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import * as os from "node:os";
3
4
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
5
  import { writeArtifact } from "../state/artifact-store.ts";
5
6
  import { readEvents, type TeamEvent } from "../state/event-log.ts";
7
+ import { redactSecrets } from "../utils/redaction.ts";
8
+
9
+ /** Replace absolute paths containing home directory with ~/ */
10
+ function redactHomePaths<T>(obj: T): T {
11
+ const home = os.homedir();
12
+ if (!home) return redactSecrets(obj) as T;
13
+ const json = JSON.stringify(obj);
14
+ const safe = json.split(home).join("~");
15
+ return redactSecrets(JSON.parse(safe)) as T;
16
+ }
6
17
 
7
18
  export interface ExportedRunBundle {
8
19
  schemaVersion: 1;
@@ -15,13 +26,16 @@ export interface ExportedRunBundle {
15
26
 
16
27
  export function exportRunBundle(manifest: TeamRunManifest, tasks: TeamTaskState[]): { jsonPath: string; markdownPath: string } {
17
28
  const events = readEvents(manifest.eventsPath);
29
+ const safeManifest = redactHomePaths(manifest);
30
+ const safeTasks = redactHomePaths(tasks);
31
+ const safeEvents = redactHomePaths(events);
18
32
  const bundle: ExportedRunBundle = {
19
33
  schemaVersion: 1,
20
34
  exportedAt: new Date().toISOString(),
21
- manifest,
22
- tasks,
23
- events,
24
- artifactPaths: manifest.artifacts.map((artifact) => artifact.path),
35
+ manifest: safeManifest as TeamRunManifest,
36
+ tasks: safeTasks as TeamTaskState[],
37
+ events: safeEvents as TeamEvent[],
38
+ artifactPaths: safeManifest.artifacts.map((artifact) => artifact.path),
25
39
  };
26
40
  const json = writeArtifact(manifest.artifactsRoot, {
27
41
  kind: "metadata",
@@ -34,22 +48,22 @@ export function exportRunBundle(manifest: TeamRunManifest, tasks: TeamTaskState[
34
48
  relativePath: "export/run-export.md",
35
49
  producer: "run-export",
36
50
  content: [
37
- `# pi-crew export ${manifest.runId}`,
51
+ `# pi-crew export ${safeManifest.runId}`,
38
52
  "",
39
53
  `Exported: ${bundle.exportedAt}`,
40
- `Status: ${manifest.status}`,
41
- `Team: ${manifest.team}`,
42
- `Workflow: ${manifest.workflow ?? "(none)"}`,
43
- `Goal: ${manifest.goal}`,
54
+ `Status: ${safeManifest.status}`,
55
+ `Team: ${safeManifest.team}`,
56
+ `Workflow: ${safeManifest.workflow ?? "(none)"}`,
57
+ `Goal: ${safeManifest.goal}`,
44
58
  "",
45
59
  "## Tasks",
46
- ...tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
60
+ ...safeTasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
47
61
  "",
48
62
  "## Artifacts",
49
- ...(manifest.artifacts.length ? manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}`) : ["- (none)"]),
63
+ ...(safeManifest.artifacts.length ? safeManifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}`) : ["- (none)"]),
50
64
  "",
51
65
  "## Recent Events",
52
- ...(events.slice(-20).map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`)),
66
+ ...(safeEvents.slice(-20).map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`)),
53
67
  "",
54
68
  ].join("\n"),
55
69
  });
@@ -4,12 +4,14 @@ import { assertRunBundle } from "./run-bundle-schema.ts";
4
4
  import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
5
5
  import { DEFAULT_PATHS } from "../config/defaults.ts";
6
6
  import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
7
+ import { detectImportConflicts, type ConflictReport } from "../runtime/delta-conflict.ts";
7
8
 
8
9
  export interface ImportedRunBundleInfo {
9
10
  runId: string;
10
11
  importedAt: string;
11
12
  bundlePath: string;
12
13
  summaryPath: string;
14
+ conflictReport?: ConflictReport;
13
15
  }
14
16
 
15
17
  function importRoot(cwd: string, scope: "project" | "user"): string {
@@ -19,10 +21,32 @@ function importRoot(cwd: string, scope: "project" | "user"): string {
19
21
 
20
22
  export function importRunBundle(cwd: string, bundlePath: string, scope: "project" | "user" = "project"): ImportedRunBundleInfo {
21
23
  const resolvedPath = path.isAbsolute(bundlePath) ? bundlePath : path.resolve(cwd, bundlePath);
24
+ // Path containment: only allow reading bundles from cwd or user home
25
+ const allowedBases = [cwd];
26
+ try { allowedBases.push(userCrewRoot()); } catch { /* ignore */ }
27
+ try { allowedBases.push(projectCrewRoot(cwd)); } catch { /* ignore */ }
28
+ const isContained = allowedBases.some((base) => resolvedPath.startsWith(base + path.sep) || resolvedPath === base);
29
+ if (!isContained) throw new Error(`Import path must be within project directory or crew root: ${resolvedPath}`);
22
30
  const raw = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
23
31
  assertRunBundle(raw);
24
32
  const runId = assertSafePathId("runId", raw.manifest.runId);
25
33
  const importedAt = new Date().toISOString();
34
+
35
+ // Non-blocking conflict detection: compare incoming bundle against any existing state.
36
+ let conflictReport: ConflictReport | undefined;
37
+ try {
38
+ const existingManifestPath = path.join(importRoot(cwd, scope), runId, "run-export.json");
39
+ if (fs.existsSync(existingManifestPath)) {
40
+ const existingRaw = JSON.parse(fs.readFileSync(existingManifestPath, "utf-8")) as { manifest?: Record<string, unknown>; tasks?: unknown[] };
41
+ conflictReport = detectImportConflicts(
42
+ { manifest: raw.manifest as unknown as Record<string, unknown>, tasks: raw.tasks as unknown[] },
43
+ { manifest: existingRaw.manifest, tasks: existingRaw.tasks },
44
+ );
45
+ }
46
+ } catch {
47
+ // Conflict detection is best-effort; do not block import on failure.
48
+ }
49
+
26
50
  const importsRoot = importRoot(cwd, scope);
27
51
  fs.mkdirSync(importsRoot, { recursive: true });
28
52
  if (fs.lstatSync(importsRoot).isSymbolicLink()) throw new Error(`Invalid import root: ${importsRoot}`);
@@ -56,5 +80,5 @@ export function importRunBundle(cwd: string, bundlePath: string, scope: "project
56
80
  ...raw.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
57
81
  "",
58
82
  ].join("\n"), "utf-8");
59
- return { runId, importedAt, bundlePath: targetJson, summaryPath: targetSummary };
83
+ return { runId, importedAt, bundlePath: targetJson, summaryPath: targetSummary, ...(conflictReport?.hasConflicts ? { conflictReport } : {}) };
60
84
  }
@@ -33,7 +33,11 @@ function collectRuns(root: string, maxEntries?: number, signal?: AbortSignal): T
33
33
  if (i % 10 === 0) token.heartbeat(`collectRuns:${i}/${selected.length}`);
34
34
  try {
35
35
  const manifest = readManifest(path.join(resolveRealContainedPath(runsRoot, selected[i]), DEFAULT_PATHS.state.manifestFile));
36
- if (manifest) results.push(manifest);
36
+ if (!manifest) continue;
37
+ // Filter out ghost runs: active status but CWD no longer exists.
38
+ // These are deadletter/replay/temp runs whose temp dirs were cleaned up.
39
+ if ((manifest.status === "queued" || manifest.status === "running" || manifest.status === "planning") && manifest.cwd && !fs.existsSync(manifest.cwd)) continue;
40
+ results.push(manifest);
37
41
  } catch { /* skip unreadable manifests */ }
38
42
  }
39
43
  return results;
@@ -1,68 +1,142 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { TeamRunManifest } from "../state/types.ts";
4
- import { resolveRealContainedPath } from "../utils/safe-paths.ts";
5
- import { projectCrewRoot } from "../utils/paths.ts";
6
- import { listRuns } from "./run-index.ts";
7
- import { logInternalError } from "../utils/internal-error.ts";
8
- import { redactSecrets } from "../utils/redaction.ts";
9
- import { createCancellationToken } from "../runtime/cancellation-token.ts";
10
-
11
- export interface PruneRunsResult {
12
- kept: string[];
13
- removed: string[];
14
- auditPath?: string;
15
- }
16
-
17
- export interface PruneRunsOptions {
18
- intent?: string;
19
- signal?: AbortSignal;
20
- }
21
-
22
- function isFinished(run: TeamRunManifest): boolean {
23
- return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
24
- }
25
-
26
- function isSafeToPrune(cwd: string, run: TeamRunManifest): boolean {
27
- try {
28
- const crewRoot = projectCrewRoot(cwd);
29
- resolveRealContainedPath(crewRoot, run.stateRoot);
30
- resolveRealContainedPath(crewRoot, run.artifactsRoot);
31
- return true;
32
- } catch {
33
- return false;
34
- }
35
- }
36
-
37
- function appendPruneAudit(cwd: string, payload: Record<string, unknown>): string | undefined {
38
- try {
39
- const filePath = path.join(projectCrewRoot(cwd), "audit", "prune.jsonl");
40
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
41
- fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ ...payload, auditedAt: new Date().toISOString() }))}\n`, "utf-8");
42
- return filePath;
43
- } catch (error) {
44
- logInternalError("prune.audit-write", error, `cwd=${cwd}`);
45
- return undefined;
46
- }
47
- }
48
-
49
- export function pruneFinishedRuns(cwd: string, keep: number, options: PruneRunsOptions = {}): PruneRunsResult {
50
- const token = createCancellationToken({ signal: options.signal });
51
- const finished = listRuns(cwd, options.signal).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
52
- const kept = finished.slice(0, keep).map((run) => run.runId);
53
- const removed: string[] = [];
54
- const toRemove = finished.slice(keep);
55
- for (let i = 0; i < toRemove.length; i++) {
56
- if (i % 5 === 0) token.heartbeat(`prune:${i}/${toRemove.length}`);
57
- const run = toRemove[i];
58
- if (!isSafeToPrune(cwd, run)) {
59
- logInternalError("prune.path-unsafe", new Error(`Skipping unsafe prune: stateRoot=${run.stateRoot}, artifactsRoot=${run.artifactsRoot}`), `runId=${run.runId}`);
60
- continue;
61
- }
62
- fs.rmSync(run.stateRoot, { recursive: true, force: true });
63
- fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
64
- removed.push(run.runId);
65
- }
66
- const auditPath = appendPruneAudit(cwd, { action: "prune", keep, intent: options.intent, kept, removed });
67
- return { kept, removed, auditPath };
68
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { TeamRunManifest } from "../state/types.ts";
4
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
5
+ import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
6
+ import { listRuns } from "./run-index.ts";
7
+ import { logInternalError } from "../utils/internal-error.ts";
8
+ import { redactSecrets } from "../utils/redaction.ts";
9
+ import { createCancellationToken } from "../runtime/cancellation-token.ts";
10
+ import { DEFAULT_PATHS } from "../config/defaults.ts";
11
+ import { isSafePathId } from "../utils/safe-paths.ts";
12
+
13
+ export interface PruneRunsResult {
14
+ kept: string[];
15
+ removed: string[];
16
+ auditPath?: string;
17
+ }
18
+
19
+ export interface PruneRunsOptions {
20
+ intent?: string;
21
+ signal?: AbortSignal;
22
+ }
23
+
24
+ function isFinished(run: TeamRunManifest): boolean {
25
+ return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
26
+ }
27
+
28
+ function isSafeToPrune(cwd: string, run: TeamRunManifest): boolean {
29
+ try {
30
+ const crewRoot = run.stateRoot.startsWith(userCrewRoot() + path.sep) ? userCrewRoot() : projectCrewRoot(cwd);
31
+ resolveRealContainedPath(crewRoot, run.stateRoot);
32
+ resolveRealContainedPath(crewRoot, run.artifactsRoot);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function appendPruneAudit(cwd: string, payload: Record<string, unknown>): string | undefined {
40
+ try {
41
+ const filePath = path.join(projectCrewRoot(cwd), "audit", "prune.jsonl");
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ ...payload, auditedAt: new Date().toISOString() }))}\n`, "utf-8");
44
+ return filePath;
45
+ } catch (error) {
46
+ logInternalError("prune.audit-write", error, `cwd=${cwd}`);
47
+ return undefined;
48
+ }
49
+ }
50
+
51
+ export function pruneFinishedRuns(cwd: string, keep: number, options: PruneRunsOptions = {}): PruneRunsResult {
52
+ const token = createCancellationToken({ signal: options.signal });
53
+ const finished = listRuns(cwd, options.signal).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
54
+ const kept = finished.slice(0, keep).map((run) => run.runId);
55
+ const removed: string[] = [];
56
+ const toRemove = finished.slice(keep);
57
+ for (let i = 0; i < toRemove.length; i++) {
58
+ if (i % 5 === 0) token.heartbeat(`prune:${i}/${toRemove.length}`);
59
+ const run = toRemove[i];
60
+ if (!isSafeToPrune(cwd, run)) {
61
+ logInternalError("prune.path-unsafe", new Error(`Skipping unsafe prune: stateRoot=${run.stateRoot}, artifactsRoot=${run.artifactsRoot}`), `runId=${run.runId}`);
62
+ continue;
63
+ }
64
+ fs.rmSync(run.stateRoot, { recursive: true, force: true });
65
+ fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
66
+ removed.push(run.runId);
67
+ }
68
+ const auditPath = appendPruneAudit(cwd, { action: "prune", keep, intent: options.intent, kept, removed });
69
+ return { kept, removed, auditPath };
70
+ }
71
+
72
+ /**
73
+ * Prune finished run directories at the user level (~/.pi/agent/extensions/pi-crew/state/runs/).
74
+ *
75
+ * This handles runs created without a project root (e.g. `team action='run'` from home directory)
76
+ * that would otherwise accumulate forever.
77
+ *
78
+ * @param keep Number of most recent finished runs to retain
79
+ * @returns kept and removed run IDs
80
+ */
81
+ export function pruneUserLevelRuns(keep: number): PruneRunsResult {
82
+ const crewRoot = userCrewRoot();
83
+ const runsRoot = path.join(crewRoot, DEFAULT_PATHS.state.runsSubdir);
84
+ if (!fs.existsSync(runsRoot)) return { kept: [], removed: [] };
85
+
86
+ // Read all run directories, parse manifests, filter to finished
87
+ const MAX_DIRS = 500;
88
+ const finished: Array<{ runId: string; updatedAt: string; stateRoot: string; artifactsRoot: string }> = [];
89
+ const ghostRemoved: string[] = [];
90
+ const dirs = fs.readdirSync(runsRoot, { withFileTypes: true })
91
+ .filter((entry) => entry.isDirectory() && isSafePathId(entry.name))
92
+ .slice(0, MAX_DIRS)
93
+ .map((entry) => entry.name);
94
+
95
+ for (const dir of dirs) {
96
+ const manifestPath = path.join(runsRoot, dir, DEFAULT_PATHS.state.manifestFile);
97
+ let manifest: TeamRunManifest | undefined;
98
+ try {
99
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as TeamRunManifest;
100
+ } catch {
101
+ continue;
102
+ }
103
+
104
+ // Ghost run cleanup: active status but CWD no longer exists.
105
+ // These are deadletter/replay/temp runs from dead Pi sessions.
106
+ const isActive = manifest.status === "queued" || manifest.status === "running" || manifest.status === "planning";
107
+ if (isActive && manifest.cwd && !fs.existsSync(manifest.cwd)) {
108
+ fs.rmSync(path.join(runsRoot, dir), { recursive: true, force: true });
109
+ ghostRemoved.push(manifest.runId);
110
+ continue;
111
+ }
112
+
113
+ if (!isFinished(manifest)) continue;
114
+
115
+ // Safety check: ensure stateRoot and artifactsRoot are contained within user crew root
116
+ try {
117
+ resolveRealContainedPath(crewRoot, manifest.stateRoot);
118
+ resolveRealContainedPath(crewRoot, manifest.artifactsRoot);
119
+ } catch {
120
+ continue;
121
+ }
122
+
123
+ finished.push({
124
+ runId: manifest.runId,
125
+ updatedAt: manifest.updatedAt,
126
+ stateRoot: manifest.stateRoot,
127
+ artifactsRoot: manifest.artifactsRoot,
128
+ });
129
+ }
130
+
131
+ // Sort newest first, keep top N, remove the rest
132
+ finished.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
133
+ const kept = finished.slice(0, keep).map((r) => r.runId);
134
+ const removed: string[] = [];
135
+ for (const run of finished.slice(keep)) {
136
+ fs.rmSync(run.stateRoot, { recursive: true, force: true });
137
+ fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
138
+ removed.push(run.runId);
139
+ }
140
+
141
+ return { kept, removed: [...removed, ...ghostRemoved] };
142
+ }
@@ -1,6 +1,15 @@
1
1
  import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
2
  import { listRuns } from "./run-index.ts";
3
- import { handleTeamTool } from "./team-tool.ts";
3
+ // Lazy-loaded: team-tool.ts pulls in entire runtime chain.
4
+ import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
5
+ let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
6
+ async function handleTeamTool(params: Parameters<typeof HandleTeamToolFn>[0], ctx: Parameters<typeof HandleTeamToolFn>[1]): Promise<Awaited<ReturnType<typeof HandleTeamToolFn>>> {
7
+ if (!_cachedHandleTeamTool) {
8
+ const mod = await import("./team-tool.ts");
9
+ _cachedHandleTeamTool = mod.handleTeamTool;
10
+ }
11
+ return _cachedHandleTeamTool(params, ctx);
12
+ }
4
13
  import { isToolError, textFromToolResult } from "./tool-result.ts";
5
14
 
6
15
  async function notifyResult(ctx: ExtensionCommandContext, result: Awaited<ReturnType<typeof handleTeamTool>>): Promise<void> {