pi-crew 0.1.51 → 0.2.1

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 (240) hide show
  1. package/CHANGELOG.md +56 -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 +13 -1
  48. package/src/config/drift-detector.ts +211 -0
  49. package/src/config/markers.ts +327 -0
  50. package/src/config/resilient-parser.ts +108 -0
  51. package/src/config/suggestions.ts +74 -0
  52. package/src/extension/cross-extension-rpc.ts +103 -94
  53. package/src/extension/project-init.ts +21 -1
  54. package/src/extension/register.ts +45 -14
  55. package/src/extension/registration/commands.ts +77 -8
  56. package/src/extension/registration/subagent-tools.ts +10 -1
  57. package/src/extension/registration/team-tool.ts +10 -1
  58. package/src/extension/registration/viewers.ts +48 -34
  59. package/src/extension/run-bundle-schema.ts +89 -89
  60. package/src/extension/run-import.ts +25 -1
  61. package/src/extension/run-index.ts +5 -1
  62. package/src/extension/run-maintenance.ts +142 -68
  63. package/src/extension/team-manager-command.ts +10 -1
  64. package/src/extension/team-tool/api.ts +441 -441
  65. package/src/extension/team-tool/doctor.ts +28 -3
  66. package/src/extension/team-tool/handle-settings.ts +195 -188
  67. package/src/extension/team-tool/inspect.ts +41 -41
  68. package/src/extension/team-tool/intent-policy.ts +42 -42
  69. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  70. package/src/extension/team-tool/plan.ts +19 -19
  71. package/src/extension/team-tool/run.ts +12 -1
  72. package/src/extension/team-tool.ts +332 -322
  73. package/src/i18n.ts +184 -184
  74. package/src/observability/exporters/otlp-exporter.ts +92 -77
  75. package/src/prompt/prompt-runtime.ts +72 -72
  76. package/src/runtime/agent-memory.ts +72 -72
  77. package/src/runtime/agent-observability.ts +114 -114
  78. package/src/runtime/async-marker.ts +26 -26
  79. package/src/runtime/attention-events.ts +28 -28
  80. package/src/runtime/auto-resume.ts +100 -0
  81. package/src/runtime/background-runner.ts +11 -1
  82. package/src/runtime/cancellation-token.ts +89 -89
  83. package/src/runtime/cancellation.ts +61 -61
  84. package/src/runtime/capability-inventory.ts +116 -116
  85. package/src/runtime/child-pi.ts +7 -2
  86. package/src/runtime/compaction-summary.ts +271 -0
  87. package/src/runtime/completion-guard.ts +190 -190
  88. package/src/runtime/crash-recovery.ts +33 -1
  89. package/src/runtime/delta-conflict.ts +360 -0
  90. package/src/runtime/direct-run.ts +35 -35
  91. package/src/runtime/foreground-control.ts +82 -82
  92. package/src/runtime/green-contract.ts +46 -46
  93. package/src/runtime/group-join.ts +106 -106
  94. package/src/runtime/heartbeat-gradient.ts +28 -28
  95. package/src/runtime/heartbeat-watcher.ts +124 -124
  96. package/src/runtime/iteration-hooks.ts +264 -0
  97. package/src/runtime/live-agent-control.ts +88 -88
  98. package/src/runtime/live-control-realtime.ts +36 -36
  99. package/src/runtime/live-extension-bridge.ts +150 -150
  100. package/src/runtime/live-irc.ts +92 -92
  101. package/src/runtime/live-session-health.ts +100 -100
  102. package/src/runtime/loop-gates.ts +129 -0
  103. package/src/runtime/metric-parser.ts +40 -0
  104. package/src/runtime/notebook-helpers.ts +90 -90
  105. package/src/runtime/orphan-sentinel.ts +7 -7
  106. package/src/runtime/parallel-research.ts +44 -44
  107. package/src/runtime/phase-progress.ts +217 -0
  108. package/src/runtime/pi-args.ts +38 -11
  109. package/src/runtime/pi-json-output.ts +111 -111
  110. package/src/runtime/pi-spawn.ts +57 -7
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/post-checks.ts +122 -0
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/prose-compressor.ts +164 -164
  115. package/src/runtime/recovery-recipes.ts +74 -74
  116. package/src/runtime/result-extractor.ts +121 -121
  117. package/src/runtime/role-permission.ts +39 -39
  118. package/src/runtime/sensitive-paths.ts +2 -2
  119. package/src/runtime/session-resources.ts +25 -25
  120. package/src/runtime/session-snapshot.ts +59 -59
  121. package/src/runtime/session-usage.ts +79 -79
  122. package/src/runtime/sidechain-output.ts +29 -29
  123. package/src/runtime/stream-preview.ts +177 -177
  124. package/src/runtime/supervisor-contact.ts +59 -59
  125. package/src/runtime/task-display.ts +38 -38
  126. package/src/runtime/task-graph.ts +207 -0
  127. package/src/runtime/task-quality.ts +207 -0
  128. package/src/runtime/task-runner/capabilities.ts +78 -78
  129. package/src/runtime/task-runner/live-executor.ts +7 -1
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  132. package/src/runtime/task-runner/result-utils.ts +14 -14
  133. package/src/runtime/task-runner/run-projection.ts +103 -103
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/team-runner.ts +117 -7
  136. package/src/runtime/worker-heartbeat.ts +21 -21
  137. package/src/runtime/worker-startup.ts +57 -57
  138. package/src/runtime/workflow-state.ts +187 -0
  139. package/src/runtime/workspace-tree.ts +298 -298
  140. package/src/schema/config-schema.ts +11 -0
  141. package/src/schema/validation-types.ts +148 -0
  142. package/src/skills/skill-templates.ts +374 -0
  143. package/src/state/active-run-registry.ts +35 -11
  144. package/src/state/atomic-write.ts +33 -26
  145. package/src/state/contracts.ts +1 -0
  146. package/src/state/event-reconstructor.ts +217 -0
  147. package/src/state/locks.ts +2 -13
  148. package/src/state/mailbox.ts +4 -3
  149. package/src/state/state-store.ts +16 -6
  150. package/src/state/task-claims.ts +44 -44
  151. package/src/state/types.ts +9 -0
  152. package/src/state/usage.ts +29 -29
  153. package/src/subagents/async-entry.ts +1 -1
  154. package/src/subagents/index.ts +3 -3
  155. package/src/subagents/live/control.ts +1 -1
  156. package/src/subagents/live/manager.ts +1 -1
  157. package/src/subagents/live/realtime.ts +1 -1
  158. package/src/subagents/live/session-runtime.ts +1 -1
  159. package/src/subagents/manager.ts +1 -1
  160. package/src/subagents/spawn.ts +1 -1
  161. package/src/teams/team-serializer.ts +38 -38
  162. package/src/types/diff.d.ts +18 -18
  163. package/src/ui/crew-footer.ts +101 -101
  164. package/src/ui/crew-select-list.ts +111 -111
  165. package/src/ui/crew-widget.ts +5 -2
  166. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  167. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  168. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  169. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  170. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  171. package/src/ui/dynamic-border.ts +25 -25
  172. package/src/ui/layout-primitives.ts +106 -106
  173. package/src/ui/loaders.ts +158 -158
  174. package/src/ui/render-coalescer.ts +51 -51
  175. package/src/ui/render-diff.ts +119 -119
  176. package/src/ui/render-scheduler.ts +143 -143
  177. package/src/ui/run-action-dispatcher.ts +10 -1
  178. package/src/ui/spinner.ts +17 -17
  179. package/src/ui/status-colors.ts +58 -58
  180. package/src/ui/syntax-highlight.ts +116 -116
  181. package/src/ui/transcript-entries.ts +258 -258
  182. package/src/utils/completion-dedupe.ts +63 -63
  183. package/src/utils/frontmatter.ts +68 -68
  184. package/src/utils/git.ts +262 -262
  185. package/src/utils/ids.ts +17 -17
  186. package/src/utils/incremental-reader.ts +104 -104
  187. package/src/utils/names.ts +27 -27
  188. package/src/utils/redaction.ts +44 -44
  189. package/src/utils/safe-paths.ts +47 -47
  190. package/src/utils/scan-cache.ts +136 -136
  191. package/src/utils/sleep.ts +40 -26
  192. package/src/utils/task-name-generator.ts +337 -337
  193. package/src/workflows/validate-workflow.ts +40 -40
  194. package/src/worktree/branch-freshness.ts +45 -45
  195. package/teams/default.team.md +12 -12
  196. package/teams/fast-fix.team.md +11 -11
  197. package/teams/implementation.team.md +18 -18
  198. package/teams/parallel-research.team.md +14 -14
  199. package/teams/research.team.md +11 -11
  200. package/teams/review.team.md +12 -12
  201. package/workflows/default.workflow.md +30 -29
  202. package/workflows/fast-fix.workflow.md +23 -22
  203. package/workflows/implementation.workflow.md +43 -43
  204. package/workflows/parallel-research.workflow.md +46 -46
  205. package/workflows/research.workflow.md +22 -22
  206. package/workflows/review.workflow.md +30 -30
  207. package/docs/refactor-tasks-phase3.md +0 -394
  208. package/docs/refactor-tasks-phase4.md +0 -564
  209. package/docs/refactor-tasks-phase5.md +0 -402
  210. package/docs/refactor-tasks-phase6.md +0 -662
  211. package/docs/refactor-tasks.md +0 -1484
  212. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  213. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  214. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  215. package/docs/research/AUDIT_PI_CREW.md +0 -457
  216. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  217. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  218. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  219. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  220. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  221. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  222. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  223. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  224. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  225. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  226. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  227. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  228. package/docs/research-extension-examples.md +0 -297
  229. package/docs/research-extension-system.md +0 -324
  230. package/docs/research-oh-my-pi-distillation.md +0 -369
  231. package/docs/research-optimization-plan.md +0 -548
  232. package/docs/research-phase10-distillation.md +0 -199
  233. package/docs/research-phase11-distillation.md +0 -201
  234. package/docs/research-phase8-operator-experience-plan.md +0 -819
  235. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  236. package/docs/research-pi-coding-agent.md +0 -357
  237. package/docs/research-source-pi-crew-reference.md +0 -174
  238. package/docs/research-ui-optimization-plan.md +0 -480
  239. package/docs/source-runtime-refactor-map.md +0 -107
  240. package/src/utils/atomic-write.ts +0 -33
@@ -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
+ }
@@ -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> {