pi-crew 0.1.51 → 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 (239) 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/doctor.ts +28 -3
  65. package/src/extension/team-tool/handle-settings.ts +195 -188
  66. package/src/extension/team-tool/inspect.ts +41 -41
  67. package/src/extension/team-tool/intent-policy.ts +42 -42
  68. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  69. package/src/extension/team-tool/plan.ts +19 -19
  70. package/src/extension/team-tool/run.ts +12 -1
  71. package/src/extension/team-tool.ts +11 -1
  72. package/src/i18n.ts +184 -184
  73. package/src/observability/exporters/otlp-exporter.ts +92 -77
  74. package/src/prompt/prompt-runtime.ts +72 -72
  75. package/src/runtime/agent-memory.ts +72 -72
  76. package/src/runtime/agent-observability.ts +114 -114
  77. package/src/runtime/async-marker.ts +26 -26
  78. package/src/runtime/attention-events.ts +28 -28
  79. package/src/runtime/auto-resume.ts +100 -0
  80. package/src/runtime/background-runner.ts +11 -1
  81. package/src/runtime/cancellation-token.ts +89 -89
  82. package/src/runtime/cancellation.ts +61 -61
  83. package/src/runtime/capability-inventory.ts +116 -116
  84. package/src/runtime/child-pi.ts +7 -2
  85. package/src/runtime/compaction-summary.ts +271 -0
  86. package/src/runtime/completion-guard.ts +190 -190
  87. package/src/runtime/crash-recovery.ts +33 -0
  88. package/src/runtime/delta-conflict.ts +360 -0
  89. package/src/runtime/direct-run.ts +35 -35
  90. package/src/runtime/foreground-control.ts +82 -82
  91. package/src/runtime/green-contract.ts +46 -46
  92. package/src/runtime/group-join.ts +106 -106
  93. package/src/runtime/heartbeat-gradient.ts +28 -28
  94. package/src/runtime/heartbeat-watcher.ts +124 -124
  95. package/src/runtime/iteration-hooks.ts +262 -0
  96. package/src/runtime/live-agent-control.ts +88 -88
  97. package/src/runtime/live-control-realtime.ts +36 -36
  98. package/src/runtime/live-extension-bridge.ts +150 -150
  99. package/src/runtime/live-irc.ts +92 -92
  100. package/src/runtime/live-session-health.ts +100 -100
  101. package/src/runtime/loop-gates.ts +129 -0
  102. package/src/runtime/metric-parser.ts +40 -0
  103. package/src/runtime/notebook-helpers.ts +90 -90
  104. package/src/runtime/orphan-sentinel.ts +7 -7
  105. package/src/runtime/parallel-research.ts +44 -44
  106. package/src/runtime/phase-progress.ts +217 -0
  107. package/src/runtime/pi-args.ts +38 -11
  108. package/src/runtime/pi-json-output.ts +111 -111
  109. package/src/runtime/pi-spawn.ts +57 -7
  110. package/src/runtime/policy-engine.ts +79 -79
  111. package/src/runtime/post-checks.ts +122 -0
  112. package/src/runtime/progress-event-coalescer.ts +43 -43
  113. package/src/runtime/prose-compressor.ts +164 -164
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/result-extractor.ts +121 -121
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/sensitive-paths.ts +2 -2
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/stream-preview.ts +177 -177
  123. package/src/runtime/supervisor-contact.ts +59 -59
  124. package/src/runtime/task-display.ts +38 -38
  125. package/src/runtime/task-graph.ts +207 -0
  126. package/src/runtime/task-quality.ts +207 -0
  127. package/src/runtime/task-runner/capabilities.ts +78 -78
  128. package/src/runtime/task-runner/live-executor.ts +7 -1
  129. package/src/runtime/task-runner/progress.ts +119 -119
  130. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  131. package/src/runtime/task-runner/result-utils.ts +14 -14
  132. package/src/runtime/task-runner/run-projection.ts +103 -103
  133. package/src/runtime/task-runner/state-helpers.ts +22 -22
  134. package/src/runtime/team-runner.ts +117 -7
  135. package/src/runtime/worker-heartbeat.ts +21 -21
  136. package/src/runtime/worker-startup.ts +57 -57
  137. package/src/runtime/workflow-state.ts +187 -0
  138. package/src/runtime/workspace-tree.ts +298 -298
  139. package/src/schema/config-schema.ts +11 -0
  140. package/src/schema/validation-types.ts +148 -0
  141. package/src/skills/skill-templates.ts +374 -0
  142. package/src/state/active-run-registry.ts +35 -11
  143. package/src/state/atomic-write.ts +33 -26
  144. package/src/state/contracts.ts +1 -0
  145. package/src/state/event-reconstructor.ts +217 -0
  146. package/src/state/locks.ts +2 -13
  147. package/src/state/mailbox.ts +4 -3
  148. package/src/state/state-store.ts +32 -14
  149. package/src/state/task-claims.ts +44 -44
  150. package/src/state/types.ts +9 -0
  151. package/src/state/usage.ts +29 -29
  152. package/src/subagents/async-entry.ts +1 -1
  153. package/src/subagents/index.ts +3 -3
  154. package/src/subagents/live/control.ts +1 -1
  155. package/src/subagents/live/manager.ts +1 -1
  156. package/src/subagents/live/realtime.ts +1 -1
  157. package/src/subagents/live/session-runtime.ts +1 -1
  158. package/src/subagents/manager.ts +1 -1
  159. package/src/subagents/spawn.ts +1 -1
  160. package/src/teams/team-serializer.ts +38 -38
  161. package/src/types/diff.d.ts +18 -18
  162. package/src/ui/crew-footer.ts +101 -101
  163. package/src/ui/crew-select-list.ts +111 -111
  164. package/src/ui/crew-widget.ts +5 -2
  165. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  166. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  167. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  168. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  169. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  170. package/src/ui/dynamic-border.ts +25 -25
  171. package/src/ui/layout-primitives.ts +106 -106
  172. package/src/ui/loaders.ts +158 -158
  173. package/src/ui/render-coalescer.ts +51 -51
  174. package/src/ui/render-diff.ts +119 -119
  175. package/src/ui/render-scheduler.ts +143 -143
  176. package/src/ui/run-action-dispatcher.ts +10 -1
  177. package/src/ui/spinner.ts +17 -17
  178. package/src/ui/status-colors.ts +58 -58
  179. package/src/ui/syntax-highlight.ts +116 -116
  180. package/src/ui/transcript-entries.ts +258 -258
  181. package/src/utils/completion-dedupe.ts +63 -63
  182. package/src/utils/frontmatter.ts +68 -68
  183. package/src/utils/git.ts +262 -262
  184. package/src/utils/ids.ts +17 -17
  185. package/src/utils/incremental-reader.ts +104 -104
  186. package/src/utils/names.ts +27 -27
  187. package/src/utils/redaction.ts +44 -44
  188. package/src/utils/safe-paths.ts +47 -47
  189. package/src/utils/scan-cache.ts +136 -136
  190. package/src/utils/sleep.ts +40 -26
  191. package/src/utils/task-name-generator.ts +337 -337
  192. package/src/workflows/validate-workflow.ts +40 -40
  193. package/src/worktree/branch-freshness.ts +45 -45
  194. package/teams/default.team.md +12 -12
  195. package/teams/fast-fix.team.md +11 -11
  196. package/teams/implementation.team.md +18 -18
  197. package/teams/parallel-research.team.md +14 -14
  198. package/teams/research.team.md +11 -11
  199. package/teams/review.team.md +12 -12
  200. package/workflows/default.workflow.md +30 -29
  201. package/workflows/fast-fix.workflow.md +23 -22
  202. package/workflows/implementation.workflow.md +43 -43
  203. package/workflows/parallel-research.workflow.md +46 -46
  204. package/workflows/research.workflow.md +22 -22
  205. package/workflows/review.workflow.md +30 -30
  206. package/docs/refactor-tasks-phase3.md +0 -394
  207. package/docs/refactor-tasks-phase4.md +0 -564
  208. package/docs/refactor-tasks-phase5.md +0 -402
  209. package/docs/refactor-tasks-phase6.md +0 -662
  210. package/docs/refactor-tasks.md +0 -1484
  211. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  212. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  213. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  214. package/docs/research/AUDIT_PI_CREW.md +0 -457
  215. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  216. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  217. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  218. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  219. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  220. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  221. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  222. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  223. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  224. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  225. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  226. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  227. package/docs/research-extension-examples.md +0 -297
  228. package/docs/research-extension-system.md +0 -324
  229. package/docs/research-oh-my-pi-distillation.md +0 -369
  230. package/docs/research-optimization-plan.md +0 -548
  231. package/docs/research-phase10-distillation.md +0 -199
  232. package/docs/research-phase11-distillation.md +0 -201
  233. package/docs/research-phase8-operator-experience-plan.md +0 -819
  234. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  235. package/docs/research-pi-coding-agent.md +0 -357
  236. package/docs/research-source-pi-crew-reference.md +0 -174
  237. package/docs/research-ui-optimization-plan.md +0 -480
  238. package/docs/source-runtime-refactor-map.md +0 -107
  239. 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> {
@@ -10,6 +10,7 @@ import { DEFAULT_PATHS } from "../../config/defaults.ts";
10
10
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
11
11
  import { getPiSpawnCommand } from "../../runtime/pi-spawn.ts";
12
12
  import { validateResources } from "../validate-resources.ts";
13
+ import { detectDrift, formatDriftReport, type DriftReport } from "../../config/drift-detector.ts";
13
14
  import { TeamToolParams } from "../../schema/team-tool-schema.ts";
14
15
  import type { PiTeamsToolResult } from "../tool-result.ts";
15
16
  import { configRecord, result, type TeamContext } from "./context.ts";
@@ -112,9 +113,19 @@ export interface TeamDoctorReportInput {
112
113
  export interface TeamDoctorReport {
113
114
  text: string;
114
115
  hasErrors: boolean;
116
+ drift?: DriftReport;
115
117
  }
116
118
 
117
119
  export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorReport {
120
+ // Compute drift once — reused in both Drift section and return value
121
+ const driftResult = detectDrift(
122
+ {
123
+ agents: allAgents(discoverAgents(input.cwd)).map((a) => a.name),
124
+ teams: allTeams(discoverTeams(input.cwd)).map((t) => t.name),
125
+ workflows: allWorkflows(discoverWorkflows(input.cwd)).map((w) => w.name),
126
+ },
127
+ loadConfig(input.cwd).config,
128
+ );
118
129
  const sections = [
119
130
  section("Runtime", () => {
120
131
  const git = commandExists("git", ["--version"]);
@@ -156,6 +167,15 @@ export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorR
156
167
  ok: input.validationErrors === 0,
157
168
  detail: `${input.validationErrors} errors, ${input.validationWarnings} warnings`,
158
169
  }]),
170
+ section("Drift", () => {
171
+ const driftErrors = driftResult.items.filter((item) => item.severity === "error").length;
172
+ const driftWarnings = driftResult.items.filter((item) => item.severity === "warning").length;
173
+ return [{
174
+ label: "config drift",
175
+ ok: !driftResult.hasDrift || driftErrors === 0,
176
+ detail: driftResult.hasDrift ? `${driftErrors} errors, ${driftWarnings} warnings` : "no drift detected",
177
+ }];
178
+ }),
159
179
  section("Schema", () => {
160
180
  const schemaIssues = auditJsonSchema(TeamToolParams);
161
181
  return [{ label: "strict-provider schema", ok: schemaIssues.length === 0, detail: schemaIssues.length ? schemaIssues.slice(0, 3).join("; ") : "team tool schema compatible" }];
@@ -181,7 +201,7 @@ export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorR
181
201
  }
182
202
  if (lines.at(-1) === "") lines.pop();
183
203
  const text = lines.join("\n");
184
- return { text, hasErrors: sections.some((sectionLines) => sectionLines.some((line) => line.includes("FAIL"))) };
204
+ return { text, hasErrors: sections.some((sectionLines) => sectionLines.some((line) => line.includes("FAIL"))), drift: driftResult.hasDrift ? driftResult : undefined };
185
205
  }
186
206
 
187
207
  export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {}): PiTeamsToolResult {
@@ -203,7 +223,7 @@ export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {})
203
223
  }
204
224
  }
205
225
  const validation = validateResources(ctx.cwd);
206
- const { text, hasErrors } = buildTeamDoctorReport({
226
+ const { text, hasErrors, drift } = buildTeamDoctorReport({
207
227
  cwd: ctx.cwd,
208
228
  configPath: loadedConfig.path,
209
229
  configErrors: loadedConfig.error ? [loadedConfig.error] : [],
@@ -213,5 +233,10 @@ export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {})
213
233
  validationWarnings: validation.issues.filter((issue) => issue.level === "warning").length,
214
234
  smokeChildPi,
215
235
  });
216
- return result(text, { action: "doctor", status: hasErrors ? "error" : "ok" }, hasErrors);
236
+ // Append detailed drift section if any drift was detected
237
+ let finalText = text;
238
+ if (drift?.hasDrift) {
239
+ finalText = `${text}\n\nDrift details:\n${formatDriftReport(drift)}`;
240
+ }
241
+ return result(finalText, { action: "doctor", status: hasErrors ? "error" : "ok" }, hasErrors);
217
242
  }