pi-crew 0.9.50 → 0.9.52

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 (609) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +216 -20
  3. package/dist/build-meta.json +2222 -1761
  4. package/dist/index.mjs +18636 -17603
  5. package/docs/actions-reference.md +431 -1
  6. package/package.json +19 -9
  7. package/schema.json +41 -0
  8. package/scripts/check-bundle-size.mjs +38 -0
  9. package/scripts/test-runner.mjs +11 -1
  10. package/skills/distill-software/SKILL.md +24 -1
  11. package/dist/index.mjs.map +0 -7
  12. package/docs/FEATURE_INTAKE.md +0 -126
  13. package/docs/HARNESS.md +0 -87
  14. package/docs/HARNESS_BACKLOG.md +0 -89
  15. package/docs/PROJECT_REVIEW.md +0 -270
  16. package/docs/PROJECT_REVIEW_FIXES.md +0 -342
  17. package/docs/PROJECT_REVIEW_ROUND4.md +0 -156
  18. package/docs/PROJECT_REVIEW_ROUND5.md +0 -86
  19. package/docs/REVIEW-FINDINGS-2026-06 +0 -357
  20. package/docs/REVIEW-FINDINGS-2026-07-CORE.md +0 -139
  21. package/docs/REVIEW-FINDINGS-2026-07.md +0 -125
  22. package/docs/TEST_MATRIX.md +0 -51
  23. package/docs/archive/README.md +0 -91
  24. package/docs/archive/followup-review-round3-2026-05-12.md +0 -342
  25. package/docs/archive/followup-review-round4-2026-05-13.md +0 -107
  26. package/docs/archive/pi-crew-bugs.md +0 -961
  27. package/docs/archive/pi-crew-test-final.md +0 -120
  28. package/docs/archive/pi-crew-test-results.md +0 -260
  29. package/docs/archive/pi-crew-test-round2.md +0 -136
  30. package/docs/archive/pi-crew-test-round4.md +0 -100
  31. package/docs/archive/pi-crew-test-round5.md +0 -70
  32. package/docs/archive/pi-crew-test-round6.md +0 -110
  33. package/docs/archive/pi-crew-v0.5.10-audit-fix-plan.md +0 -60
  34. package/docs/archive/pi-crew-v0.5.11-audit-fix-plan.md +0 -92
  35. package/docs/archive/pi-crew-v0.5.12-audit-fix-plan.md +0 -76
  36. package/docs/archive/pi-crew-v0.5.13-audit-fix-plan.md +0 -75
  37. package/docs/archive/pi-crew-v0.5.14-audit-fix-plan.md +0 -75
  38. package/docs/archive/pi-crew-v0.5.16-audit-fix-plan.md +0 -35
  39. package/docs/archive/pi-crew-v0.5.17-audit-fix-plan.md +0 -80
  40. package/docs/archive/pi-crew-v0.5.5-audit-fix-plan.md +0 -133
  41. package/docs/archive/pi-crew-v0.5.9-audit-fix-plan.md +0 -88
  42. package/docs/archive/pi-mono-opportunities.md +0 -969
  43. package/docs/archive/pi-mono-review.md +0 -291
  44. package/docs/archive/pi-subagent4-comparison.md +0 -261
  45. package/docs/archive/pi-subagents3-deep-analysis.md +0 -508
  46. package/docs/bugs/bug-021-notification-badge-counter-misleading.md +0 -293
  47. package/docs/bugs/bug-023-chain-windows-path-resolution.md +0 -106
  48. package/docs/bugs/bug-quota-display-truncation.md +0 -223
  49. package/docs/bugs/cross-session-notification-leakage.md +0 -82
  50. package/docs/code-review-2026-05-11.md +0 -592
  51. package/docs/coding-agent-optimization.md +0 -268
  52. package/docs/comparison-pi-subagents-vs-pi-crew.md +0 -303
  53. package/docs/decisions/0001-durable-state.md +0 -41
  54. package/docs/decisions/0002-child-process-for-async.md +0 -42
  55. package/docs/decisions/0003-depth-guard.md +0 -36
  56. package/docs/decisions/0004-execfile-over-exec.md +0 -34
  57. package/docs/decisions/0005-no-parameter-properties.md +0 -49
  58. package/docs/decisions/0006-publish-bundled-esm.md +0 -63
  59. package/docs/decisions/0007-active-run-binary-index.md +0 -54
  60. package/docs/decisions/0008-child-pi-warm-pool.md +0 -61
  61. package/docs/decisions/2026-07-21-broker-phase4-default-on.md +0 -77
  62. package/docs/decisions/2026-07-21-broker-windows-perms.md +0 -91
  63. package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +0 -99
  64. package/docs/decisions/2026-07-24-oidc-trusted-publishing.md +0 -112
  65. package/docs/decisions/README.md +0 -26
  66. package/docs/deep-review-report.md +0 -384
  67. package/docs/distillation/cybersecurity-patterns.md +0 -294
  68. package/docs/feature-analysis-subagent4.md +0 -305
  69. package/docs/fix-plan-disabletools-exit-null.md +0 -219
  70. package/docs/fixes/BATCH_A_H1_H2.md +0 -86
  71. package/docs/fixes/bug-006-foreground-cancel-concurrent.md +0 -78
  72. package/docs/fixes/bug-007-async-notifier-stale-ctx.md +0 -112
  73. package/docs/fixes/bug-008-child-process-silent-timeout.md +0 -100
  74. package/docs/fixes/bug-009-executor-yield-limit-needs-attention.md +0 -75
  75. package/docs/fixes/bug-010-child-process-api-key-filtered.md +0 -109
  76. package/docs/fixes/bug-011-spawn-pi-enoent.md +0 -92
  77. package/docs/fixes/bug-012-essential-env-stripped.md +0 -89
  78. package/docs/fixes/bug-013-background-runner-death.md +0 -84
  79. package/docs/fixes/bug-014-infinite-retry-loop-needs-attention.md +0 -82
  80. package/docs/fixes/bug-015-background-runner-sigterm.md +0 -65
  81. package/docs/fixes/bug-017-background-runner-session-shutdown.md +0 -66
  82. package/docs/fixes/bug-017-background-runner-sigkill-double-fork.md +0 -28
  83. package/docs/fixes/bug-018-child-pi-worker-stdin-hang.md +0 -61
  84. package/docs/fixes/bug-019-phantom-runs-temp-workspace.md +0 -52
  85. package/docs/fixes/bug-020-infinite-retry-loop-needs-attention.md +0 -47
  86. package/docs/fixes/v0.9.10/locks-fix-verify.md +0 -3
  87. package/docs/fixes/v0.9.10/smoke-test.md +0 -12
  88. package/docs/followup-plan-2026-05-12.md +0 -463
  89. package/docs/followup-review-2026-05-12.md +0 -297
  90. package/docs/goals.md +0 -59
  91. package/docs/implementation-plan-top3.md +0 -333
  92. package/docs/issue-29-analysis.md +0 -189
  93. package/docs/live-mailbox-runtime.md +0 -36
  94. package/docs/migration/atomic-write-v2-migration.md +0 -299
  95. package/docs/next-upgrade-roadmap.md +0 -808
  96. package/docs/oh-my-pi-research.md +0 -509
  97. package/docs/optimization-plan-2026-07.md +0 -366
  98. package/docs/optimization-plan.md +0 -644
  99. package/docs/patterns/command-agent-skill.md +0 -71
  100. package/docs/perf/baseline-2026-05.md +0 -113
  101. package/docs/perf/final-report-2026-05.md +0 -206
  102. package/docs/perf/optimization-plan-2026-07-verified.md +0 -396
  103. package/docs/perf/performance-audit-report-2026-07.md +0 -948
  104. package/docs/perf/performance-review-2026-07.md +0 -287
  105. package/docs/perf/sprint-1-report.md +0 -71
  106. package/docs/perf/sprint-2-report.md +0 -81
  107. package/docs/perf/sprint-2.5-report.md +0 -53
  108. package/docs/perf/sprint-3-report.md +0 -36
  109. package/docs/perf/sprint-4-report.md +0 -47
  110. package/docs/perf/sprint-5-report.md +0 -51
  111. package/docs/perf/sprint-6-report.md +0 -94
  112. package/docs/perf/sprint-7-report.md +0 -74
  113. package/docs/perf/upgrade-plan-2026-05.md +0 -147
  114. package/docs/performance-optimization-execution-plan.md +0 -520
  115. package/docs/phase4-triage.md +0 -58
  116. package/docs/pi-crew-investigation-report.md +0 -411
  117. package/docs/product/README.md +0 -31
  118. package/docs/product/platform.md +0 -27
  119. package/docs/product/runtime-safety.md +0 -37
  120. package/docs/product/team-run.md +0 -39
  121. package/docs/product/team-tool.md +0 -37
  122. package/docs/runtime-analysis-child-vs-live.md +0 -171
  123. package/docs/runtime-migration-in-process-analysis.md +0 -250
  124. package/docs/skills/REFERENCE.md +0 -138
  125. package/docs/stories/README.md +0 -32
  126. package/docs/stories/US-DEPS-major-upgrade.md +0 -62
  127. package/docs/stories/backlog.md +0 -36
  128. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +0 -1270
  129. package/docs/templates/decision.md +0 -27
  130. package/docs/templates/story.md +0 -44
  131. package/docs/templates/validation-report.md +0 -32
  132. package/docs/ui-optimization-plan.md +0 -447
  133. package/src/adapters/claude-adapter.ts +0 -23
  134. package/src/adapters/codex-adapter.ts +0 -21
  135. package/src/adapters/cursor-adapter.ts +0 -17
  136. package/src/adapters/export-util.ts +0 -143
  137. package/src/adapters/index.ts +0 -15
  138. package/src/adapters/registry.ts +0 -18
  139. package/src/adapters/types.ts +0 -23
  140. package/src/agents/agent-config.ts +0 -194
  141. package/src/agents/agent-search.ts +0 -98
  142. package/src/agents/agent-serializer.ts +0 -38
  143. package/src/agents/discover-agents.ts +0 -623
  144. package/src/benchmark/benchmark-runner.ts +0 -313
  145. package/src/benchmark/feedback-loop.ts +0 -73
  146. package/src/config/config.ts +0 -1269
  147. package/src/config/defaults.ts +0 -193
  148. package/src/config/drift-detector.ts +0 -290
  149. package/src/config/markers.ts +0 -330
  150. package/src/config/resilient-parser.ts +0 -117
  151. package/src/config/role-tools.ts +0 -118
  152. package/src/config/suggestions.ts +0 -75
  153. package/src/config/types.ts +0 -278
  154. package/src/errors.ts +0 -194
  155. package/src/extension/action-suggestions.ts +0 -117
  156. package/src/extension/async-notifier.ts +0 -179
  157. package/src/extension/autonomous-policy.ts +0 -209
  158. package/src/extension/command-completions.ts +0 -120
  159. package/src/extension/context-status-injection.ts +0 -185
  160. package/src/extension/crew-autocomplete.ts +0 -131
  161. package/src/extension/crew-cleanup.ts +0 -168
  162. package/src/extension/crew-input-router.ts +0 -93
  163. package/src/extension/crew-shortcuts.ts +0 -87
  164. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  165. package/src/extension/crew-vibes/config.ts +0 -195
  166. package/src/extension/crew-vibes/figures.ts +0 -101
  167. package/src/extension/crew-vibes/font-detect.ts +0 -71
  168. package/src/extension/crew-vibes/footer.ts +0 -292
  169. package/src/extension/crew-vibes/index.ts +0 -432
  170. package/src/extension/crew-vibes/provider-usage.ts +0 -396
  171. package/src/extension/crew-vibes/render.ts +0 -206
  172. package/src/extension/crew-vibes/speed.ts +0 -286
  173. package/src/extension/cross-extension-rpc.ts +0 -350
  174. package/src/extension/help.ts +0 -62
  175. package/src/extension/import-index.ts +0 -74
  176. package/src/extension/knowledge-injection.ts +0 -402
  177. package/src/extension/management.ts +0 -571
  178. package/src/extension/message-renderers.ts +0 -108
  179. package/src/extension/notification-router.ts +0 -152
  180. package/src/extension/notification-sink.ts +0 -54
  181. package/src/extension/pi-api.ts +0 -56
  182. package/src/extension/plan-orchestrate.ts +0 -302
  183. package/src/extension/project-init.ts +0 -170
  184. package/src/extension/register.ts +0 -113
  185. package/src/extension/registration/artifact-cleanup.ts +0 -20
  186. package/src/extension/registration/command-registration.ts +0 -58
  187. package/src/extension/registration/command-utils.ts +0 -58
  188. package/src/extension/registration/commands.ts +0 -1228
  189. package/src/extension/registration/compaction-guard.ts +0 -333
  190. package/src/extension/registration/context-builder.ts +0 -135
  191. package/src/extension/registration/crash-recovery-cache.ts +0 -54
  192. package/src/extension/registration/foreground-run-controller.ts +0 -238
  193. package/src/extension/registration/hook-registration.ts +0 -119
  194. package/src/extension/registration/lazy-configurers.ts +0 -124
  195. package/src/extension/registration/lifecycle-handlers.ts +0 -923
  196. package/src/extension/registration/lifecycle.ts +0 -259
  197. package/src/extension/registration/observability.ts +0 -322
  198. package/src/extension/registration/registration-types.ts +0 -158
  199. package/src/extension/registration/runtime-cleanup.ts +0 -230
  200. package/src/extension/registration/subagent-helpers.ts +0 -125
  201. package/src/extension/registration/subagent-manager-setup.ts +0 -330
  202. package/src/extension/registration/subagent-tools.ts +0 -456
  203. package/src/extension/registration/team-tool.ts +0 -220
  204. package/src/extension/registration/tool-registration.ts +0 -50
  205. package/src/extension/registration/ui.ts +0 -172
  206. package/src/extension/registration/viewers.ts +0 -121
  207. package/src/extension/registration/wire-cross-extension.ts +0 -37
  208. package/src/extension/result-watcher.ts +0 -139
  209. package/src/extension/rpc-hmac.ts +0 -256
  210. package/src/extension/run-bundle-schema.ts +0 -104
  211. package/src/extension/run-export.ts +0 -97
  212. package/src/extension/run-import.ts +0 -168
  213. package/src/extension/run-index.ts +0 -131
  214. package/src/extension/run-maintenance.ts +0 -190
  215. package/src/extension/session-summary.ts +0 -18
  216. package/src/extension/team-manager-command.ts +0 -153
  217. package/src/extension/team-onboard.ts +0 -177
  218. package/src/extension/team-recommendation.ts +0 -288
  219. package/src/extension/team-tool/anchor.ts +0 -172
  220. package/src/extension/team-tool/api.ts +0 -1244
  221. package/src/extension/team-tool/auto-summarize.ts +0 -142
  222. package/src/extension/team-tool/cache-control.ts +0 -19
  223. package/src/extension/team-tool/cancel.ts +0 -337
  224. package/src/extension/team-tool/chain-dispatch.ts +0 -95
  225. package/src/extension/team-tool/chain-executor.ts +0 -379
  226. package/src/extension/team-tool/config-patch.ts +0 -50
  227. package/src/extension/team-tool/context.ts +0 -149
  228. package/src/extension/team-tool/destructive-gate.ts +0 -52
  229. package/src/extension/team-tool/doctor.ts +0 -431
  230. package/src/extension/team-tool/explain.ts +0 -282
  231. package/src/extension/team-tool/failure-patterns.ts +0 -118
  232. package/src/extension/team-tool/goal-wrap.ts +0 -330
  233. package/src/extension/team-tool/goal.ts +0 -556
  234. package/src/extension/team-tool/handle-schedule.ts +0 -332
  235. package/src/extension/team-tool/handle-settings.ts +0 -520
  236. package/src/extension/team-tool/health-monitor.ts +0 -481
  237. package/src/extension/team-tool/inspect.ts +0 -88
  238. package/src/extension/team-tool/intent-policy.ts +0 -45
  239. package/src/extension/team-tool/lifecycle-actions.ts +0 -615
  240. package/src/extension/team-tool/orchestrate.ts +0 -98
  241. package/src/extension/team-tool/parallel-dispatch.ts +0 -183
  242. package/src/extension/team-tool/plan.ts +0 -50
  243. package/src/extension/team-tool/respond.ts +0 -150
  244. package/src/extension/team-tool/run-not-found.ts +0 -49
  245. package/src/extension/team-tool/run.ts +0 -1035
  246. package/src/extension/team-tool/status.ts +0 -260
  247. package/src/extension/team-tool/workflow-manage.ts +0 -261
  248. package/src/extension/team-tool-types.ts +0 -26
  249. package/src/extension/team-tool.ts +0 -1116
  250. package/src/extension/tool-result.ts +0 -16
  251. package/src/extension/validate-resources.ts +0 -108
  252. package/src/hooks/registry.ts +0 -200
  253. package/src/hooks/types.ts +0 -69
  254. package/src/i18n.ts +0 -212
  255. package/src/observability/correlation.ts +0 -50
  256. package/src/observability/event-bus.ts +0 -86
  257. package/src/observability/event-to-metric.ts +0 -170
  258. package/src/observability/exporters/adapter.ts +0 -27
  259. package/src/observability/exporters/otlp-exporter.ts +0 -356
  260. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  261. package/src/observability/metric-registry.ts +0 -100
  262. package/src/observability/metric-retention.ts +0 -64
  263. package/src/observability/metric-sink.ts +0 -99
  264. package/src/observability/metrics-primitives.ts +0 -219
  265. package/src/plugins/plugin-define.ts +0 -6
  266. package/src/plugins/plugin-registry.ts +0 -32
  267. package/src/plugins/plugins/index.ts +0 -3
  268. package/src/plugins/plugins/nextjs.ts +0 -19
  269. package/src/plugins/plugins/vite.ts +0 -10
  270. package/src/plugins/plugins/vitest.ts +0 -9
  271. package/src/prompt/prompt-runtime.ts +0 -380
  272. package/src/runtime/adaptive-plan.ts +0 -563
  273. package/src/runtime/agent-control.ts +0 -215
  274. package/src/runtime/agent-memory.ts +0 -81
  275. package/src/runtime/agent-observability.ts +0 -122
  276. package/src/runtime/anchor-manager.ts +0 -475
  277. package/src/runtime/async-marker.ts +0 -32
  278. package/src/runtime/async-runner.ts +0 -381
  279. package/src/runtime/attention-events.ts +0 -28
  280. package/src/runtime/auto-summarize.ts +0 -344
  281. package/src/runtime/background-runner.ts +0 -837
  282. package/src/runtime/batch-barrier.ts +0 -147
  283. package/src/runtime/broker-issuer.ts +0 -37
  284. package/src/runtime/cancellation-token.ts +0 -99
  285. package/src/runtime/cancellation.ts +0 -99
  286. package/src/runtime/capability-inventory.ts +0 -137
  287. package/src/runtime/chain-parser.ts +0 -237
  288. package/src/runtime/chain-runner.ts +0 -606
  289. package/src/runtime/checkpoint.ts +0 -291
  290. package/src/runtime/child-pi-constants.ts +0 -42
  291. package/src/runtime/child-pi-kill.ts +0 -180
  292. package/src/runtime/child-pi-pool.ts +0 -68
  293. package/src/runtime/child-pi-spawn.ts +0 -287
  294. package/src/runtime/child-pi-steering.ts +0 -128
  295. package/src/runtime/child-pi-streams.ts +0 -296
  296. package/src/runtime/child-pi-transcript.ts +0 -169
  297. package/src/runtime/child-pi.ts +0 -1105
  298. package/src/runtime/coalesce-tasks.ts +0 -268
  299. package/src/runtime/code-summary.ts +0 -292
  300. package/src/runtime/command-trace.ts +0 -105
  301. package/src/runtime/compact-pipeline.ts +0 -56
  302. package/src/runtime/compact-stages/ansi-strip-stage.ts +0 -25
  303. package/src/runtime/compact-stages/blank-collapse-stage.ts +0 -31
  304. package/src/runtime/compact-stages/deduplicate-stage.ts +0 -34
  305. package/src/runtime/compact-stages/head-snap-stage.ts +0 -57
  306. package/src/runtime/compact-stages/index.ts +0 -23
  307. package/src/runtime/compact-stages/tail-capture-stage.ts +0 -77
  308. package/src/runtime/compact-stages/truncation-stage.ts +0 -74
  309. package/src/runtime/compaction-summary.ts +0 -278
  310. package/src/runtime/completion-guard.ts +0 -207
  311. package/src/runtime/concurrency.ts +0 -58
  312. package/src/runtime/crash-classification.ts +0 -229
  313. package/src/runtime/crash-recovery.ts +0 -577
  314. package/src/runtime/crew-agent-records.ts +0 -593
  315. package/src/runtime/crew-agent-runtime.ts +0 -62
  316. package/src/runtime/crew-broker-child.ts +0 -88
  317. package/src/runtime/crew-broker-client.ts +0 -673
  318. package/src/runtime/crew-broker-tokens.ts +0 -84
  319. package/src/runtime/crew-broker.ts +0 -1276
  320. package/src/runtime/crew-hooks.ts +0 -227
  321. package/src/runtime/cross-extension-rpc.ts +0 -143
  322. package/src/runtime/custom-tools/irc-tool.ts +0 -282
  323. package/src/runtime/custom-tools/submit-result-tool.ts +0 -102
  324. package/src/runtime/deadletter.ts +0 -47
  325. package/src/runtime/delivery-coordinator.ts +0 -211
  326. package/src/runtime/delta-conflict.ts +0 -348
  327. package/src/runtime/deterministic-ast.ts +0 -161
  328. package/src/runtime/diagnostic-export.ts +0 -171
  329. package/src/runtime/direct-run.ts +0 -45
  330. package/src/runtime/dwf-state-store.ts +0 -102
  331. package/src/runtime/dynamic-workflow-context.ts +0 -1016
  332. package/src/runtime/dynamic-workflow-runner.ts +0 -310
  333. package/src/runtime/effectiveness.ts +0 -90
  334. package/src/runtime/errors/crew-errors.ts +0 -162
  335. package/src/runtime/event-stream-bridge.ts +0 -98
  336. package/src/runtime/foreground-control.ts +0 -193
  337. package/src/runtime/foreground-watchdog.ts +0 -132
  338. package/src/runtime/global-worker-cap.ts +0 -96
  339. package/src/runtime/goal-achievement.ts +0 -148
  340. package/src/runtime/goal-evaluator.ts +0 -361
  341. package/src/runtime/goal-loop-runner.ts +0 -828
  342. package/src/runtime/goal-state-store.ts +0 -219
  343. package/src/runtime/green-contract.ts +0 -55
  344. package/src/runtime/group-join.ts +0 -267
  345. package/src/runtime/handoff-manager.ts +0 -598
  346. package/src/runtime/heartbeat-gradient.ts +0 -36
  347. package/src/runtime/heartbeat-watcher.ts +0 -239
  348. package/src/runtime/hidden-handoff.ts +0 -407
  349. package/src/runtime/important-line-classifier.ts +0 -140
  350. package/src/runtime/intercom-bridge.ts +0 -187
  351. package/src/runtime/iteration-hooks.ts +0 -305
  352. package/src/runtime/live-agent-control.ts +0 -136
  353. package/src/runtime/live-agent-manager.ts +0 -704
  354. package/src/runtime/live-control-realtime.ts +0 -56
  355. package/src/runtime/live-extension-bridge.ts +0 -148
  356. package/src/runtime/live-irc.ts +0 -97
  357. package/src/runtime/live-session-health.ts +0 -108
  358. package/src/runtime/live-session-runtime.ts +0 -1139
  359. package/src/runtime/loop-gates.ts +0 -128
  360. package/src/runtime/manifest-cache.ts +0 -375
  361. package/src/runtime/mcp-proxy.ts +0 -105
  362. package/src/runtime/metric-parser.ts +0 -36
  363. package/src/runtime/model-fallback.ts +0 -409
  364. package/src/runtime/model-resolver.ts +0 -126
  365. package/src/runtime/model-scope.ts +0 -158
  366. package/src/runtime/orphan-worker-registry.ts +0 -465
  367. package/src/runtime/output-validator.ts +0 -202
  368. package/src/runtime/overflow-recovery.ts +0 -206
  369. package/src/runtime/parallel-research.ts +0 -68
  370. package/src/runtime/parallel-utils.ts +0 -160
  371. package/src/runtime/parent-guard.ts +0 -134
  372. package/src/runtime/path-overlap.ts +0 -150
  373. package/src/runtime/peer-dep.ts +0 -292
  374. package/src/runtime/per-write-validator.ts +0 -181
  375. package/src/runtime/phase-progress.ts +0 -217
  376. package/src/runtime/phase-tracker.ts +0 -385
  377. package/src/runtime/pi-args.ts +0 -655
  378. package/src/runtime/pi-json-output.ts +0 -170
  379. package/src/runtime/pi-spawn.ts +0 -273
  380. package/src/runtime/pipeline-runner.ts +0 -523
  381. package/src/runtime/plan-templates.ts +0 -201
  382. package/src/runtime/policy-engine.ts +0 -115
  383. package/src/runtime/post-checks.ts +0 -142
  384. package/src/runtime/post-exit-stdio-guard.ts +0 -104
  385. package/src/runtime/process-lifecycle.ts +0 -491
  386. package/src/runtime/process-status.ts +0 -154
  387. package/src/runtime/progress-event-coalescer.ts +0 -44
  388. package/src/runtime/progress-tracker.ts +0 -124
  389. package/src/runtime/prose-compressor.ts +0 -162
  390. package/src/runtime/recovery-recipes.ts +0 -203
  391. package/src/runtime/replace.ts +0 -570
  392. package/src/runtime/resilient-edit.ts +0 -153
  393. package/src/runtime/result-extractor.ts +0 -213
  394. package/src/runtime/retry-executor.ts +0 -100
  395. package/src/runtime/retry-runner.ts +0 -336
  396. package/src/runtime/role-permission.ts +0 -59
  397. package/src/runtime/run-coalesced-task-group.ts +0 -308
  398. package/src/runtime/run-drift.ts +0 -219
  399. package/src/runtime/run-tracker.ts +0 -116
  400. package/src/runtime/runtime-policy.ts +0 -29
  401. package/src/runtime/runtime-resolver.ts +0 -172
  402. package/src/runtime/runtime-warmup.ts +0 -160
  403. package/src/runtime/scheduler.ts +0 -353
  404. package/src/runtime/semaphore.ts +0 -141
  405. package/src/runtime/sensitive-paths.ts +0 -93
  406. package/src/runtime/session-resources.ts +0 -25
  407. package/src/runtime/session-snapshot.ts +0 -59
  408. package/src/runtime/session-usage.ts +0 -79
  409. package/src/runtime/settings-store.ts +0 -155
  410. package/src/runtime/sidechain-output.ts +0 -35
  411. package/src/runtime/single-agent-compose.ts +0 -84
  412. package/src/runtime/skill-effectiveness.ts +0 -524
  413. package/src/runtime/skill-instructions.ts +0 -391
  414. package/src/runtime/stale-reconciler.ts +0 -680
  415. package/src/runtime/stream-preview.ts +0 -184
  416. package/src/runtime/streaming-output.ts +0 -50
  417. package/src/runtime/subagent-manager.ts +0 -561
  418. package/src/runtime/subprocess-tool-registry.ts +0 -70
  419. package/src/runtime/supervisor-contact.ts +0 -64
  420. package/src/runtime/task-display.ts +0 -52
  421. package/src/runtime/task-graph-scheduler.ts +0 -227
  422. package/src/runtime/task-graph.ts +0 -290
  423. package/src/runtime/task-health.ts +0 -83
  424. package/src/runtime/task-id.ts +0 -161
  425. package/src/runtime/task-output-context.ts +0 -602
  426. package/src/runtime/task-packet.ts +0 -268
  427. package/src/runtime/task-quality.ts +0 -199
  428. package/src/runtime/task-runner/capabilities.ts +0 -78
  429. package/src/runtime/task-runner/context-retrieval.ts +0 -163
  430. package/src/runtime/task-runner/live-executor.ts +0 -227
  431. package/src/runtime/task-runner/output-splitter.ts +0 -152
  432. package/src/runtime/task-runner/progress.ts +0 -157
  433. package/src/runtime/task-runner/prompt-builder.ts +0 -278
  434. package/src/runtime/task-runner/prompt-pipeline.ts +0 -88
  435. package/src/runtime/task-runner/result-utils.ts +0 -16
  436. package/src/runtime/task-runner/retrieval-orchestrator.ts +0 -310
  437. package/src/runtime/task-runner/run-projection.ts +0 -125
  438. package/src/runtime/task-runner/state-helpers.ts +0 -176
  439. package/src/runtime/task-runner/tail-read.ts +0 -34
  440. package/src/runtime/task-runner.ts +0 -1420
  441. package/src/runtime/team-runner-artifacts.ts +0 -13
  442. package/src/runtime/team-runner.ts +0 -1993
  443. package/src/runtime/tool-output-pruner.ts +0 -333
  444. package/src/runtime/tool-progress.ts +0 -278
  445. package/src/runtime/usage-tracker.ts +0 -73
  446. package/src/runtime/verification-gates.ts +0 -571
  447. package/src/runtime/verification-integrity.ts +0 -105
  448. package/src/runtime/verification-worktree.ts +0 -186
  449. package/src/runtime/worker-heartbeat.ts +0 -25
  450. package/src/runtime/worker-startup.ts +0 -83
  451. package/src/runtime/workflow-state.ts +0 -195
  452. package/src/runtime/workspace-lock.ts +0 -443
  453. package/src/runtime/workspace-tree.ts +0 -312
  454. package/src/runtime/yield-handler.ts +0 -224
  455. package/src/runtime/zombie-scanner.ts +0 -298
  456. package/src/schema/config-schema.ts +0 -327
  457. package/src/schema/team-tool-schema.ts +0 -415
  458. package/src/schema/validation-types.ts +0 -159
  459. package/src/skills/discover-skills.ts +0 -197
  460. package/src/skills/skill-templates.ts +0 -456
  461. package/src/skills/validate.ts +0 -285
  462. package/src/state/active-run-registry.ts +0 -439
  463. package/src/state/artifact-store.ts +0 -176
  464. package/src/state/atomic-write.ts +0 -903
  465. package/src/state/blob-store.ts +0 -308
  466. package/src/state/contracts.ts +0 -158
  467. package/src/state/crew-init.ts +0 -269
  468. package/src/state/decision-ledger.ts +0 -345
  469. package/src/state/event-log-rotation.ts +0 -342
  470. package/src/state/event-log.ts +0 -1311
  471. package/src/state/event-reconstructor.ts +0 -229
  472. package/src/state/gitignore-manager.ts +0 -46
  473. package/src/state/health-store.ts +0 -79
  474. package/src/state/hook-instinct-bridge.ts +0 -94
  475. package/src/state/hook-integrations.ts +0 -51
  476. package/src/state/instinct-store.ts +0 -275
  477. package/src/state/jsonl-writer.ts +0 -107
  478. package/src/state/locks.ts +0 -562
  479. package/src/state/mailbox.ts +0 -916
  480. package/src/state/observation-store.ts +0 -176
  481. package/src/state/run-cache.ts +0 -193
  482. package/src/state/run-graph.ts +0 -183
  483. package/src/state/run-metrics.ts +0 -165
  484. package/src/state/schedule.ts +0 -166
  485. package/src/state/session-state-map.ts +0 -51
  486. package/src/state/state-store.ts +0 -958
  487. package/src/state/task-claims.ts +0 -61
  488. package/src/state/tiered-eval.ts +0 -480
  489. package/src/state/types-eval.ts +0 -58
  490. package/src/state/types.ts +0 -440
  491. package/src/state/usage.ts +0 -138
  492. package/src/state/worker-atomic-writer.ts +0 -194
  493. package/src/subagents/async-entry.ts +0 -1
  494. package/src/subagents/index.ts +0 -3
  495. package/src/subagents/live/control.ts +0 -1
  496. package/src/subagents/live/manager.ts +0 -1
  497. package/src/subagents/live/realtime.ts +0 -1
  498. package/src/subagents/live/session-runtime.ts +0 -1
  499. package/src/subagents/manager.ts +0 -1
  500. package/src/subagents/spawn.ts +0 -1
  501. package/src/teams/discover-teams.ts +0 -187
  502. package/src/teams/team-config.ts +0 -27
  503. package/src/teams/team-serializer.ts +0 -38
  504. package/src/tools/safe-bash-extension.ts +0 -54
  505. package/src/tools/safe-bash.ts +0 -505
  506. package/src/types/diff.d.ts +0 -18
  507. package/src/types/new-api-types.ts +0 -35
  508. package/src/ui/agent-management-overlay.ts +0 -160
  509. package/src/ui/card-colors.ts +0 -139
  510. package/src/ui/crew-footer.ts +0 -102
  511. package/src/ui/crew-select-list.ts +0 -111
  512. package/src/ui/dashboard-panes/agents-pane.ts +0 -164
  513. package/src/ui/dashboard-panes/cancellation-pane.ts +0 -66
  514. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  515. package/src/ui/dashboard-panes/health-pane.ts +0 -31
  516. package/src/ui/dashboard-panes/mailbox-pane.ts +0 -35
  517. package/src/ui/dashboard-panes/metrics-pane.ts +0 -37
  518. package/src/ui/dashboard-panes/progress-pane.ts +0 -38
  519. package/src/ui/dashboard-panes/transcript-pane.ts +0 -10
  520. package/src/ui/deploy-bundled-themes.ts +0 -71
  521. package/src/ui/dwf-phase-display.ts +0 -151
  522. package/src/ui/dynamic-border.ts +0 -35
  523. package/src/ui/heartbeat-aggregator.ts +0 -74
  524. package/src/ui/key-utils.ts +0 -42
  525. package/src/ui/keybinding-map.ts +0 -248
  526. package/src/ui/layout-primitives.ts +0 -106
  527. package/src/ui/live-conversation-overlay.ts +0 -183
  528. package/src/ui/live-duration.ts +0 -55
  529. package/src/ui/live-run-sidebar.ts +0 -301
  530. package/src/ui/loaders.ts +0 -176
  531. package/src/ui/mascot.ts +0 -436
  532. package/src/ui/overlays/agent-picker-overlay.ts +0 -65
  533. package/src/ui/overlays/confirm-overlay.ts +0 -60
  534. package/src/ui/overlays/help-overlay.ts +0 -177
  535. package/src/ui/overlays/mailbox-compose-overlay.ts +0 -181
  536. package/src/ui/overlays/mailbox-compose-preview.ts +0 -78
  537. package/src/ui/overlays/mailbox-detail-overlay.ts +0 -154
  538. package/src/ui/pi-ui-compat.ts +0 -68
  539. package/src/ui/powerbar-publisher.ts +0 -459
  540. package/src/ui/render-coalescer.ts +0 -101
  541. package/src/ui/render-diff.ts +0 -160
  542. package/src/ui/render-scheduler.ts +0 -224
  543. package/src/ui/run-action-dispatcher.ts +0 -182
  544. package/src/ui/run-dashboard.ts +0 -913
  545. package/src/ui/run-event-bus.ts +0 -381
  546. package/src/ui/run-snapshot-cache.ts +0 -1057
  547. package/src/ui/settings-overlay.ts +0 -1067
  548. package/src/ui/snapshot-types.ts +0 -90
  549. package/src/ui/spinner.ts +0 -17
  550. package/src/ui/status-colors.ts +0 -143
  551. package/src/ui/syntax-highlight.ts +0 -136
  552. package/src/ui/terminal-status.ts +0 -269
  553. package/src/ui/theme-adapter.ts +0 -271
  554. package/src/ui/theme-discovery.ts +0 -199
  555. package/src/ui/tool-progress-formatter.ts +0 -93
  556. package/src/ui/tool-render.ts +0 -453
  557. package/src/ui/tool-renderers/brief-mode.ts +0 -225
  558. package/src/ui/tool-renderers/index.ts +0 -726
  559. package/src/ui/transcript-cache.ts +0 -128
  560. package/src/ui/transcript-entries.ts +0 -256
  561. package/src/ui/transcript-viewer.ts +0 -424
  562. package/src/ui/widget/index.ts +0 -357
  563. package/src/ui/widget/widget-formatters.ts +0 -182
  564. package/src/ui/widget/widget-model.ts +0 -110
  565. package/src/ui/widget/widget-renderer.ts +0 -182
  566. package/src/ui/widget/widget-types.ts +0 -36
  567. package/src/utils/bm25-search.ts +0 -235
  568. package/src/utils/completion-dedupe.ts +0 -63
  569. package/src/utils/conflict-detect.ts +0 -668
  570. package/src/utils/env-allowlist.ts +0 -30
  571. package/src/utils/env-filter.ts +0 -155
  572. package/src/utils/file-coalescer.ts +0 -98
  573. package/src/utils/fingerprint.ts +0 -180
  574. package/src/utils/frontmatter.ts +0 -70
  575. package/src/utils/fs-watch.ts +0 -37
  576. package/src/utils/gh-protocol.ts +0 -556
  577. package/src/utils/git.ts +0 -260
  578. package/src/utils/guards.ts +0 -107
  579. package/src/utils/ids.ts +0 -24
  580. package/src/utils/incremental-reader.ts +0 -220
  581. package/src/utils/internal-error.ts +0 -10
  582. package/src/utils/names.ts +0 -36
  583. package/src/utils/ndjson.ts +0 -115
  584. package/src/utils/paths.ts +0 -227
  585. package/src/utils/project-detector.ts +0 -160
  586. package/src/utils/redaction.ts +0 -347
  587. package/src/utils/resolve-shell.ts +0 -36
  588. package/src/utils/run-watcher-registry.ts +0 -152
  589. package/src/utils/safe-paths.ts +0 -383
  590. package/src/utils/scan-cache.ts +0 -144
  591. package/src/utils/session-utils.ts +0 -110
  592. package/src/utils/sleep.ts +0 -54
  593. package/src/utils/socket-path.ts +0 -127
  594. package/src/utils/sse-parser.ts +0 -129
  595. package/src/utils/task-name-generator.ts +0 -337
  596. package/src/utils/timings.ts +0 -33
  597. package/src/utils/token-counter.ts +0 -200
  598. package/src/utils/visual.ts +0 -207
  599. package/src/workflows/cost-estimator.ts +0 -34
  600. package/src/workflows/discover-workflows.ts +0 -290
  601. package/src/workflows/intermediate-store.ts +0 -166
  602. package/src/workflows/preflight-validator.ts +0 -168
  603. package/src/workflows/topology-analyzer.ts +0 -203
  604. package/src/workflows/validate-workflow.ts +0 -50
  605. package/src/workflows/workflow-config.ts +0 -70
  606. package/src/workflows/workflow-serializer.ts +0 -32
  607. package/src/worktree/branch-freshness.ts +0 -112
  608. package/src/worktree/cleanup.ts +0 -341
  609. package/src/worktree/worktree-manager.ts +0 -1200
