pi-crew 0.9.56 → 0.9.57

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 (307) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +53 -5
  4. package/dist/index.mjs +5350 -4350
  5. package/docs/README.md +54 -0
  6. package/package.json +12 -12
  7. package/skills/real-test-pi-crew/SKILL.md +93 -3
  8. package/src/agents/discover-agents.ts +68 -10
  9. package/src/benchmark/feedback-loop.ts +1 -1
  10. package/src/config/config.ts +2 -1
  11. package/src/config/types.ts +12 -0
  12. package/src/extension/action-suggestions.ts +16 -63
  13. package/src/extension/async-notifier.ts +3 -3
  14. package/src/extension/command-completions.ts +2 -2
  15. package/src/extension/crew-cleanup.ts +3 -3
  16. package/src/extension/crew-vibes/footer.ts +4 -2
  17. package/src/extension/cross-extension-rpc.ts +10 -5
  18. package/src/extension/knowledge-injection.ts +57 -10
  19. package/src/extension/register.ts +2 -2
  20. package/src/extension/registration/artifact-cleanup.ts +41 -3
  21. package/src/extension/registration/commands.ts +4 -4
  22. package/src/extension/registration/context-builder.ts +10 -10
  23. package/src/extension/registration/crash-recovery-cache.ts +3 -3
  24. package/src/extension/registration/foreground-run-controller.ts +1 -1
  25. package/src/extension/registration/lazy-configurers.ts +2 -2
  26. package/src/extension/registration/lifecycle-handlers.ts +12 -12
  27. package/src/extension/registration/lifecycle.ts +2 -2
  28. package/src/extension/registration/observability.ts +2 -2
  29. package/src/extension/registration/registration-types.ts +3 -3
  30. package/src/extension/registration/runtime-cleanup.ts +1 -1
  31. package/src/extension/registration/subagent-helpers.ts +2 -2
  32. package/src/extension/registration/subagent-manager-setup.ts +2 -2
  33. package/src/extension/registration/subagent-tools.ts +21 -16
  34. package/src/extension/registration/team-tool.ts +108 -17
  35. package/src/extension/registration/viewers.ts +3 -4
  36. package/src/extension/registration/wire-cross-extension.ts +7 -7
  37. package/src/extension/rpc-hmac.ts +3 -2
  38. package/src/extension/run-bundle-schema.ts +13 -4
  39. package/src/extension/run-export.ts +2 -2
  40. package/src/extension/run-index.ts +2 -3
  41. package/src/extension/run-maintenance.ts +57 -1
  42. package/src/extension/team-manager-command.ts +3 -1
  43. package/src/extension/team-tool/anchor.ts +8 -2
  44. package/src/extension/team-tool/api.ts +117 -39
  45. package/src/extension/team-tool/cancel.ts +23 -8
  46. package/src/extension/team-tool/chain-executor.ts +1 -1
  47. package/src/extension/team-tool/dispatch/automate.ts +17 -2
  48. package/src/extension/team-tool/dispatch/control.ts +17 -2
  49. package/src/extension/team-tool/dispatch/manage.ts +34 -2
  50. package/src/extension/team-tool/dispatch/run.ts +17 -2
  51. package/src/extension/team-tool/dispatch/status.ts +37 -5
  52. package/src/extension/team-tool/doctor.ts +2 -2
  53. package/src/extension/team-tool/explain.ts +1 -1
  54. package/src/extension/team-tool/goal-wrap.ts +5 -5
  55. package/src/extension/team-tool/goal.ts +30 -9
  56. package/src/extension/team-tool/handle-schedule.ts +37 -19
  57. package/src/extension/team-tool/inspect.ts +21 -5
  58. package/src/extension/team-tool/lifecycle-actions.ts +53 -10
  59. package/src/extension/team-tool/parallel-dispatch.ts +14 -5
  60. package/src/extension/team-tool/param-error.ts +24 -0
  61. package/src/extension/team-tool/respond.ts +20 -6
  62. package/src/extension/team-tool/run.ts +27 -17
  63. package/src/extension/team-tool/status.ts +11 -5
  64. package/src/extension/team-tool.ts +26 -19
  65. package/src/hooks/registry.ts +1 -1
  66. package/src/i18n.ts +22 -0
  67. package/src/observability/event-bus.ts +1 -1
  68. package/src/prompt/prompt-runtime.ts +2 -2
  69. package/src/runtime/README.md +35 -0
  70. package/src/runtime/agent-control.ts +1 -1
  71. package/src/runtime/anchor-manager.ts +0 -2
  72. package/src/runtime/async-runner.ts +1 -1
  73. package/src/runtime/attention-events.ts +1 -1
  74. package/src/runtime/auto-summarize.ts +0 -2
  75. package/src/runtime/background-runner.ts +103 -30
  76. package/src/runtime/{crew-broker-child.ts → broker/crew-broker-child.ts} +2 -2
  77. package/src/runtime/{crew-broker-client.ts → broker/crew-broker-client.ts} +4 -7
  78. package/src/runtime/{crew-broker.ts → broker/crew-broker.ts} +13 -21
  79. package/src/runtime/chain-runner.ts +50 -5
  80. package/src/runtime/{child-pi-constants.ts → child-pi/child-pi-constants.ts} +1 -1
  81. package/src/runtime/{child-pi-kill.ts → child-pi/child-pi-kill.ts} +3 -3
  82. package/src/runtime/{child-pi-spawn.ts → child-pi/child-pi-spawn.ts} +28 -12
  83. package/src/runtime/{child-pi-steering.ts → child-pi/child-pi-steering.ts} +1 -1
  84. package/src/runtime/{child-pi-streams.ts → child-pi/child-pi-streams.ts} +11 -4
  85. package/src/runtime/{child-pi-transcript.ts → child-pi/child-pi-transcript.ts} +6 -6
  86. package/src/runtime/{child-pi.ts → child-pi/child-pi.ts} +28 -72
  87. package/src/runtime/{compact-stages → compaction/compact-stages}/bounded-tail.ts +1 -1
  88. package/src/runtime/{compact-stages → compaction/compact-stages}/index.ts +2 -2
  89. package/src/runtime/{compaction-summary.ts → compaction/compaction-summary.ts} +3 -3
  90. package/src/runtime/{tool-output-pruner.ts → compaction/tool-output-pruner.ts} +1 -1
  91. package/src/runtime/custom-tools/irc-tool.ts +2 -2
  92. package/src/runtime/delivery-coordinator.ts +0 -3
  93. package/src/runtime/diagnostic-export.ts +3 -3
  94. package/src/runtime/foreground-control.ts +1 -1
  95. package/src/runtime/foreground-watchdog.ts +1 -1
  96. package/src/runtime/{adaptive-plan.ts → goal-workflow/adaptive-plan.ts} +8 -8
  97. package/src/runtime/{dynamic-workflow-context.ts → goal-workflow/dynamic-workflow-context.ts} +16 -16
  98. package/src/runtime/{dynamic-workflow-runner.ts → goal-workflow/dynamic-workflow-runner.ts} +10 -10
  99. package/src/runtime/{goal-achievement.ts → goal-workflow/goal-achievement.ts} +3 -3
  100. package/src/runtime/{goal-evaluator.ts → goal-workflow/goal-evaluator.ts} +8 -8
  101. package/src/runtime/{goal-loop-runner.ts → goal-workflow/goal-loop-runner.ts} +27 -25
  102. package/src/runtime/{goal-state-store.ts → goal-workflow/goal-state-store.ts} +8 -8
  103. package/src/runtime/{plan-templates.ts → goal-workflow/plan-templates.ts} +1 -1
  104. package/src/runtime/group-join.ts +3 -3
  105. package/src/runtime/handoff-manager.ts +0 -2
  106. package/src/runtime/{heartbeat-watcher.ts → heartbeat/heartbeat-watcher.ts} +7 -7
  107. package/src/runtime/iteration-hooks.ts +1 -1
  108. package/src/runtime/{live-agent-control.ts → live-session/live-agent-control.ts} +2 -2
  109. package/src/runtime/{live-agent-manager.ts → live-session/live-agent-manager.ts} +4 -4
  110. package/src/runtime/{live-session-runtime.ts → live-session/live-session-runtime.ts} +39 -41
  111. package/src/runtime/manifest-cache.ts +1 -1
  112. package/src/runtime/{model-fallback.ts → model/model-fallback.ts} +1 -1
  113. package/src/runtime/{pi-args.ts → model/pi-args.ts} +20 -5
  114. package/src/runtime/model/provider-extensions.ts +116 -0
  115. package/src/runtime/{runtime-policy.ts → model/runtime-policy.ts} +2 -2
  116. package/src/runtime/{runtime-resolver.ts → model/runtime-resolver.ts} +3 -3
  117. package/src/runtime/{runtime-warmup.ts → model/runtime-warmup.ts} +8 -8
  118. package/src/runtime/orphan-worker-registry.ts +1 -1
  119. package/src/runtime/{progress-tracker.ts → output/progress-tracker.ts} +1 -1
  120. package/src/runtime/{result-extractor.ts → output/result-extractor.ts} +4 -4
  121. package/src/runtime/{sidechain-output.ts → output/sidechain-output.ts} +2 -2
  122. package/src/runtime/{stream-preview.ts → output/stream-preview.ts} +1 -1
  123. package/src/runtime/{streaming-output.ts → output/streaming-output.ts} +2 -2
  124. package/src/runtime/{tool-progress.ts → output/tool-progress.ts} +1 -1
  125. package/src/runtime/parent-guard.ts +30 -10
  126. package/src/runtime/pipeline-runner.ts +2 -2
  127. package/src/runtime/policy-engine.ts +2 -2
  128. package/src/runtime/{cancellation.ts → process/cancellation.ts} +1 -1
  129. package/src/runtime/{post-exit-stdio-guard.ts → process/post-exit-stdio-guard.ts} +1 -1
  130. package/src/runtime/{process-lifecycle.ts → process/process-lifecycle.ts} +3 -3
  131. package/src/runtime/{checkpoint.ts → recovery/checkpoint.ts} +4 -4
  132. package/src/runtime/{crash-recovery.ts → recovery/crash-recovery.ts} +93 -21
  133. package/src/runtime/{overflow-recovery.ts → recovery/overflow-recovery.ts} +1 -1
  134. package/src/runtime/{recovery-recipes.ts → recovery/recovery-recipes.ts} +1 -1
  135. package/src/runtime/{retry-executor.ts → recovery/retry-executor.ts} +2 -2
  136. package/src/runtime/{retry-runner.ts → recovery/retry-runner.ts} +2 -8
  137. package/src/runtime/run-tracker.ts +1 -1
  138. package/src/runtime/run-worker.ts +3 -3
  139. package/src/runtime/{coalesce-tasks.ts → scheduling/coalesce-tasks.ts} +3 -3
  140. package/src/runtime/{concurrency.ts → scheduling/concurrency.ts} +1 -1
  141. package/src/runtime/{parallel-research.ts → scheduling/parallel-research.ts} +4 -2
  142. package/src/runtime/{run-coalesced-task-group.ts → scheduling/run-coalesced-task-group.ts} +95 -36
  143. package/src/runtime/{task-graph-scheduler.ts → scheduling/task-graph-scheduler.ts} +1 -1
  144. package/src/runtime/session-resources.ts +1 -1
  145. package/src/runtime/settings-store.ts +1 -1
  146. package/src/runtime/single-agent-compose.ts +1 -0
  147. package/src/runtime/stale-reconciler.ts +1 -1
  148. package/src/runtime/subagent-manager.ts +1 -1
  149. package/src/runtime/supervisor-contact.ts +1 -1
  150. package/src/runtime/task-output-context.ts +7 -5
  151. package/src/runtime/task-runner/child-executor.ts +62 -22
  152. package/src/runtime/task-runner/live-executor.ts +8 -8
  153. package/src/runtime/task-runner/post-execution.ts +12 -12
  154. package/src/runtime/task-runner/pre-execution.ts +6 -6
  155. package/src/runtime/task-runner/progress.ts +1 -1
  156. package/src/runtime/task-runner/run-projection.ts +1 -1
  157. package/src/runtime/task-runner/scaffold-executor.ts +1 -1
  158. package/src/runtime/task-runner/state-helpers.ts +2 -2
  159. package/src/runtime/task-runner.ts +4 -4
  160. package/src/runtime/team-runner.ts +380 -139
  161. package/src/runtime/{completion-guard.ts → verification/completion-guard.ts} +1 -1
  162. package/src/runtime/{green-contract.ts → verification/green-contract.ts} +1 -1
  163. package/src/runtime/{post-checks.ts → verification/post-checks.ts} +4 -4
  164. package/src/runtime/{verification-gates.ts → verification/verification-gates.ts} +5 -5
  165. package/src/runtime/{verification-integrity.ts → verification/verification-integrity.ts} +1 -1
  166. package/src/schema/config-schema.ts +1 -0
  167. package/src/schema/team-tool-schema.ts +133 -177
  168. package/src/state/README.md +44 -0
  169. package/src/state/atomic-write.ts +24 -3
  170. package/src/state/{locks.ts → coordination/locks.ts} +118 -38
  171. package/src/state/{mailbox.ts → coordination/mailbox.ts} +14 -8
  172. package/src/state/{schedule.ts → coordination/schedule.ts} +3 -3
  173. package/src/state/{task-claims.ts → coordination/task-claims.ts} +1 -1
  174. package/src/state/decision-ledger.ts +1 -1
  175. package/src/state/{event-log-rotation.ts → event-log/event-log-rotation.ts} +161 -31
  176. package/src/state/{event-log.ts → event-log/event-log.ts} +38 -26
  177. package/src/state/{jsonl-writer.ts → event-log/jsonl-writer.ts} +14 -6
  178. package/src/state/hook-instinct-bridge.ts +2 -2
  179. package/src/state/{active-run-registry.ts → stores/active-run-registry.ts} +7 -7
  180. package/src/state/{artifact-store.ts → stores/artifact-store.ts} +62 -4
  181. package/src/state/{blob-store.ts → stores/blob-store.ts} +3 -3
  182. package/src/state/{health-store.ts → stores/health-store.ts} +3 -3
  183. package/src/state/{instinct-store.ts → stores/instinct-store.ts} +3 -3
  184. package/src/state/{observation-store.ts → stores/observation-store.ts} +2 -2
  185. package/src/state/{run-cache.ts → stores/run-cache.ts} +4 -4
  186. package/src/state/{run-graph.ts → stores/run-graph.ts} +4 -4
  187. package/src/state/{run-metrics.ts → stores/run-metrics.ts} +3 -3
  188. package/src/state/{state-store.ts → stores/state-store.ts} +236 -17
  189. package/src/state/types.ts +27 -3
  190. package/src/ui/component.ts +55 -0
  191. package/src/ui/crew-select-list.ts +3 -1
  192. package/src/ui/dashboard-panes/agents-pane.ts +1 -1
  193. package/src/ui/dwf-phase-display.ts +1 -1
  194. package/src/ui/heartbeat-aggregator.ts +1 -1
  195. package/src/ui/keybinding-map.ts +179 -6
  196. package/src/ui/layout-primitives.ts +9 -5
  197. package/src/ui/live-conversation-overlay.ts +1 -1
  198. package/src/ui/live-run-sidebar.ts +1 -1
  199. package/src/ui/overlay-stack.ts +148 -0
  200. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  201. package/src/ui/overlays/mailbox-compose-overlay.ts +1 -1
  202. package/src/ui/overlays/mailbox-detail-overlay.ts +2 -2
  203. package/src/ui/powerbar-publisher.ts +1 -1
  204. package/src/ui/render-scheduler.ts +1 -1
  205. package/src/ui/run-action-dispatcher.ts +3 -3
  206. package/src/ui/run-dashboard.ts +5 -33
  207. package/src/ui/run-event-bus.ts +3 -3
  208. package/src/ui/run-snapshot-cache.ts +37 -18
  209. package/src/ui/settings-overlay.ts +13 -21
  210. package/src/ui/snapshot-types.ts +1 -1
  211. package/src/ui/status-colors.ts +0 -2
  212. package/src/ui/terminal-status.ts +1 -1
  213. package/src/ui/theme-adapter.ts +33 -13
  214. package/src/ui/tool-renderers/index.ts +8 -0
  215. package/src/ui/transcript-viewer.ts +37 -13
  216. package/src/ui/widget/index.ts +32 -9
  217. package/src/ui/widget/widget-formatters.ts +62 -4
  218. package/src/ui/widget/widget-model.ts +2 -2
  219. package/src/ui/widget/widget-renderer.ts +1 -1
  220. package/src/utils/conflict-detect.ts +58 -5
  221. package/src/utils/ndjson.ts +2 -2
  222. package/src/utils/socket-path.ts +1 -1
  223. package/src/workflows/workflow-config.ts +1 -1
  224. package/src/worktree/cleanup.ts +1 -1
  225. package/src/worktree/worktree-manager.ts +88 -39
  226. package/workflows/chain.workflow.md +2 -2
  227. package/index.bundle.ts +0 -25
  228. package/scripts/bench-check.mjs +0 -96
  229. package/scripts/bench-cold-start.mjs +0 -216
  230. package/scripts/build-bundle.mjs +0 -92
  231. package/scripts/check-all-skills.ts +0 -294
  232. package/scripts/check-bundle-size.mjs +0 -38
  233. package/scripts/check-bundle-staleness.mjs +0 -97
  234. package/scripts/check-conflict-markers.mjs +0 -91
  235. package/scripts/check-lazy-imports.mjs +0 -28
  236. package/scripts/dev-runner.mjs +0 -106
  237. package/scripts/profile-startup.mjs +0 -125
  238. package/scripts/release-smoke.mjs +0 -74
  239. package/scripts/run-bench.mjs +0 -47
  240. package/scripts/run-real-chain.ts +0 -41
  241. package/scripts/smoke-team-perf.ts +0 -43
  242. package/scripts/test-issue-29-crash.ts +0 -111
  243. package/scripts/test-issue-29-e2e.ts +0 -183
  244. package/scripts/test-issue-29-real-runtime.ts +0 -330
  245. package/scripts/test-issue-29-real-tasks.ts +0 -387
  246. package/scripts/test-issue-29-team-tool.ts +0 -105
  247. package/scripts/test-runner.mjs +0 -84
  248. package/scripts/verify-flicker-fix.ts +0 -109
  249. package/scripts/verify-skill.ts +0 -550
  250. package/scripts/watch-bundle.mjs +0 -259
  251. package/scripts/watchdog-harness.ts +0 -119
  252. package/src/adapters/claude-adapter.ts +0 -23
  253. package/src/adapters/codex-adapter.ts +0 -21
  254. package/src/adapters/cursor-adapter.ts +0 -17
  255. package/src/adapters/export-util.ts +0 -143
  256. package/src/adapters/index.ts +0 -15
  257. package/src/adapters/registry.ts +0 -18
  258. package/src/adapters/types.ts +0 -23
  259. package/src/runtime/code-summary.ts +0 -292
  260. package/src/runtime/cross-extension-rpc.ts +0 -143
  261. package/src/runtime/hidden-handoff.ts +0 -407
  262. package/src/runtime/prose-compressor.ts +0 -162
  263. package/src/subagents/async-entry.ts +0 -1
  264. package/src/subagents/index.ts +0 -3
  265. package/src/subagents/live/control.ts +0 -1
  266. package/src/subagents/live/manager.ts +0 -1
  267. package/src/subagents/live/realtime.ts +0 -1
  268. package/src/subagents/live/session-runtime.ts +0 -1
  269. package/src/subagents/manager.ts +0 -1
  270. package/src/subagents/spawn.ts +0 -1
  271. package/test-integration-check.ts +0 -118
  272. /package/src/runtime/{broker-issuer.ts → broker/broker-issuer.ts} +0 -0
  273. /package/src/runtime/{crew-broker-tokens.ts → broker/crew-broker-tokens.ts} +0 -0
  274. /package/src/runtime/{child-pi-pool.ts → child-pi/child-pi-pool.ts} +0 -0
  275. /package/src/runtime/{compact-pipeline.ts → compaction/compact-pipeline.ts} +0 -0
  276. /package/src/runtime/{compact-stages → compaction/compact-stages}/ansi-strip-stage.ts +0 -0
  277. /package/src/runtime/{compact-stages → compaction/compact-stages}/blank-collapse-stage.ts +0 -0
  278. /package/src/runtime/{compact-stages → compaction/compact-stages}/deduplicate-stage.ts +0 -0
  279. /package/src/runtime/{compact-stages → compaction/compact-stages}/head-snap-stage.ts +0 -0
  280. /package/src/runtime/{compact-stages → compaction/compact-stages}/tail-capture-stage.ts +0 -0
  281. /package/src/runtime/{compact-stages → compaction/compact-stages}/truncation-stage.ts +0 -0
  282. /package/src/runtime/{important-line-classifier.ts → compaction/important-line-classifier.ts} +0 -0
  283. /package/src/runtime/{heartbeat-gradient.ts → heartbeat/heartbeat-gradient.ts} +0 -0
  284. /package/src/runtime/{worker-heartbeat.ts → heartbeat/worker-heartbeat.ts} +0 -0
  285. /package/src/runtime/{worker-startup.ts → heartbeat/worker-startup.ts} +0 -0
  286. /package/src/runtime/{intercom-bridge.ts → live-session/intercom-bridge.ts} +0 -0
  287. /package/src/runtime/{live-control-realtime.ts → live-session/live-control-realtime.ts} +0 -0
  288. /package/src/runtime/{live-extension-bridge.ts → live-session/live-extension-bridge.ts} +0 -0
  289. /package/src/runtime/{live-irc.ts → live-session/live-irc.ts} +0 -0
  290. /package/src/runtime/{live-session-health.ts → live-session/live-session-health.ts} +0 -0
  291. /package/src/runtime/{model-resolver.ts → model/model-resolver.ts} +0 -0
  292. /package/src/runtime/{model-scope.ts → model/model-scope.ts} +0 -0
  293. /package/src/runtime/{output-validator.ts → output/output-validator.ts} +0 -0
  294. /package/src/runtime/{pi-json-output.ts → output/pi-json-output.ts} +0 -0
  295. /package/src/runtime/{progress-event-coalescer.ts → output/progress-event-coalescer.ts} +0 -0
  296. /package/src/runtime/{cancellation-token.ts → process/cancellation-token.ts} +0 -0
  297. /package/src/runtime/{zombie-scanner.ts → process/zombie-scanner.ts} +0 -0
  298. /package/src/runtime/{crash-classification.ts → recovery/crash-classification.ts} +0 -0
  299. /package/src/runtime/{batch-barrier.ts → scheduling/batch-barrier.ts} +0 -0
  300. /package/src/runtime/{global-worker-cap.ts → scheduling/global-worker-cap.ts} +0 -0
  301. /package/src/runtime/{parallel-utils.ts → scheduling/parallel-utils.ts} +0 -0
  302. /package/src/runtime/{scheduler.ts → scheduling/scheduler.ts} +0 -0
  303. /package/src/runtime/{semaphore.ts → scheduling/semaphore.ts} +0 -0
  304. /package/src/runtime/{task-graph.ts → scheduling/task-graph.ts} +0 -0
  305. /package/src/runtime/{verification-worktree.ts → verification/verification-worktree.ts} +0 -0
  306. /package/src/state/{event-reconstructor.ts → event-log/event-reconstructor.ts} +0 -0
  307. /package/src/state/{worker-atomic-writer.ts → event-log/worker-atomic-writer.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
- import { logInternalError } from "../utils/internal-error.ts";
3
- import { redactJsonLine } from "../utils/redaction.ts";
2
+ import { logInternalError } from "../../utils/internal-error.ts";
3
+ import { redactJsonLine } from "../../utils/redaction.ts";
4
4
 
5
5
  export interface DrainableSource {
6
6
  pause(): void;
@@ -35,8 +35,12 @@ export interface JsonlWriter {
35
35
  export function createJsonlWriter(filePath: string | undefined, source: DrainableSource, deps: JsonlWriterDeps = {}): JsonlWriter {
36
36
  if (!filePath) {
37
37
  return {
38
- writeLine() {},
39
- async close() {},
38
+ writeLine() {
39
+ /* no-op: null writer */
40
+ },
41
+ async close() {
42
+ /* no-op: null writer */
43
+ },
40
44
  };
41
45
  }
42
46
 
@@ -46,8 +50,12 @@ export function createJsonlWriter(filePath: string | undefined, source: Drainabl
46
50
  stream = createWriteStream(filePath);
47
51
  } catch {
48
52
  return {
49
- writeLine() {},
50
- async close() {},
53
+ writeLine() {
54
+ /* no-op: stream creation failed */
55
+ },
56
+ async close() {
57
+ /* no-op: stream creation failed */
58
+ },
51
59
  };
52
60
  }
53
61
 
@@ -6,13 +6,13 @@
6
6
  import { crewHooks } from "../runtime/crew-hooks.ts";
7
7
 
8
8
  // Lazy-initialized store and paths
9
- let storeInstance: import("./instinct-store.js").InstinctStore | null = null;
9
+ let storeInstance: import("./stores/instinct-store.js").InstinctStore | null = null;
10
10
  let pathsInstance: typeof import("../utils/paths.js") | null = null;
11
11
 
12
12
  async function getStore() {
13
13
  if (!storeInstance) {
14
14
  // LAZY: defer dynamic import of ./instinct-store.js to its call site.
15
- const { InstinctStore } = await import("./instinct-store.js");
15
+ const { InstinctStore } = await import("./stores/instinct-store.js");
16
16
  // LAZY: defer dynamic import of ../utils/paths.js to its call site.
17
17
  const paths = await import("../utils/paths.js");
18
18
  storeInstance = new InstinctStore(paths.projectCrewRoot(process.cwd()));
@@ -1,13 +1,13 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
5
- import { userCrewRoot } from "../utils/paths.ts";
6
- import { isSafePathId } from "../utils/safe-paths.ts";
7
- import { sharedScanCache } from "../utils/scan-cache.ts";
8
- import { sleepSync } from "../utils/sleep.ts";
9
- import { atomicWriteJson, isSymlinkSafePath, renameWithRetry } from "./atomic-write.ts";
10
- import type { TeamRunManifest } from "./types.ts";
4
+ import { DEFAULT_CACHE, DEFAULT_PATHS } from "../../config/defaults.ts";
5
+ import { userCrewRoot } from "../../utils/paths.ts";
6
+ import { isSafePathId } from "../../utils/safe-paths.ts";
7
+ import { sharedScanCache } from "../../utils/scan-cache.ts";
8
+ import { sleepSync } from "../../utils/sleep.ts";
9
+ import { atomicWriteJson, isSymlinkSafePath, renameWithRetry } from "../atomic-write.ts";
10
+ import type { TeamRunManifest } from "../types.ts";
11
11
 
12
12
  /** Magic bytes prefix for binary registry to prevent deserialization of hostile files. */
13
13
  const BINARY_MAGIC = Buffer.from("PICREW2BIN", "utf-8");
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { redactSecretString, redactSecrets } from "../utils/redaction.ts";
5
- import { resolveRealContainedPath } from "../utils/safe-paths.ts";
6
- import { atomicWriteFile } from "./atomic-write.ts";
7
- import type { ArtifactDescriptor } from "./types.ts";
4
+ import { redactSecretString, redactSecrets } from "../../utils/redaction.ts";
5
+ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
6
+ import { atomicWriteFile } from "../atomic-write.ts";
7
+ import type { ArtifactDescriptor } from "../types.ts";
8
8
 
9
9
  function hashContent(content: string): string {
10
10
  return createHash("sha256").update(content).digest("hex");
@@ -101,6 +101,64 @@ export function cleanupOldArtifacts(artifactsRoot: string, options: ArtifactClea
101
101
  if (!didCleanup) writeCleanupMarker(artifactsRoot, markerFile);
102
102
  }
103
103
 
104
+ /**
105
+ * ST-10: Default TTL for "temporary" retention artifacts (1 hour).
106
+ * Temporary artifacts (progress, notepad) are short-lived and should not
107
+ * accumulate. One hour balances cleanup aggressiveness with the possibility
108
+ * that a long-running task writes periodic progress updates.
109
+ */
110
+ export const TEMPORARY_RETENTION_TTL_MS = 60 * 60 * 1000;
111
+
112
+ /**
113
+ * ST-10: Check whether an artifact descriptor indicates the file should be
114
+ * pruned (expired).
115
+ *
116
+ * Enforces:
117
+ * - `expiresAt`: if set and now ≥ expiresAt → expired.
118
+ * - `retention: "temporary"`: if createdAt age ≥ temporaryTtlMs → expired.
119
+ * ("run" and "project" retentions are handled by run-level prune and the
120
+ * age-based {@link cleanupOldArtifacts}, not by this per-descriptor check.)
121
+ */
122
+ export function isArtifactExpired(desc: ArtifactDescriptor, nowMs: number, temporaryTtlMs: number = TEMPORARY_RETENTION_TTL_MS): boolean {
123
+ // Explicit expiresAt takes precedence.
124
+ if (desc.expiresAt) {
125
+ const expiryMs = Date.parse(desc.expiresAt);
126
+ if (Number.isFinite(expiryMs) && nowMs >= expiryMs) return true;
127
+ }
128
+ // "temporary" retention has a short TTL.
129
+ if (desc.retention === "temporary" && desc.createdAt) {
130
+ const createdMs = Date.parse(desc.createdAt);
131
+ if (Number.isFinite(createdMs) && nowMs - createdMs >= temporaryTtlMs) return true;
132
+ }
133
+ return false;
134
+ }
135
+
136
+ /**
137
+ * ST-10: Prune expired artifacts by enforcing ArtifactDescriptor.retention/expiresAt.
138
+ *
139
+ * Deletes artifact files whose descriptor indicates expiry (expiresAt passed
140
+ * or temporary-retention TTL exceeded). Returns the count of files deleted.
141
+ *
142
+ * Unlike {@link cleanupOldArtifacts} (which is filesystem age-based on raw
143
+ * files), this operates on artifact *metadata* from the manifest's `artifacts[]`
144
+ * and deletes specific expired files via their descriptor `path`.
145
+ */
146
+ export function pruneExpiredArtifacts(descriptors: ArtifactDescriptor[], options?: { now?: number; temporaryTtlMs?: number }): number {
147
+ const nowMs = options?.now ?? Date.now();
148
+ const temporaryTtlMs = options?.temporaryTtlMs ?? TEMPORARY_RETENTION_TTL_MS;
149
+ let deleted = 0;
150
+ for (const desc of descriptors) {
151
+ if (!isArtifactExpired(desc, nowMs, temporaryTtlMs)) continue;
152
+ try {
153
+ fs.unlinkSync(desc.path);
154
+ deleted++;
155
+ } catch {
156
+ // Best-effort: file may already be removed or path may be stale.
157
+ }
158
+ }
159
+ return deleted;
160
+ }
161
+
104
162
  function resolveInside(baseDir: string, relativePath: string): string {
105
163
  // Check if baseDir is a symlink on every call to prevent symlink attacks
106
164
  try {
@@ -1,9 +1,9 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { resolveRealContainedPath } from "../utils/safe-paths.ts";
5
- import { atomicWriteFile, renameWithRetry } from "./atomic-write.ts";
6
- import { withFileLockSync } from "./locks.ts";
4
+ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
5
+ import { atomicWriteFile, renameWithRetry } from "../atomic-write.ts";
6
+ import { withFileLockSync } from "../coordination/locks.ts";
7
7
 
8
8
  const SHA256_HEX = /^[a-f0-9]{64}$/i;
9
9
 
@@ -1,8 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { ManifestSummary } from "../runtime/task-health.ts";
4
- import { computeRunHealth } from "../runtime/task-health.ts";
5
- import { atomicWriteFile } from "./atomic-write.ts";
3
+ import type { ManifestSummary } from "../../runtime/task-health.ts";
4
+ import { computeRunHealth } from "../../runtime/task-health.ts";
5
+ import { atomicWriteFile } from "../atomic-write.ts";
6
6
 
7
7
  // Relative to the crew root (`<cwd>/.crew`). BUG A fix (pts/2 hang
8
8
  // investigation 2026-06-16): this was `.crew/state/health`, which double-joined
@@ -1,8 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { assertSafePathId } from "../utils/safe-paths.ts";
5
- import { atomicWriteFile } from "./atomic-write.ts";
4
+ import { assertSafePathId } from "../../utils/safe-paths.ts";
5
+ import { atomicWriteFile } from "../atomic-write.ts";
6
6
 
7
7
  /**
8
8
  * Represents a learned instinct that guides agent behavior.
@@ -272,4 +272,4 @@ export class InstinctStore {
272
272
  export {
273
273
  getGlobalStorageDir,
274
274
  getProjectStorageDir,
275
- } from "../utils/project-detector.ts";
275
+ } from "../../utils/project-detector.ts";
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { logInternalError } from "../utils/internal-error.ts";
3
- import { atomicWriteJson } from "./atomic-write.ts";
2
+ import { logInternalError } from "../../utils/internal-error.ts";
3
+ import { atomicWriteJson } from "../atomic-write.ts";
4
4
 
5
5
  // ── Types ────────────────────────────────────────────────────────────────
6
6
 
@@ -1,10 +1,10 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { projectCrewRoot } from "../utils/paths.ts";
5
- import { atomicWriteJson } from "./atomic-write.ts";
6
- import { withFileLockSync } from "./locks.ts";
7
- import type { TeamTaskState } from "./types.ts";
4
+ import { projectCrewRoot } from "../../utils/paths.ts";
5
+ import { atomicWriteJson } from "../atomic-write.ts";
6
+ import { withFileLockSync } from "../coordination/locks.ts";
7
+ import type { TeamTaskState } from "../types.ts";
8
8
 
9
9
  const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
10
10
 
@@ -1,9 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { projectCrewRoot } from "../utils/paths.ts";
4
- import { assertSafePathId } from "../utils/safe-paths.ts";
5
- import { atomicWriteFile } from "./atomic-write.ts";
6
- import type { TeamRunManifest, TeamTaskState } from "./types.ts";
3
+ import { projectCrewRoot } from "../../utils/paths.ts";
4
+ import { assertSafePathId } from "../../utils/safe-paths.ts";
5
+ import { atomicWriteFile } from "../atomic-write.ts";
6
+ import type { TeamRunManifest, TeamTaskState } from "../types.ts";
7
7
 
8
8
  export interface RunGraphNode {
9
9
  id: string;
@@ -1,8 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { projectCrewRoot } from "../utils/paths.ts";
4
- import { assertSafePathId } from "../utils/safe-paths.ts";
5
- import { atomicWriteJson, readJsonFile } from "./atomic-write.ts";
3
+ import { projectCrewRoot } from "../../utils/paths.ts";
4
+ import { assertSafePathId } from "../../utils/safe-paths.ts";
5
+ import { atomicWriteJson, readJsonFile } from "../atomic-write.ts";
6
6
  import { loadRunManifestById } from "./state-store.ts";
7
7
 
8
8
  /**
@@ -1,22 +1,29 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as fsp from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
5
- import { errors } from "../errors.ts";
6
- import type { TeamConfig } from "../teams/team-config.ts";
7
- import { createRunId, createTaskId } from "../utils/ids.ts";
8
- import { logInternalError } from "../utils/internal-error.ts";
9
- import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
10
- import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
11
- import { toPiSessionId } from "../utils/session-utils.ts";
12
- import type { WorkflowConfig } from "../workflows/workflow-config.ts";
4
+ import { DEFAULT_CACHE, DEFAULT_PATHS } from "../../config/defaults.ts";
5
+ import { errors } from "../../errors.ts";
6
+ import type { TeamConfig } from "../../teams/team-config.ts";
7
+ import { createRunId, createTaskId } from "../../utils/ids.ts";
8
+ import { logInternalError } from "../../utils/internal-error.ts";
9
+ import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
10
+ import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../../utils/safe-paths.ts";
11
+ import { toPiSessionId } from "../../utils/session-utils.ts";
12
+ import type { WorkflowConfig } from "../../workflows/workflow-config.ts";
13
+ import {
14
+ atomicWriteJson,
15
+ atomicWriteJsonAsync,
16
+ atomicWriteJsonCoalesced,
17
+ flushPendingAtomicWrites,
18
+ readJsonFile,
19
+ } from "../atomic-write.ts";
20
+ import { canTransitionRunStatus, isTeamTaskStatus } from "../contracts.ts";
21
+ import { withRunLock, withRunLockSync } from "../coordination/locks.ts";
22
+ import { appendEvent } from "../event-log/event-log.ts";
23
+ import { reconstructTasksFromEvents } from "../event-log/event-reconstructor.ts";
24
+ import type { TeamRunManifest, TeamTaskState } from "../types.ts";
25
+ import { CURRENT_SCHEMA_VERSION, CURRENT_TASKS_SCHEMA_VERSION } from "../types.ts";
13
26
  import { unregisterActiveRun } from "./active-run-registry.ts";
14
- import { atomicWriteJson, atomicWriteJsonAsync, atomicWriteJsonCoalesced, flushPendingAtomicWrites, readJsonFile } from "./atomic-write.ts";
15
- import { canTransitionRunStatus } from "./contracts.ts";
16
- import { appendEvent } from "./event-log.ts";
17
- import { withRunLock, withRunLockSync } from "./locks.ts";
18
- import type { TeamRunManifest, TeamTaskState } from "./types.ts";
19
- import { CURRENT_SCHEMA_VERSION } from "./types.ts";
20
27
 
21
28
  /**
22
29
  * stat() the manifest with a brief retry on Windows for the AV-scan window.
@@ -424,7 +431,32 @@ export async function saveRunManifestAsync(manifest: TeamRunManifest): Promise<v
424
431
  });
425
432
  }
426
433
 
434
+ /**
435
+ * ST-4: Defense-in-depth guard — refuse to persist an empty tasks array over
436
+ * a previously non-empty tasks file. Prevents the cascade where a corrupt
437
+ * tasks.json (→ [] on load via readJsonFile catching SyntaxError) is then
438
+ * persisted as [], permanently destroying the data. If the on-disk file has
439
+ * tasks and the incoming array is empty, we log a warning and refuse.
440
+ *
441
+ * @returns true if the write should proceed, false if it was refused.
442
+ */
443
+ function shouldPersistTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]): boolean {
444
+ if (tasks.length > 0) return true;
445
+ const existing = extractTaskArray(readJsonFile<unknown>(manifest.tasksPath));
446
+ if (existing.length > 0) {
447
+ console.warn(
448
+ `[state-store] refusing to persist empty tasks over ${existing.length} existing task(s) — possible corrupt-load cascade (runId=${manifest.runId})`,
449
+ );
450
+ return false;
451
+ }
452
+ return true;
453
+ }
454
+
427
455
  export function saveRunTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]): void {
456
+ // ST-4: refuse to persist [] over a previously-non-empty tasks file.
457
+ // Prevents the cascade where a corrupt tasks.json (→ [] on load) is then
458
+ // persisted as [], permanently destroying the data.
459
+ if (!shouldPersistTasks(manifest, tasks)) return;
428
460
  // FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
429
461
  invalidateRunCache(manifest.stateRoot);
430
462
 
@@ -501,6 +533,8 @@ export function saveRunTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]):
501
533
  */
502
534
  /** @internal */
503
535
  export function saveRunTasksCoalesced(manifest: TeamRunManifest, tasks: TeamTaskState[], skipCoalesce: boolean = false): void {
536
+ // ST-4: refuse to persist [] over a previously-non-empty tasks file.
537
+ if (!shouldPersistTasks(manifest, tasks)) return;
504
538
  // FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
505
539
  invalidateRunCache(manifest.stateRoot);
506
540
  try {
@@ -512,6 +546,8 @@ export function saveRunTasksCoalesced(manifest: TeamRunManifest, tasks: TeamTask
512
546
  }
513
547
 
514
548
  export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<void> {
549
+ // ST-4: refuse to persist [] over a previously-non-empty tasks file.
550
+ if (!shouldPersistTasks(manifest, tasks)) return;
515
551
  // FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
516
552
  invalidateRunCache(manifest.stateRoot);
517
553
  try {
@@ -733,6 +769,185 @@ async function readJsonFileAsync<T>(filePath: string): Promise<T | undefined> {
733
769
  }
734
770
  }
735
771
 
772
+ /**
773
+ * ST-4: Rename a corrupt file to a quarantine path (`.corrupt-<ts>`) so it is
774
+ * preserved for debugging but no longer read as the primary source of truth.
775
+ */
776
+ function quarantineCorruptFile(filePath: string): void {
777
+ try {
778
+ fs.renameSync(filePath, `${filePath}.corrupt-${Date.now()}`);
779
+ } catch {
780
+ // Best-effort — if rename fails (file already gone, permission, etc.),
781
+ // we still proceed with reconstruction from the event log.
782
+ }
783
+ }
784
+
785
+ /**
786
+ * ST-4: Convert event-reconstructor output into TeamTaskState[].
787
+ * Reconstructed tasks carry lifecycle data (id, status, timing) from the
788
+ * event log; auxiliary fields (role, agent, title) are filled with defaults
789
+ * since they are not present in lifecycle events.
790
+ */
791
+ function reconstructTasksFromEventLog(eventsPath: string, runId: string): TeamTaskState[] {
792
+ try {
793
+ const result = reconstructTasksFromEvents(eventsPath);
794
+ const tasks: TeamTaskState[] = [];
795
+ for (const [, rt] of result.tasks) {
796
+ tasks.push({
797
+ id: rt.id,
798
+ runId,
799
+ role: "unknown",
800
+ agent: "unknown",
801
+ title: "reconstructed from events",
802
+ status: isTeamTaskStatus(rt.status) ? rt.status : "queued",
803
+ dependsOn: [],
804
+ cwd: "",
805
+ startedAt: rt.startedAt,
806
+ finishedAt: rt.finishedAt,
807
+ error: rt.error,
808
+ segment: rt.segment,
809
+ diagnostics: rt.diagnostics,
810
+ metrics: rt.metrics,
811
+ });
812
+ }
813
+ return tasks;
814
+ } catch {
815
+ return [];
816
+ }
817
+ }
818
+
819
+ /**
820
+ * ST-4: Load tasks.json with corruption recovery (sync path).
821
+ *
822
+ * Distinguishes:
823
+ * - ENOENT / ENOTDIR → legitimate empty → [] (NOT quarantined).
824
+ * - SyntaxError (parse failure) → corrupt → quarantine `.corrupt-<ts>` AND
825
+ * reconstruct from events.jsonl. If reconstruction yields tasks, persist
826
+ * them so subsequent loads see a valid file.
827
+ * - Non-array JSON (e.g. `{}`) → corrupt → same as SyntaxError.
828
+ * - Valid array → return as-is.
829
+ */
830
+
831
+ /**
832
+ * ST-9: Extract the task array from a tasks.json payload.
833
+ *
834
+ * Accepts both the v0 legacy bare-array format and the v1+ envelope
835
+ * `{ schemaVersion, tasks }`. Returns [] for unrecognized shapes.
836
+ */
837
+ function extractTaskArray(raw: unknown): TeamTaskState[] {
838
+ if (Array.isArray(raw)) return raw as TeamTaskState[];
839
+ if (raw !== null && typeof raw === "object" && "tasks" in raw) {
840
+ const envelope = raw as { tasks?: unknown };
841
+ if (Array.isArray(envelope.tasks)) return envelope.tasks as TeamTaskState[];
842
+ }
843
+ return [];
844
+ }
845
+
846
+ /**
847
+ * ST-9: Whether `parsed` is a recognizable tasks.json shape (v0 bare array
848
+ * or v1+ envelope). Used to distinguish legitimate formats from corruption.
849
+ */
850
+ function isRecognizableTasksPayload(parsed: unknown): boolean {
851
+ if (Array.isArray(parsed)) return true;
852
+ if (parsed !== null && typeof parsed === "object" && "tasks" in parsed) {
853
+ return Array.isArray((parsed as { tasks?: unknown }).tasks);
854
+ }
855
+ return false;
856
+ }
857
+
858
+ /**
859
+ * ST-9: Version-check + migration hook for tasks.json.
860
+ *
861
+ * tasks.json has two on-disk shapes:
862
+ * - v0 (current): bare JSON array `TeamTaskState[]` — what saveRunTasks*
863
+ * write today (backward-compatible; no schemaVersion envelope).
864
+ * - v1+ (future): envelope `{ schemaVersion: number, tasks: TeamTaskState[] }`
865
+ * — read-supported defensively for a future write-side switch.
866
+ *
867
+ * This detects the shape and returns the task array. v0 needs NO migration
868
+ * (it IS the current write format). For v1+ envelopes, a schemaVersion
869
+ * mismatch warns (mirroring the manifest check). Future breaking changes
870
+ * add real migration logic here.
871
+ */
872
+ function migrateTasksFile(parsed: unknown, runId: string): TeamTaskState[] {
873
+ // v0 current: bare array (no schemaVersion envelope) — what writers produce.
874
+ if (Array.isArray(parsed)) {
875
+ // v0 bare array is the CURRENT write format (saveRunTasks* write the
876
+ // array directly, by design — backward compat). Nothing to migrate:
877
+ // return as-is. (v1+ envelope read-support below is defensive, for a
878
+ // future write-side switch.) Do NOT warn here — it would fire for 100%
879
+ // of runs on every load and flood the UI on startup.
880
+ return parsed as TeamTaskState[];
881
+ }
882
+ // v1+ envelope: { schemaVersion, tasks }.
883
+ if (parsed !== null && typeof parsed === "object" && "tasks" in parsed) {
884
+ const envelope = parsed as { schemaVersion?: unknown; tasks?: unknown };
885
+ const detected = typeof envelope.schemaVersion === "number" ? envelope.schemaVersion : 0;
886
+ if (detected !== CURRENT_TASKS_SCHEMA_VERSION) {
887
+ console.warn(
888
+ `[state-store] tasks.json schemaVersion mismatch: expected ${CURRENT_TASKS_SCHEMA_VERSION}, got ${detected}. Run ${runId} may be incompatible.`,
889
+ );
890
+ }
891
+ }
892
+ return extractTaskArray(parsed);
893
+ }
894
+ function loadTasksWithRecovery(tasksPath: string, eventsPath: string, runId: string): TeamTaskState[] {
895
+ let content: string;
896
+ try {
897
+ content = fs.readFileSync(tasksPath, "utf-8");
898
+ } catch {
899
+ // ENOENT / ENOTDIR / other read errors → empty (retry loop handles
900
+ // transient instability; ENOENT is a legitimate empty run).
901
+ return [];
902
+ }
903
+ let parsed: unknown;
904
+ try {
905
+ parsed = JSON.parse(content);
906
+ } catch {
907
+ // SyntaxError — corrupt file.
908
+ quarantineCorruptFile(tasksPath);
909
+ const reconstructed = reconstructTasksFromEventLog(eventsPath, runId);
910
+ if (reconstructed.length > 0) atomicWriteJson(tasksPath, reconstructed);
911
+ return reconstructed;
912
+ }
913
+ if (!isRecognizableTasksPayload(parsed)) {
914
+ // Neither v0 bare array nor v1+ envelope (e.g. `{}`) — corrupt.
915
+ quarantineCorruptFile(tasksPath);
916
+ const reconstructed = reconstructTasksFromEventLog(eventsPath, runId);
917
+ if (reconstructed.length > 0) atomicWriteJson(tasksPath, reconstructed);
918
+ return reconstructed;
919
+ }
920
+ return migrateTasksFile(parsed, runId);
921
+ }
922
+
923
+ /**
924
+ * ST-4: async twin of {@link loadTasksWithRecovery}.
925
+ */
926
+ async function loadTasksWithRecoveryAsync(tasksPath: string, eventsPath: string, runId: string): Promise<TeamTaskState[]> {
927
+ let content: string;
928
+ try {
929
+ content = await fs.promises.readFile(tasksPath, "utf-8");
930
+ } catch {
931
+ return [];
932
+ }
933
+ let parsed: unknown;
934
+ try {
935
+ parsed = JSON.parse(content);
936
+ } catch {
937
+ quarantineCorruptFile(tasksPath);
938
+ const reconstructed = reconstructTasksFromEventLog(eventsPath, runId);
939
+ if (reconstructed.length > 0) await atomicWriteJsonAsync(tasksPath, reconstructed);
940
+ return reconstructed;
941
+ }
942
+ if (!isRecognizableTasksPayload(parsed)) {
943
+ quarantineCorruptFile(tasksPath);
944
+ const reconstructed = reconstructTasksFromEventLog(eventsPath, runId);
945
+ if (reconstructed.length > 0) await atomicWriteJsonAsync(tasksPath, reconstructed);
946
+ return reconstructed;
947
+ }
948
+ return migrateTasksFile(parsed, runId);
949
+ }
950
+
736
951
  /**
737
952
  * Load a run manifest and its tasks by runId.
738
953
  * WARNING: This function provides best-effort consistency only. The sentinel-based
@@ -813,7 +1028,7 @@ export function loadRunManifestById(cwd: string, runId: string): { manifest: Tea
813
1028
  const freshStat = fs.statSync(manifestPath);
814
1029
  manifest = readJsonFile<TeamRunManifest>(manifestPath);
815
1030
  const freshTasksStat = fs.existsSync(tasksPath) ? fs.statSync(tasksPath) : undefined;
816
- tasks = readJsonFile<TeamTaskState[]>(tasksPath) ?? [];
1031
+ tasks = loadTasksWithRecovery(tasksPath, manifest?.eventsPath ?? path.join(stateRoot, "events.jsonl"), manifest?.runId ?? runId);
817
1032
  // If size/mtime didn't change between stat and read, we're consistent.
818
1033
  if (
819
1034
  freshStat.mtimeMs === manifestStat.mtimeMs &&
@@ -933,7 +1148,11 @@ export async function loadRunManifestByIdAsync(
933
1148
  const freshStat = await fs.promises.stat(manifestPath);
934
1149
  manifest = await readJsonFileAsync<TeamRunManifest>(manifestPath);
935
1150
  const freshTasksStat = await fs.promises.stat(tasksPath).catch(() => undefined);
936
- tasks = (await readJsonFileAsync<TeamTaskState[]>(tasksPath)) ?? [];
1151
+ tasks = await loadTasksWithRecoveryAsync(
1152
+ tasksPath,
1153
+ manifest?.eventsPath ?? path.join(stateRoot, "events.jsonl"),
1154
+ manifest?.runId ?? runId,
1155
+ );
937
1156
  // If size/mtime didn't change between stat and read, we're consistent.
938
1157
  if (
939
1158
  freshStat.mtimeMs === manifestStat.mtimeMs &&
@@ -1,9 +1,9 @@
1
- import type { CrashClass } from "../runtime/crash-classification.ts";
2
1
  import type { CrewAgentProgress } from "../runtime/crew-agent-runtime.ts";
3
- import type { WorkerHeartbeatState } from "../runtime/worker-heartbeat.ts";
2
+ import type { WorkerHeartbeatState } from "../runtime/heartbeat/worker-heartbeat.ts";
3
+ import type { CrashClass } from "../runtime/recovery/crash-classification.ts";
4
4
  import type { TeamRunStatus, TeamTaskStatus } from "./contracts.ts";
5
+ import type { TaskClaimState } from "./coordination/task-claims.ts";
5
6
  import type { CoherenceMark, RolloutEntry } from "./decision-ledger.ts";
6
- import type { TaskClaimState } from "./task-claims.ts";
7
7
 
8
8
  export type { TeamRunStatus, TeamTaskStatus } from "./contracts.ts";
9
9
  export type { CoherenceMark, CrewAgentProgress, RolloutEntry };
@@ -183,6 +183,30 @@ export interface CrewAttentionEventData {
183
183
  */
184
184
  export const CURRENT_SCHEMA_VERSION = 1 as const;
185
185
 
186
+ /**
187
+ * ST-9: schemaVersion sentinel for tasks.json (and future goal-state/mailbox
188
+ * files). Mirrors {@link CURRENT_SCHEMA_VERSION} for the manifest.
189
+ *
190
+ * tasks.json has two on-disk shapes:
191
+ * - v0 (legacy): bare JSON array `TeamTaskState[]` — no schemaVersion.
192
+ * - v1+ (current): envelope `{ schemaVersion, tasks }` — see {@link TasksFileData}.
193
+ *
194
+ * The reader detects the shape, warns on version mismatch, and applies
195
+ * migration logic in state-store.ts (the structured migration hook).
196
+ */
197
+ export const CURRENT_TASKS_SCHEMA_VERSION = 1 as const;
198
+
199
+ /**
200
+ * ST-9: Versioned envelope for tasks.json on disk.
201
+ * Writers produce this shape; readers accept it OR the legacy v0 bare array.
202
+ * `TeamTaskState` is defined later in this module but interface declarations
203
+ * are type-hoisted so the forward reference is valid.
204
+ */
205
+ export interface TasksFileData {
206
+ schemaVersion: typeof CURRENT_TASKS_SCHEMA_VERSION;
207
+ tasks: TeamTaskState[];
208
+ }
209
+
186
210
  export interface TeamRunManifest {
187
211
  schemaVersion: typeof CURRENT_SCHEMA_VERSION;
188
212
  runId: string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * PR-G3 (UI-3) — single shared Component contract for the UI layer.
3
+ *
4
+ * Five parallel "Component" shapes previously existed across src/ui/:
5
+ *
6
+ * - `RenderableComponent` (layout-primitives.ts)
7
+ * - `WidgetComponent` (widget/index.ts)
8
+ * - `DashboardComponent` (run-dashboard.ts)
9
+ * - `type Component` (transcript-viewer.ts)
10
+ * - `type Component = Container|Text` (tool-renderers/index.ts)
11
+ *
12
+ * They all share the same two core methods (render + invalidate) but could
13
+ * not be used interchangeably, which blocked polymorphic dispatch and shared
14
+ * testing. This module names that shared contract so any UI component can be
15
+ * treated uniformly.
16
+ *
17
+ * This is purely additive: every existing component already structurally
18
+ * satisfies these interfaces (render + invalidate are required everywhere;
19
+ * handleInput is present on the interactive ones). Adapting the existing
20
+ * declarations to `extends`/alias these types changes NO runtime behavior —
21
+ * it only makes the relationship explicit and compiler-enforced.
22
+ *
23
+ * Note: @earendil-works/pi-tui also declares a `Component` interface with the
24
+ * same render+invalidate core (plus an optional handleInput). `CrewComponent`
25
+ * is a structural supertype of it, so pi-tui components opt in automatically.
26
+ */
27
+
28
+ /** Minimum shared contract every UI component satisfies. */
29
+ export interface CrewComponent {
30
+ /**
31
+ * Drop cached render state so the next `render` recomputes from fresh data.
32
+ * Called on theme changes, resize, or any state mutation that invalidates
33
+ * previously produced lines.
34
+ */
35
+ invalidate(): void;
36
+ /**
37
+ * Produce rendered lines for the given terminal width.
38
+ * @param width - available terminal width in cells
39
+ * @returns one string per output row (already padded/ANSI-colored)
40
+ */
41
+ render(width: number): string[];
42
+ }
43
+
44
+ /**
45
+ * A component that also accepts raw key/input data. Interactive viewers
46
+ * (RunDashboard, DurableTranscriptViewer) and overlays (ConfirmOverlay,
47
+ * MailboxDetailOverlay, AgentPickerOverlay) implement this; purely
48
+ * presentational components (Container, Text, Spacer, Box) do not.
49
+ */
50
+ export interface InteractiveComponent extends CrewComponent {
51
+ /** Optional stable id (used by overlay stacks for lookup/debug). */
52
+ id?: string;
53
+ /** Consume a raw input chunk (a keypress escape sequence). */
54
+ handleInput(data: string): void;
55
+ }