@@ -1,958 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as fsp from "node:fs/promises";
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";
13
- 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
-
20
- /**
21
- * stat() the manifest with a brief retry on Windows for the AV-scan window.
22
- *
23
- * On the GitHub Actions windows-latest runner, Windows Defender real-time
24
- * scanning can make a freshly-written manifest.json briefly invisible to
25
- * statSync (ENOENT) even though the write succeeded and the file is on disk.
26
- * loadRunManifestById is called right after createRunManifest in tests and in
27
- * production (e.g. refreshPersistedSubagentRecord), so without a retry the
28
- * caller sees a phantom "missing" run.
29
- *
30
- * On non-Windows, ENOENT means the file genuinely doesn't exist — passthrough
31
- * (throw immediately) with no retry. On Windows, ENOENT/EPERM/EBUSY/EAGAIN get
32
- * a handful of short retries (~30ms worst case) before giving up and throwing
33
- * so the caller's catch returns undefined as before.
34
- */
35
- function statManifestWithWindowsRetry(manifestPath: string): fs.Stats {
36
- if (process.platform !== "win32") return fs.statSync(manifestPath);
37
- const retryable = new Set(["ENOENT", "EPERM", "EBUSY", "EAGAIN"]);
38
- for (let attempt = 0; attempt < 5; attempt++) {
39
- try {
40
- return fs.statSync(manifestPath);
41
- } catch (error) {
42
- const code = (error as NodeJS.ErrnoException).code;
43
- if (!retryable.has(code ?? "")) throw error;
44
- const end = Date.now() + Math.min(8, 1 * 2 ** attempt);
45
- while (Date.now() < end) {
46
- /* brief spin to ride out the AV scan window */
47
- }
48
- }
49
- }
50
- return fs.statSync(manifestPath); // last attempt — let caller's catch handle ENOENT
51
- }
52
-
53
- export interface RunPaths {
54
- runId: string;
55
- stateRoot: string;
56
- artifactsRoot: string;
57
- manifestPath: string;
58
- tasksPath: string;
59
- eventsPath: string;
60
- }
61
-
62
- export interface ManifestCacheEntry {
63
- manifest: TeamRunManifest;
64
- tasks: TeamTaskState[];
65
- manifestMtimeMs: number;
66
- manifestSize: number;
67
- tasksMtimeMs: number;
68
- tasksSize: number;
69
- cachedAt?: number;
70
- generation?: number;
71
- }
72
-
73
- // F1: per-stateRoot generation counter. The previous global counter was
74
- // incremented on every `invalidateRunCache` call regardless of which state's
75
- // cache was invalidated, so a write to run A caused a hot-path miss in run B's
76
- // cache even though B's on-disk files were untouched. With per-stateRoot
77
- // generations, run B only misses its own cache when its own writes happen.
78
- // NOTE: This generation counter is process-local. Cross-process consistency
79
- // (e.g., parent + child processes writing to the same run) relies on the
80
- // mtime/size checks in loadRunManifestById, not on this counter.
81
- const manifestCacheGeneration = new Map<string, number>();
82
- function genOf(stateRoot: string): number {
83
- return manifestCacheGeneration.get(stateRoot) ?? 0;
84
- }
85
-
86
- const MANIFEST_CACHE_TTL_MS = 60 * 1000; // 60 seconds (FIX: increased from 15s for read-heavy workloads; render tick 160ms caused frequent misses)
87
- const LOAD_MANIFEST_RETRY_LIMIT = 5; // Configurable retry limit for mtime/size stability checks under contention
88
- const manifestCache = new Map<string, ManifestCacheEntry>();
89
-
90
- /** @internal — exported for TTL-eviction unit testing (Round 19). */
91
- export function __test__setManifestCache(stateRoot: string, entry: ManifestCacheEntry): void {
92
- setManifestCache(stateRoot, entry);
93
- }
94
-
95
- /** @internal — exported for TTL-eviction unit testing (Round 19). */
96
- export function __test__getManifestCacheEntry(stateRoot: string): ManifestCacheEntry | undefined {
97
- return manifestCache.get(stateRoot);
98
- }
99
-
100
- /** @internal — the TTL in ms used for manifest cache eviction. */
101
- export const MANIFEST_CACHE_TTL_MS_VALUE = MANIFEST_CACHE_TTL_MS;
102
-
103
- function setManifestCache(stateRoot: string, entry: ManifestCacheEntry): void {
104
- if (manifestCache.has(stateRoot)) manifestCache.delete(stateRoot);
105
- entry.cachedAt = Date.now();
106
- // Stamp with the *current* per-root generation so a concurrent writer's
107
- // increment between read and write invalidates this cache hit.
108
- entry.generation = genOf(stateRoot);
109
- // FIX: Evict all stale entries by TTL before adding new entry.
110
- // This ensures entries that are never accessed still get evicted
111
- // based on TTL, not just entries that are hit.
112
- const now = Date.now();
113
- for (const [key, val] of manifestCache.entries()) {
114
- if (val.cachedAt && now - val.cachedAt > MANIFEST_CACHE_TTL_MS) {
115
- manifestCache.delete(key);
116
- }
117
- }
118
- manifestCache.set(stateRoot, entry);
119
- while (manifestCache.size > DEFAULT_CACHE.manifestMaxEntries) {
120
- // FIX: Evict oldest entry by cachedAt (LRU), not insertion order.
121
- // cachedAt is set on both initial insertion and cache hits, so
122
- // frequently accessed entries bubble to the end and survive longer.
123
- let oldestKey: string | undefined;
124
- let oldestTime = Infinity;
125
- for (const [key, val] of manifestCache.entries()) {
126
- const t = val.cachedAt ?? 0;
127
- if (t < oldestTime) {
128
- oldestTime = t;
129
- oldestKey = key;
130
- }
131
- }
132
- if (!oldestKey) break;
133
- manifestCache.delete(oldestKey);
134
- }
135
- }
136
-
137
- function useProjectState(cwd: string): boolean {
138
- return findRepoRoot(cwd) !== undefined;
139
- }
140
-
141
- function invalidateRunCache(stateRoot: string): void {
142
- manifestCache.delete(stateRoot);
143
- // F1: only bump the generation of THIS stateRoot — sibling runs keep their
144
- // generation intact, so concurrent writes to run A no longer evict run B's
145
- // hot cache.
146
- manifestCacheGeneration.set(stateRoot, genOf(stateRoot) + 1);
147
- }
148
-
149
- function scopeBaseRoot(cwd: string): string {
150
- return useProjectState(cwd) ? projectCrewRoot(cwd) : userCrewRoot();
151
- }
152
-
153
- function resolveRunStateRoot(cwd: string, runId: string): string | undefined {
154
- assertSafePathId("runId", runId);
155
- const runsRoot = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.runsSubdir);
156
- const scopedPath = resolveContainedRelativePath(runsRoot, runId, "runId");
157
- try {
158
- // Single atomic validation: resolves through symlinks via realpath,
159
- // verifies containment within runsRoot, and throws ENOENT if missing.
160
- // Eliminates the TOCTOU window from the previous existsSync + lstatSync
161
- // + resolveRealContainedPath sequence.
162
- resolveRealContainedPath(runsRoot, runId);
163
- } catch {
164
- return undefined;
165
- }
166
- return scopedPath;
167
- }
168
-
169
- function validateRunManifestPaths(cwd: string, runId: string, manifest: TeamRunManifest, stateRoot: string, tasksPath: string): boolean {
170
- // Issue 2 fix: Reject manifests missing status field to prevent undefined
171
- // behavior in callers like canTransitionRunStatus(manifest.status, newStatus).
172
- if (!manifest.status || typeof manifest.status !== "string") return false;
173
- if (
174
- manifest.runId !== runId ||
175
- manifest.stateRoot !== stateRoot ||
176
- manifest.tasksPath !== tasksPath ||
177
- manifest.eventsPath !== path.join(stateRoot, "events.jsonl")
178
- )
179
- return false;
180
- const artifactsParent = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.artifactsSubdir);
181
- const expectedArtifactsRoot = resolveContainedRelativePath(artifactsParent, runId, "runId");
182
- if (manifest.artifactsRoot !== expectedArtifactsRoot) return false;
183
- // Always validate artifactsRoot is not a symlink, even when manifest has
184
- // no artifacts entries. A symlinked artifactsRoot pointing outside the
185
- // artifacts parent is a security violation (could write to attacker-
186
- // controlled locations). This check catches the case where the run was
187
- // created legitimately but the artifactsRoot was later replaced with a
188
- // symlink between manifest creation and load.
189
- if (fs.existsSync(expectedArtifactsRoot)) {
190
- try {
191
- if (fs.lstatSync(expectedArtifactsRoot).isSymbolicLink()) return false;
192
- resolveRealContainedPath(artifactsParent, runId);
193
- } catch {
194
- return false;
195
- }
196
- } else if (manifest.artifacts && manifest.artifacts.length > 0) {
197
- // Has artifacts entries but directory doesn't exist - benign state for
198
- // runs still in progress.
199
- return true;
200
- }
201
- return true;
202
- }
203
-
204
- export function createRunPaths(cwd: string, runId = createRunId()): RunPaths {
205
- if (!cwd || typeof cwd !== "string") {
206
- throw new Error(`Invalid cwd: ${cwd}`);
207
- }
208
- assertSafePathId("runId", runId);
209
- const baseRoot = scopeBaseRoot(cwd);
210
- const stateRoot = resolveContainedRelativePath(path.join(baseRoot, DEFAULT_PATHS.state.runsSubdir), runId, "runId");
211
- const artifactsRoot = resolveContainedRelativePath(path.join(baseRoot, DEFAULT_PATHS.state.artifactsSubdir), runId, "runId");
212
- return {
213
- runId,
214
- stateRoot,
215
- artifactsRoot,
216
- manifestPath: path.join(stateRoot, DEFAULT_PATHS.state.manifestFile),
217
- tasksPath: path.join(stateRoot, DEFAULT_PATHS.state.tasksFile),
218
- eventsPath: path.join(stateRoot, DEFAULT_PATHS.state.eventsFile),
219
- };
220
- }
221
-
222
- export function createTasksFromWorkflow(runId: string, workflow: WorkflowConfig, team: TeamConfig, cwd: string): TeamTaskState[] {
223
- const stepToTaskId = new Map(workflow.steps.map((step, index) => [step.id, createTaskId(step.id, index)]));
224
- return workflow.steps.map((step, index) => {
225
- const role = team.roles.find((candidate) => candidate.name === step.role);
226
- const id = stepToTaskId.get(step.id) ?? createTaskId(step.id, index);
227
- const dependencies = step.dependsOn ?? [];
228
- const children = workflow.steps
229
- .filter((candidate) => candidate.dependsOn?.includes(step.id))
230
- .map((candidate) => stepToTaskId.get(candidate.id))
231
- .filter((childId): childId is string => childId !== undefined);
232
- return {
233
- id,
234
- runId,
235
- stepId: step.id,
236
- role: step.role,
237
- agent: role?.agent ?? step.role,
238
- title: step.id,
239
- status: "queued",
240
- dependsOn: dependencies,
241
- cwd,
242
- model: step.model,
243
- graph: {
244
- taskId: id,
245
- parentId: dependencies[0] ? stepToTaskId.get(dependencies[0]) : undefined,
246
- children,
247
- dependencies: dependencies.map((dep) => stepToTaskId.get(dep) ?? dep),
248
- queue: dependencies.length ? "blocked" : "ready",
249
- },
250
- };
251
- });
252
- }
253
-
254
- export function createRunManifest(params: {
255
- cwd: string;
256
- team: TeamConfig;
257
- workflow?: WorkflowConfig;
258
- goal: string;
259
- workspaceMode?: "single" | "worktree";
260
- ownerSessionId?: string;
261
- runKind?: "team-run" | "goal-loop" | "dynamic-workflow";
262
- /** round-14 P1-5: typed workflow arguments for .dwf.ts scripts (ctx.args<T>()). */
263
- args?: unknown;
264
- }): { manifest: TeamRunManifest; tasks: TeamTaskState[]; paths: RunPaths } {
265
- const paths = createRunPaths(params.cwd);
266
- const now = new Date().toISOString();
267
- const tasks = params.workflow ? createTasksFromWorkflow(paths.runId, params.workflow, params.team, params.cwd) : [];
268
- const manifest: TeamRunManifest = {
269
- schemaVersion: 1,
270
- runId: paths.runId,
271
- sessionId: toPiSessionId(paths.runId),
272
- team: params.team.name,
273
- workflow: params.workflow?.name,
274
- goal: params.goal,
275
- status: "queued",
276
- workspaceMode: params.workspaceMode ?? params.team.workspaceMode ?? "single",
277
- createdAt: now,
278
- updatedAt: now,
279
- cwd: params.cwd,
280
- stateRoot: paths.stateRoot,
281
- artifactsRoot: paths.artifactsRoot,
282
- tasksPath: paths.tasksPath,
283
- eventsPath: paths.eventsPath,
284
- artifacts: [],
285
- ...(params.ownerSessionId ? { ownerSessionId: params.ownerSessionId } : {}),
286
- runKind: params.runKind ?? "team-run",
287
- ...(params.args !== undefined ? { args: params.args } : {}),
288
- };
289
- fs.mkdirSync(paths.stateRoot, { recursive: true });
290
- fs.mkdirSync(paths.artifactsRoot, { recursive: true });
291
- // FIX: Use saveManifestAndTasksAtomicSync and check result to detect
292
- // partial failure. If tasksWritten=false when manifestWritten=true,
293
- // throw to ensure manifest and tasks are always consistent.
294
- const result = saveManifestAndTasksAtomicSync(manifest, tasks);
295
- if (!result.manifestWritten || !result.tasksWritten) {
296
- // Surface the underlying error message (result.error is String(err) from
297
- // saveManifestAndTasksAtomicSync). Passing it through errors.fileWrite as a
298
- // fake ErrnoException loses the message (reads .code → undefined →
299
- // "unknown"). Include it explicitly in the thrown message so CI logs and
300
- // production callers can see WHY the write failed instead of ": unknown".
301
- const cause = result.error ? `: ${result.error}` : "";
302
- throw errors
303
- .fileWrite(paths.stateRoot, {
304
- code: "EWRITEFAIL",
305
- } as NodeJS.ErrnoException)
306
- .withContext(
307
- `saveManifestAndTasksAtomicSync: manifestWritten=${result.manifestWritten}, tasksWritten=${result.tasksWritten}${cause}`,
308
- );
309
- }
310
- appendEvent(paths.eventsPath, {
311
- type: "run.created",
312
- runId: paths.runId,
313
- data: { team: params.team.name, workflow: params.workflow?.name },
314
- metadata: {
315
- seq: 1,
316
- provenance: "team_runner",
317
- sessionIdentity: {
318
- title: params.team.name,
319
- workspace: params.cwd,
320
- purpose: params.goal,
321
- },
322
- ownership: {
323
- owner: params.team.name,
324
- workflowScope: params.workflow?.name ?? "manual",
325
- watcherAction: "act",
326
- },
327
- confidence: "high",
328
- },
329
- });
330
- invalidateRunCache(paths.stateRoot);
331
- return { manifest, tasks, paths };
332
- }
333
-
334
- export function saveRunManifest(manifest: TeamRunManifest): void {
335
- // FIX: Capture the cached tasks array + mtime/size BEFORE we invalidate the
336
- // cache. The previous implementation re-read tasks.json from disk after the
337
- // manifest write (a JSON.parse + fs.readFileSync per call), which defeated
338
- // the whole point of the manifest cache on the hot update path. Reusing the
339
- // already-cached entry is safe because:
340
- // 1. The cache entry's tasks array matches the tasks array reflected by
341
- // its tasksMtimeMs/tasksSize — the three values move together.
342
- // 2. After invalidate, saveRunTasks (or any other writer) would bump the
343
- // per-stateRoot generation, so a stale cache hit cannot serve.
344
- // 3. On cache miss, we fall back to tasks: [] with mtime/size 0 — the same
345
- // shape the original pre-FIX code produced — so behavior is unchanged.
346
- const cachedBeforeInvalidate = manifestCache.get(manifest.stateRoot);
347
- const cachedTasks = cachedBeforeInvalidate?.tasks ?? [];
348
- const cachedTasksMtimeMs = cachedBeforeInvalidate?.tasksMtimeMs ?? 0;
349
- const cachedTasksSize = cachedBeforeInvalidate?.tasksSize ?? 0;
350
- // FIX: Invalidate cache BEFORE atomic write. The order matters for crash
351
- // safety: if we invalidated after the write and crashed before invalidation,
352
- // the stale cache entry (up to MANIFEST_CACHE_TTL_MS old) could be served.
353
- // By invalidating first, the worst case is a cache miss forcing a disk read,
354
- // which is always safe.
355
- invalidateRunCache(manifest.stateRoot);
356
- const manifestPath = path.join(manifest.stateRoot, "manifest.json");
357
- atomicWriteJson(manifestPath, manifest);
358
- // FIX: Re-populate cache with actual mtime/size so loadRunManifestById
359
- // doesn't miss the cache on next read. Without this, every load until
360
- // TTL expires would hit disk because cached 0 !== any real mtime.
361
- // NOTE: tasks is reused from the pre-invalidate cache snapshot above; if no
362
- // cache existed, tasks is [] (matching pre-FIX behavior). Callers that need
363
- // fresh tasks should call saveRunTasks or loadRunTasks separately.
364
- const manifestStat = fs.statSync(manifestPath);
365
- setManifestCache(manifest.stateRoot, {
366
- manifest,
367
- tasks: cachedTasks,
368
- manifestMtimeMs: manifestStat.mtimeMs,
369
- manifestSize: manifestStat.size,
370
- tasksMtimeMs: cachedTasksMtimeMs,
371
- tasksSize: cachedTasksSize,
372
- });
373
- }
374
-
375
- export async function saveRunManifestAsync(manifest: TeamRunManifest): Promise<void> {
376
- // FIX: Capture cached tasks array + mtime/size BEFORE invalidating, same
377
- // rationale as the sync saveRunManifest above. The async path previously
378
- // always set tasks: [] with mtime/size 0, so any cache hit was guaranteed
379
- // to look stale to loadRunManifestById until something else wrote tasks.json.
380
- const cachedBeforeInvalidate = manifestCache.get(manifest.stateRoot);
381
- const cachedTasks = cachedBeforeInvalidate?.tasks ?? [];
382
- const cachedTasksMtimeMs = cachedBeforeInvalidate?.tasksMtimeMs ?? 0;
383
- const cachedTasksSize = cachedBeforeInvalidate?.tasksSize ?? 0;
384
- // FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving
385
- // after a crash. See saveRunManifest for full explanation.
386
- invalidateRunCache(manifest.stateRoot);
387
- const manifestPath = path.join(manifest.stateRoot, "manifest.json");
388
- await atomicWriteJsonAsync(manifestPath, manifest);
389
- // FIX: Re-populate cache with actual mtime/size. See saveRunManifest.
390
- // RACE GUARD: another concurrent async save (OPT-02) may unlink+rewrite
391
- // manifest.json between our atomicWriteJsonAsync and this stat. If stat
392
- // hits ENOENT, use fallback values — the cache will look stale on the
393
- // next load and re-read from disk, which is correct.
394
- let manifestStat: { mtimeMs: number; size: number };
395
- try {
396
- manifestStat = await fs.promises.stat(manifestPath);
397
- } catch (statError) {
398
- const code = String((statError as NodeJS.ErrnoException).code ?? "");
399
- if (code !== "ENOENT") throw statError;
400
- manifestStat = { mtimeMs: 0, size: 0 };
401
- }
402
- setManifestCache(manifest.stateRoot, {
403
- manifest,
404
- tasks: cachedTasks,
405
- manifestMtimeMs: manifestStat.mtimeMs,
406
- manifestSize: manifestStat.size,
407
- tasksMtimeMs: cachedTasksMtimeMs,
408
- tasksSize: cachedTasksSize,
409
- });
410
- }
411
-
412
- export function saveRunTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]): void {
413
- // FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
414
- invalidateRunCache(manifest.stateRoot);
415
-
416
- // Guard: if the run state directory has been removed (prune/forget/cleanup),
417
- // silently return — there is nothing to persist and the run is gone.
418
- try {
419
- fs.statSync(manifest.stateRoot);
420
- } catch {
421
- return;
422
- }
423
-
424
- atomicWriteJson(manifest.tasksPath, tasks);
425
- // FIX: Re-populate cache with actual mtime/size for manifest and tasks.
426
- // Note: We re-read manifest from disk to get its current mtime/size
427
- // since we only wrote tasks here.
428
- const manifestPath = path.join(manifest.stateRoot, "manifest.json");
429
- let manifestStat: fs.Stats;
430
- let tasksStat: fs.Stats;
431
- try {
432
- manifestStat = fs.statSync(manifestPath);
433
- tasksStat = fs.statSync(manifest.tasksPath);
434
- } catch {
435
- // Run state disappeared between the guard above and now — give up gracefully.
436
- return;
437
- }
438
- // FIX: If cache was evicted, re-read manifest from disk rather than using
439
- // a minimal fallback. A stale minimal manifest with only runId populated
440
- // would cause manifest.status to be undefined, breaking status checks.
441
- // If the manifest cannot be read, throw an error — callers using withRunLock
442
- // should never hit this case, and the error indicates a serious problem.
443
- // FIX: Also check that the re-read manifest has a status field. If not,
444
- // fall back to the manifest parameter's status rather than serving a
445
- // degraded manifest that would break status transition checks.
446
- const cached = manifestCache.get(manifest.stateRoot);
447
- const manifestEntry = cached?.manifest ?? readJsonFile<TeamRunManifest>(manifestPath);
448
- if (!manifestEntry) {
449
- return; // Run deleted between guard and read
450
- }
451
- // Preserve current status from the manifest parameter if the on-disk
452
- // manifest is missing it (could be a partial write).
453
- if (!manifestEntry.status) {
454
- manifestEntry.status = manifest.status;
455
- }
456
- setManifestCache(manifest.stateRoot, {
457
- manifest: manifestEntry,
458
- tasks,
459
- manifestMtimeMs: manifestStat.mtimeMs,
460
- manifestSize: manifestStat.size,
461
- tasksMtimeMs: tasksStat.mtimeMs,
462
- tasksSize: tasksStat.size,
463
- });
464
- }
465
-
466
- /**
467
- * 2.1 caller-migration helper: coalesced variant. Use only when the
468
- * caller does NOT immediately read tasks.json afterwards (the read would
469
- * see the previous on-disk content while the write is still buffered).
470
- * Bulk update paths that fan out into multiple writer call sites are the
471
- * intended use case. Single-update + read-update loops (e.g.
472
- * persistSingleTaskUpdate) should keep using saveRunTasks — OR call
473
- * `flushPendingAtomicWrites()` immediately before their read to force
474
- * any pending coalesced writes to land on disk first.
475
- *
476
- * (perf review 2026-07 F4) — now used by persistSingleTaskUpdate's
477
- * non-terminal checkpoint path; that caller calls flushPendingAtomicWrites()
478
- * before its read-modify-write load to defeat the stale-read window.
479
- *
480
- * ST-7: pass `skipCoalesce: true` for terminal task status transitions
481
- * (completed/failed/cancelled/needs_attention/skipped) to bypass the
482
- * 50ms coalesce window — a SIGKILL in that window would otherwise leave
483
- * tasks.json stale (showing "running") while events.jsonl already shows
484
- * the terminal event, causing false zombie detection / double-execution
485
- * on crash recovery.
486
- */
487
- /** @internal */
488
- export function saveRunTasksCoalesced(manifest: TeamRunManifest, tasks: TeamTaskState[], skipCoalesce: boolean = false): void {
489
- // FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
490
- invalidateRunCache(manifest.stateRoot);
491
- try {
492
- fs.statSync(manifest.stateRoot);
493
- } catch {
494
- return;
495
- }
496
- atomicWriteJsonCoalesced(manifest.tasksPath, tasks, undefined, undefined, skipCoalesce);
497
- }
498
-
499
- export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<void> {
500
- // FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
501
- invalidateRunCache(manifest.stateRoot);
502
- try {
503
- await fsp.access(manifest.stateRoot);
504
- } catch {
505
- return;
506
- }
507
- await atomicWriteJsonAsync(manifest.tasksPath, tasks);
508
- }
509
-
510
- /**
511
- * Save manifest and tasks files with individual atomic writes.
512
- * FIX: Changed from Promise.all (parallel, non-jointly-atomic) to sequential
513
- * writes to ensure manifest is written before tasks. A crash between writes
514
- * leaves them in a known state (manifest is older, tasks is newer) which
515
- * loadRunManifestById's retry loop can detect via mtime comparison.
516
- * NOTE: There is no stale-reconciler component — the retry loop provides
517
- * best-effort detection of mid-write crashes by re-reading until mtime/size
518
- * are stable. For strict atomicity, callers should use withRunLock().
519
- * FIX: Returns a result object so callers know which write step failed.
520
- * If manifest write succeeds but tasks write fails, the caller can recover.
521
- */
522
- /** @internal */
523
- interface SaveManifestAndTasksResult {
524
- manifestWritten: boolean;
525
- tasksWritten: boolean;
526
- error?: string;
527
- }
528
-
529
- async function saveManifestAndTasksAtomic(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<SaveManifestAndTasksResult> {
530
- let manifestWritten = false;
531
- let tasksWritten = false;
532
- try {
533
- await withRunLock(manifest, async () => {
534
- // FIX: Invalidate cache BEFORE writes to prevent stale cache serving.
535
- // Sequential writes instead of Promise.all to ensure manifest is
536
- // written before tasks. If a crash occurs between writes, manifest is
537
- // the older timestamp which stale-reconciler uses to detect inconsistency.
538
- invalidateRunCache(manifest.stateRoot);
539
- await atomicWriteJsonAsync(path.join(manifest.stateRoot, "manifest.json"), manifest);
540
- manifestWritten = true;
541
- await atomicWriteJsonAsync(manifest.tasksPath, tasks);
542
- tasksWritten = true;
543
- });
544
- } catch (err) {
545
- return {
546
- manifestWritten,
547
- tasksWritten,
548
- // FIX: Use String(err) to safely convert any thrown value (Error,
549
- // string, number, null, undefined) to a string. err.message would be
550
- // undefined for non-Error throwables, losing useful context.
551
- error: String(err),
552
- };
553
- }
554
- return { manifestWritten: true, tasksWritten: true };
555
- }
556
-
557
- /** @internal */
558
- function saveManifestAndTasksAtomicSync(manifest: TeamRunManifest, tasks: TeamTaskState[]): SaveManifestAndTasksResult {
559
- let manifestWritten = false;
560
- let tasksWritten = false;
561
- try {
562
- withRunLockSync(manifest, () => {
563
- // FIX: Invalidate cache BEFORE writes to prevent stale cache serving.
564
- invalidateRunCache(manifest.stateRoot);
565
- atomicWriteJson(path.join(manifest.stateRoot, "manifest.json"), manifest);
566
- manifestWritten = true;
567
- atomicWriteJson(manifest.tasksPath, tasks);
568
- tasksWritten = true;
569
- });
570
- } catch (err) {
571
- return {
572
- manifestWritten,
573
- tasksWritten,
574
- // FIX: Use String(err) to safely convert any thrown value (Error,
575
- // string, number, null, undefined) to a string. err.message would be
576
- // undefined for non-Error throwables, losing useful context.
577
- error: String(err),
578
- };
579
- }
580
- return { manifestWritten: true, tasksWritten: true };
581
- }
582
-
583
- export interface UpdateRunStatusOptions {
584
- data?: Record<string, unknown>;
585
- metadata?: Parameters<typeof appendEvent>[1]["metadata"];
586
- }
587
-
588
- export function updateRunStatus(
589
- manifest: TeamRunManifest,
590
- status: TeamRunManifest["status"],
591
- summary?: string,
592
- options: UpdateRunStatusOptions = {},
593
- ): TeamRunManifest {
594
- if (!canTransitionRunStatus(manifest.status, status)) {
595
- throw errors.invalidStatusTransition(manifest.status, status);
596
- }
597
- const updated: TeamRunManifest = {
598
- ...manifest,
599
- status,
600
- updatedAt: new Date().toISOString(),
601
- summary: summary ?? manifest.summary,
602
- };
603
- saveRunManifest(updated);
604
- // Unregister from active-run-index when run reaches a terminal status.
605
- // Without this, stale entries accumulate (e.g. integration tests in /tmp) and
606
- // Pi UI shows ghost "queued" runs that are actually completed/failed/cancelled.
607
- // Note: "blocked" is excluded because blocked runs can be unblocked later.
608
- if (status === "completed" || status === "failed" || status === "cancelled") {
609
- try {
610
- unregisterActiveRun(updated.runId);
611
- } catch {
612
- /* non-critical */
613
- }
614
- }
615
- appendEvent(updated.eventsPath, {
616
- type: `run.${status}`,
617
- runId: updated.runId,
618
- message: summary,
619
- ...(options.data ? { data: options.data } : {}),
620
- metadata: {
621
- provenance: "team_runner",
622
- sessionIdentity: {
623
- title: updated.team,
624
- workspace: updated.cwd,
625
- purpose: updated.goal,
626
- },
627
- ownership: {
628
- owner: updated.team,
629
- workflowScope: updated.workflow ?? "manual",
630
- watcherAction: "act",
631
- },
632
- confidence: "high",
633
- ...options.metadata,
634
- },
635
- });
636
- return updated;
637
- }
638
-
639
- export function __test__manifestCacheSize(): number {
640
- return manifestCache.size;
641
- }
642
-
643
- export function __test__clearManifestCache(): void {
644
- manifestCache.clear();
645
- }
646
-
647
- /**
648
- * OPT-08 additive helper: drop the manifest cache entry for `stateRoot` after
649
- * flushing any pending coalesced atomic writes (process-wide). Forces the next
650
- * `loadRunManifestById` / `loadRunManifestByIdAsync` for this run to hit disk
651
- * instead of serving a possibly-stale cache hit. Idempotent: safe to call
652
- * multiple times, including for unknown stateRoots.
653
- *
654
- * This is an additive helper ONLY — it does NOT replace the load-bearing CAS
655
- * loop in `persistSingleTaskUpdate`, nor does it bypass `withRunLock` /
656
- * `fs.statSync`-based invalidation. It is a safe exit hatch for callers that
657
- * want a guaranteed cache drop (e.g. cleanup paths, end-of-run shutdown).
658
- *
659
- * Scope note: `flushPendingAtomicWrites()` flushes ALL pending coalesced
660
- * writes process-wide, not just those under `stateRoot`. The coalescer has no
661
- * per-stateRoot filter; flushing the whole queue is the simplest correct
662
- * behavior and matches the existing `flushPendingAtomicWrites()` contract
663
- * used by cleanupRuntime / process exit handlers.
664
- */
665
- export async function unloadRun(stateRoot: string): Promise<void> {
666
- // Flush first so any in-flight buffered write lands on disk before we drop
667
- // the cache entry. Otherwise a coalesced write could fire AFTER unloadRun
668
- // returns, re-populate the cache from disk, and re-stale it.
669
- flushPendingAtomicWrites();
670
- // invalidateRunCache bumps the per-stateRoot generation counter so even if
671
- // some in-process reader has a stale reference, the next cache lookup
672
- // misses (generation mismatch) and re-reads from disk.
673
- invalidateRunCache(stateRoot);
674
- }
675
-
676
- /**
677
- * OPT-08 additive helper: read-only inspection of the manifest cache.
678
- * Returns current size, configured limits, and per-stateRoot observability
679
- * (cache generation + age in ms). Pure read — never mutates cache state.
680
- * Useful for `tests`, dashboards, and pre-shutdown sanity checks.
681
- */
682
- export interface ManifestCacheStats {
683
- size: number;
684
- maxEntries: number;
685
- ttlMs: number;
686
- perStateRoot: Record<string, { generation: number; ageMs: number }>;
687
- }
688
-
689
- export function getManifestCacheStats(): ManifestCacheStats {
690
- const now = Date.now();
691
- const perStateRoot: Record<string, { generation: number; ageMs: number }> = {};
692
- for (const [stateRoot, entry] of manifestCache.entries()) {
693
- perStateRoot[stateRoot] = {
694
- generation: entry.generation ?? genOf(stateRoot),
695
- // ageMs is 0 when cachedAt is unset (defensive — setManifestCache
696
- // always sets it today, but a future regression would otherwise
697
- // surface as `undefined` in the stats object).
698
- ageMs: entry.cachedAt ? now - entry.cachedAt : 0,
699
- };
700
- }
701
- return {
702
- size: manifestCache.size,
703
- maxEntries: DEFAULT_CACHE.manifestMaxEntries,
704
- ttlMs: MANIFEST_CACHE_TTL_MS,
705
- perStateRoot,
706
- };
707
- }
708
-
709
- async function readJsonFileAsync<T>(filePath: string): Promise<T | undefined> {
710
- try {
711
- return JSON.parse(await fs.promises.readFile(filePath, "utf-8")) as T;
712
- } catch (err) {
713
- const code = (err as NodeJS.ErrnoException).code;
714
- if (code !== "ENOENT" && code !== "ENOTDIR") {
715
- logInternalError("readJsonFileAsync", err, `filePath=${filePath}`);
716
- }
717
- return undefined;
718
- }
719
- }
720
-
721
- /**
722
- * Load a run manifest and its tasks by runId.
723
- * WARNING: This function provides best-effort consistency only. The sentinel-based
724
- * retry loop does NOT guarantee manifest/tasks consistency under contention —
725
- * a concurrent writer can complete a full write cycle between the final stat
726
- * and the read. For strict consistency, callers MUST wrap load+modify+save in
727
- * withRunLock(). Callers that need guaranteed consistency should use the lock.
728
- */
729
- export function loadRunManifestById(cwd: string, runId: string): { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined {
730
- const stateRoot = resolveRunStateRoot(cwd, runId);
731
- if (!stateRoot) return undefined;
732
- const manifestPath = path.join(stateRoot, "manifest.json");
733
- const tasksPath = path.join(stateRoot, "tasks.json");
734
-
735
- let manifestStat: fs.Stats;
736
- try {
737
- manifestStat = statManifestWithWindowsRetry(manifestPath);
738
- } catch {
739
- return undefined;
740
- }
741
- const cached = manifestCache.get(stateRoot);
742
- // Issue 1 fix: Note that the cache read-check-use sequence below is NOT atomic
743
- // under concurrent modification. While individual JS Map operations are atomic,
744
- // the sequence spanning multiple filesystem operations is not atomic. Callers MUST
745
- // NOT invoke loadRunManifestById concurrently with cache-modifying operations
746
- // (setManifestCache, invalidateRunCache) for the same stateRoot. The generation
747
- // counter provides some protection, but does not make the read-check-use atomic.
748
- let tasksStat: fs.Stats | undefined;
749
- try {
750
- tasksStat = fs.statSync(tasksPath);
751
- } catch {
752
- tasksStat = undefined;
753
- }
754
- const tasksMtimeMs = tasksStat?.mtimeMs ?? 0;
755
- if (
756
- cached &&
757
- cached.manifestMtimeMs === manifestStat.mtimeMs &&
758
- cached.manifestSize === manifestStat.size &&
759
- cached.tasksMtimeMs === tasksMtimeMs &&
760
- cached.tasksSize === (tasksStat?.size ?? 0) &&
761
- cached.generation === genOf(stateRoot)
762
- ) {
763
- // TTL eviction: expire stale entries even if mtime matches
764
- // FIX: Also evict entries where cachedAt is undefined — such entries are
765
- // effectively immortal otherwise (the `cached.cachedAt &&` check would skip
766
- // them every time). This can happen if a cache entry was created by
767
- // setManifestCache that didn't set cachedAt (shouldn't happen in current
768
- // code, but defensive against future regressions).
769
- if (!cached.cachedAt || Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
770
- manifestCache.delete(stateRoot);
771
- } else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
772
- manifestCache.delete(stateRoot);
773
- return undefined;
774
- } else if (!fs.existsSync(tasksPath)) {
775
- // Tasks file was deleted after cache was populated — this is a cache miss,
776
- // not a manifest inconsistency. The cache check passes because
777
- // tasksMtimeMs=0 and tasksSize=0 match a cache entry written when tasks.json
778
- // didn't exist. We fall through to the retry loop which will correctly
779
- // detect the missing file and return undefined. The alternative (treating
780
- // this as an inconsistency) would require failing the cache hit path, which
781
- // is unnecessary since the retry loop handles it correctly anyway.
782
- manifestCache.delete(stateRoot);
783
- return undefined;
784
- } else {
785
- return { manifest: cached.manifest, tasks: cached.tasks ?? [] };
786
- }
787
- }
788
-
789
- // FIX: Sentinel-based retry loop for best-effort consistency. Re-stat and
790
- // re-read until mtime/size are stable. The retry limit is now configurable
791
- // via LOAD_MANIFEST_RETRY_LIMIT (was hardcoded 3). High contention can still
792
- // cause non-convergence — for strict consistency, callers MUST use withRunLock().
793
- // Issue 3 fix: Made retry limit configurable instead of hardcoded 3.
794
- let attempts = 0;
795
- let manifest: TeamRunManifest | undefined;
796
- let tasks: TeamTaskState[] | undefined;
797
- while (attempts < LOAD_MANIFEST_RETRY_LIMIT) {
798
- const freshStat = fs.statSync(manifestPath);
799
- manifest = readJsonFile<TeamRunManifest>(manifestPath);
800
- const freshTasksStat = fs.existsSync(tasksPath) ? fs.statSync(tasksPath) : undefined;
801
- tasks = readJsonFile<TeamTaskState[]>(tasksPath) ?? [];
802
- // If size/mtime didn't change between stat and read, we're consistent.
803
- if (
804
- freshStat.mtimeMs === manifestStat.mtimeMs &&
805
- freshStat.size === manifestStat.size &&
806
- (!freshTasksStat || (freshTasksStat.mtimeMs === tasksStat?.mtimeMs && freshTasksStat.size === tasksStat?.size))
807
- ) {
808
- break;
809
- }
810
- attempts += 1;
811
- manifestStat = freshStat;
812
- tasksStat = freshTasksStat;
813
- }
814
- // WARNING: Best-effort consistency only — retry loop detected mtime/size
815
- // instability. A concurrent writer can still complete a full write cycle
816
- // between the final stat and the read. Callers needing strict consistency
817
- // MUST use withRunLock() around load+modify+save.
818
- if (attempts > 0) {
819
- // Round 19: downgrade to debug — retry-loop instability is expected under
820
- // concurrent writes (live team runs constantly append to tasks.json).
821
- // This is best-effort by design; strict consistency requires withRunLock().
822
- console.debug(
823
- `[state-store] loadRunManifestById: retry loop detected instability for run ${runId} after ${attempts} attempt(s) — best-effort only, use withRunLock() for strict consistency`,
824
- );
825
- }
826
- // NOTE: manifest mtime may legitimately be >= tasks mtime because
827
- // saveManifestAndTasksAtomicSync writes manifest before tasks. However,
828
- // if a crash occurs AFTER tasks is written but BEFORE manifest is written,
829
- // tasks mtime would be > manifest mtime (the opposite). The retry loop
830
- // above detects this crash state by re-reading until mtime/size are stable
831
- // — the final stable state is what gets used. Because the retry loop
832
- // handles the crash case, we do NOT fail based on this comparison alone.
833
- // It does not indicate corruption on its own.
834
- if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
835
- setManifestCache(stateRoot, {
836
- manifest,
837
- tasks: tasks ?? [],
838
- manifestMtimeMs: manifestStat.mtimeMs,
839
- manifestSize: manifestStat.size,
840
- tasksMtimeMs,
841
- tasksSize: tasksStat?.size ?? 0,
842
- });
843
- return { manifest, tasks: tasks ?? [] };
844
- }
845
-
846
- export async function loadRunManifestByIdAsync(
847
- cwd: string,
848
- runId: string,
849
- ): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined> {
850
- const stateRoot = resolveRunStateRoot(cwd, runId);
851
- if (!stateRoot) return undefined;
852
- const manifestPath = path.join(stateRoot, "manifest.json");
853
- const tasksPath = path.join(stateRoot, "tasks.json");
854
-
855
- let manifestStat: fs.Stats;
856
- try {
857
- manifestStat = await fs.promises.stat(manifestPath);
858
- } catch {
859
- return undefined;
860
- }
861
- const cached = manifestCache.get(stateRoot);
862
- // Issue 1 fix: Note that the cache read-check-use sequence below is NOT atomic
863
- // under concurrent modification. Same as sync version — see loadRunManifestById.
864
- let tasksStat: fs.Stats | undefined;
865
- try {
866
- tasksStat = await fs.promises.stat(tasksPath);
867
- } catch {
868
- tasksStat = undefined;
869
- }
870
- const tasksMtimeMs = tasksStat?.mtimeMs ?? 0;
871
- if (
872
- cached &&
873
- cached.manifestMtimeMs === manifestStat.mtimeMs &&
874
- cached.manifestSize === manifestStat.size &&
875
- cached.tasksMtimeMs === tasksMtimeMs &&
876
- cached.tasksSize === (tasksStat?.size ?? 0) &&
877
- cached.generation === genOf(stateRoot)
878
- ) {
879
- // TTL eviction: expire stale entries even if mtime matches
880
- // FIX: Also evict entries where cachedAt is undefined — such entries are
881
- // effectively immortal otherwise (the `cached.cachedAt &&` check would skip
882
- // them every time). This can happen if a cache entry was created by
883
- // setManifestCache that didn't set cachedAt (shouldn't happen in current
884
- // code, but defensive against future regressions).
885
- if (!cached.cachedAt || Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
886
- manifestCache.delete(stateRoot);
887
- } else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
888
- manifestCache.delete(stateRoot);
889
- return undefined;
890
- } else if (
891
- !(await fsp.access(tasksPath).then(
892
- () => true,
893
- () => false,
894
- ))
895
- ) {
896
- // Tasks file was deleted after cache was populated — do not serve stale cache.
897
- manifestCache.delete(stateRoot);
898
- return undefined;
899
- } else {
900
- return { manifest: cached.manifest, tasks: cached.tasks ?? [] };
901
- }
902
- }
903
-
904
- // FIX: Sentinel-based retry loop to close TOCTOU window between stat and read.
905
- // Matches the pattern used in the sync loadRunManifestById.
906
- // Issue 3 fix: Made retry limit configurable instead of hardcoded 3.
907
- let manifest: TeamRunManifest | undefined;
908
- let tasks: TeamTaskState[] | undefined;
909
- let attempts = 0;
910
- while (attempts < LOAD_MANIFEST_RETRY_LIMIT) {
911
- const freshStat = await fs.promises.stat(manifestPath);
912
- manifest = await readJsonFileAsync<TeamRunManifest>(manifestPath);
913
- const freshTasksStat = await fs.promises.stat(tasksPath).catch(() => undefined);
914
- tasks = (await readJsonFileAsync<TeamTaskState[]>(tasksPath)) ?? [];
915
- // If size/mtime didn't change between stat and read, we're consistent.
916
- if (
917
- freshStat.mtimeMs === manifestStat.mtimeMs &&
918
- freshStat.size === manifestStat.size &&
919
- (!freshTasksStat || (freshTasksStat.mtimeMs === tasksStat?.mtimeMs && freshTasksStat.size === tasksStat?.size))
920
- ) {
921
- break;
922
- }
923
- attempts += 1;
924
- manifestStat = freshStat;
925
- tasksStat = freshTasksStat;
926
- }
927
- // WARNING: Best-effort consistency only — retry loop detected mtime/size
928
- // instability. A concurrent writer can still complete a full write cycle
929
- // between the final stat and the read. Callers needing strict consistency
930
- // MUST use withRunLock() around load+modify+save.
931
- if (attempts > 0) {
932
- // Round 19: downgrade to debug — retry-loop instability is expected under
933
- // concurrent writes (live team runs constantly append to tasks.json).
934
- // This is best-effort by design; strict consistency requires withRunLock().
935
- console.debug(
936
- `[state-store] loadRunManifestByIdAsync: retry loop detected instability for run ${runId} after ${attempts} attempt(s) — best-effort only, use withRunLock() for strict consistency`,
937
- );
938
- }
939
- // NOTE: manifest mtime may legitimately be >= tasks mtime because
940
- // saveManifestAndTasksAtomicSync writes manifest before tasks. However,
941
- // if a crash occurs AFTER tasks is written but BEFORE manifest is written,
942
- // tasks mtime would be > manifest mtime (the opposite). The retry loop
943
- // above detects this crash state by re-reading until mtime/size are stable
944
- // — the final stable state is what gets used. Because the retry loop
945
- // handles the crash case, we do NOT fail based on this comparison alone.
946
- // It does not indicate corruption on its own.
947
-
948
- if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
949
- setManifestCache(stateRoot, {
950
- manifest,
951
- tasks: tasks ?? [],
952
- manifestMtimeMs: manifestStat.mtimeMs,
953
- manifestSize: manifestStat.size,
954
- tasksMtimeMs,
955
- tasksSize: tasksStat?.size ?? 0,
956
- });
957
- return { manifest, tasks: tasks ?? [] };
958
- }