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
@@ -5,12 +5,12 @@
5
5
  */
6
6
 
7
7
  import { listRecentRuns } from "../../extension/run-index.ts";
8
- import { reconcileAllStaleRuns } from "../../runtime/crash-recovery.ts";
9
8
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
10
9
  import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
11
- import { evictStaleLiveAgentHandles } from "../../runtime/live-agent-manager.ts";
10
+ import { evictStaleLiveAgentHandles } from "../../runtime/live-session/live-agent-manager.ts";
12
11
  import type { ManifestCache } from "../../runtime/manifest-cache.ts";
13
12
  import { isDisplayActiveRun } from "../../runtime/process-status.ts";
13
+ import { reconcileAllStaleRuns } from "../../runtime/recovery/crash-recovery.ts";
14
14
  import type { TeamRunManifest } from "../../state/types.ts";
15
15
  import type { RunSnapshotCache } from "../snapshot-types.ts";
16
16
  import type { WidgetRun } from "./widget-types.ts";
@@ -4,7 +4,7 @@
4
4
  * Extracted from crew-widget.ts.
5
5
  */
6
6
 
7
- import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
7
+ import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
8
8
  import { isFinishedRunStatus } from "../../runtime/process-status.ts";
9
9
  import { truncate } from "../../utils/visual.ts";
10
10
  import { Box, Text } from "../layout-primitives.ts";
@@ -341,11 +341,20 @@ export function parseConflictUri(raw: string): ParsedConflictUri | null {
341
341
  * Works by locating the recorded marker block by content (anchored to
342
342
  * `entry.startLine` as the preferred match), so out-of-band edits earlier
343
343
  * in the file that shift line numbers don't break resolution.
344
+ *
345
+ * EOL preservation: lines are split keeping each line's original ending
346
+ * bytes (CRLF / LF / none). Only the conflicting region is rewritten; every
347
+ * other line is round-tripped byte-for-byte, so a mixed-EOL file produces a
348
+ * minimal git diff (previously every line ending was normalised, rewriting
349
+ * the whole file even when only a few lines conflicted).
344
350
  */
345
351
  export function spliceConflict(originalText: string, entry: ConflictEntry, replacement: string): string {
346
- // Preserve the original line ending (CRLF or LF) for a byte-accurate round-trip.
347
- const eol = originalText.includes("\r\n") ? "\r\n" : "\n";
348
- const lines = originalText.split(/\r?\n/);
352
+ // Split preserving each line's original EOL so non-conflicting lines are
353
+ // round-tripped byte-for-byte (minimal git diff on mixed-EOL files).
354
+ const segments = splitPreservingEol(originalText);
355
+ // Content-only view (CR stripped) — identical to the old `split(/\r?\n/)`,
356
+ // so conflict detection / region matching behaviour is unchanged.
357
+ const lines = segments.map((s) => s.content);
349
358
  const expected = buildRecordedRegion(entry);
350
359
  const match = locateRegion(lines, expected, entry.startLine - 1);
351
360
  if (!match) {
@@ -356,8 +365,52 @@ export function spliceConflict(originalText: string, entry: ConflictEntry, repla
356
365
 
357
366
  const trimmed = normalizeTrailingNewline(replacement);
358
367
  const replacementLines = trimmed.split("\n");
359
- const next = [...lines.slice(0, match.startIdx), ...replacementLines, ...lines.slice(match.endIdx + 1)];
360
- return next.join(eol);
368
+ // EOL for the inserted lines: prefer the EOL that originally terminated the
369
+ // conflict region (local style); fall back to the file's dominant EOL.
370
+ const regionEol = segments[match.endIdx]?.eol ?? "";
371
+ const interEol = regionEol || (originalText.includes("\r\n") ? "\r\n" : "\n");
372
+
373
+ const out: string[] = [];
374
+ // Lines before the conflict — verbatim (original content + original EOL).
375
+ for (let i = 0; i < match.startIdx; i++) out.push(segments[i].content, segments[i].eol);
376
+ // Replacement lines — intermediate ones use the local EOL; the final one
377
+ // reuses the boundary EOL that originally separated the region from the
378
+ // following line, preserving that line's leading boundary exactly.
379
+ for (let i = 0; i < replacementLines.length; i++) {
380
+ out.push(replacementLines[i]);
381
+ out.push(i < replacementLines.length - 1 ? interEol : regionEol);
382
+ }
383
+ // Lines after the conflict — verbatim.
384
+ for (let i = match.endIdx + 1; i < segments.length; i++) out.push(segments[i].content, segments[i].eol);
385
+ return out.join("");
386
+ }
387
+
388
+ /**
389
+ * Split `text` into `{ content, eol }` segments keeping each line's exact
390
+ * ending bytes. The `content` view equals `text.split(/\r?\n/)`, but the
391
+ * original EOL (`"\r\n"`, `"\n"`, or `""` for the final line) is retained
392
+ * so callers can reconstruct the text byte-for-byte.
393
+ */
394
+ function splitPreservingEol(text: string): { content: string; eol: string }[] {
395
+ const segments: { content: string; eol: string }[] = [];
396
+ const n = text.length;
397
+ let i = 0;
398
+ while (i <= n) {
399
+ const nl = text.indexOf("\n", i);
400
+ if (nl === -1) {
401
+ segments.push({ content: text.slice(i), eol: "" });
402
+ break;
403
+ }
404
+ let contentEnd = nl;
405
+ let eol = "\n";
406
+ if (nl > 0 && text.charCodeAt(nl - 1) === 13 /* \r */) {
407
+ contentEnd = nl - 1;
408
+ eol = "\r\n";
409
+ }
410
+ segments.push({ content: text.slice(i, contentEnd), eol });
411
+ i = nl + 1;
412
+ }
413
+ return segments;
361
414
  }
362
415
 
363
416
  /** Reconstruct the recorded marker block as it should appear in the file. */
@@ -2,7 +2,7 @@
2
2
  * ndjson.ts — Canonical broker NDJSON framing primitives (encoder + decoder
3
3
  * + typed errors). Newline-delimited JSON, one frame per `\\n`.
4
4
  *
5
- * Moved out of the parallel-work stub `src/runtime/crew-broker-deps.ts`.
5
+ * Moved out of the parallel-work stub `src/runtime/broker/crew-broker-deps.ts`.
6
6
  * The public surface (MAX_BROKER_FRAME_BYTES, BrokerError, BrokerErrorCode,
7
7
  * encodeBrokerFrame, NdjsonDecoder) is preserved verbatim so importers
8
8
  * can be updated with a single import-path change.
@@ -112,4 +112,4 @@ export class NdjsonDecoder {
112
112
  // External re-exports
113
113
  // ============================================================================
114
114
  // Re-exported here for callers that import all broker primitives from ndjson.ts.
115
- export { newBrokerToken } from "../runtime/crew-broker-tokens.ts";
115
+ export { newBrokerToken } from "../runtime/broker/crew-broker-tokens.ts";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * socket-path.ts — Canonical broker Unix-socket / named-pipe path utilities.
3
3
  *
4
- * Moved out of the parallel-work stub `src/runtime/crew-broker-deps.ts`.
4
+ * Moved out of the parallel-work stub `src/runtime/broker/crew-broker-deps.ts`.
5
5
  * The public surface (hashSessionId, getBrokerSocketPath,
6
6
  * prepareBrokerSocketDir, removeStaleBrokerSocket) is preserved verbatim
7
7
  * so importers can be updated with a single import-path change.
@@ -63,7 +63,7 @@ export interface WorkflowConfig {
63
63
  * multiple ready tasks sharing role + cwd are grouped into a single
64
64
  * multi-task worker prompt to reduce per-task cold-start cost. Disabled
65
65
  * in v0.9.17 unless the workflow author opts in. See
66
- * src/runtime/coalesce-tasks.ts. */
66
+ * src/runtime/scheduling/coalesce-tasks.ts. */
67
67
  coalesceMicroTasks?: boolean;
68
68
  }
69
69
 
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { DEFAULT_PATHS } from "../config/defaults.ts";
5
- import { writeArtifact } from "../state/artifact-store.ts";
5
+ import { writeArtifact } from "../state/stores/artifact-store.ts";
6
6
  import type { TeamRunManifest } from "../state/types.ts";
7
7
  import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
8
8
  import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
@@ -5,7 +5,7 @@ import * as path from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { loadConfig } from "../config/config.ts";
7
7
  import { DEFAULT_PATHS } from "../config/defaults.ts";
8
- import { writeArtifact } from "../state/artifact-store.ts";
8
+ import { writeArtifact } from "../state/stores/artifact-store.ts";
9
9
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
10
10
  import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
11
11
  import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
@@ -583,7 +583,7 @@ export function overlaySeedPaths(repoRoot: string, worktreePath: string, seedPat
583
583
  * recovered via `git apply` / manual restore. Best-effort: a snapshot failure
584
584
  * only logs (it must not block the clean-slate reuse flow).
585
585
  */
586
- function snapshotDirtyWorktree(manifest: TeamRunManifest, task: TeamTaskState, worktreePath: string, dirtyStatus: string): boolean {
586
+ export function snapshotDirtyWorktree(manifest: TeamRunManifest, task: TeamTaskState, worktreePath: string, dirtyStatus: string): boolean {
587
587
  try {
588
588
  const parts: string[] = [
589
589
  `# Worktree recovery snapshot`,
@@ -591,15 +591,18 @@ function snapshotDirtyWorktree(manifest: TeamRunManifest, task: TeamTaskState, w
591
591
  `capturedAt: ${new Date().toISOString()}`,
592
592
  "",
593
593
  ];
594
+ // ST-1: use --binary so tracked binary modifications are recoverable via `git apply`.
594
595
  let trackedDiff = "";
595
596
  try {
596
- trackedDiff = git(worktreePath, ["diff", "HEAD"]);
597
+ trackedDiff = git(worktreePath, ["diff", "HEAD", "--binary"]);
597
598
  } catch {
598
599
  trackedDiff = "";
599
600
  }
600
601
  if (trackedDiff.trim()) {
601
- parts.push("## Tracked changes (`git diff HEAD`)", "```diff", trackedDiff, "```", "");
602
+ parts.push("## Tracked changes (`git diff HEAD --binary`)", "```diff", trackedDiff, "```", "");
602
603
  }
604
+ // ST-1: per-file byte cap — skip/truncate larger files with a note to avoid OOM.
605
+ const MAX_FILE_BYTES = 256 * 1024;
603
606
  for (const line of dirtyStatus.split("\n")) {
604
607
  if (!line.startsWith("?? ")) continue;
605
608
  // Strip the "?? " prefix and surrounding git quotes.
@@ -608,8 +611,27 @@ function snapshotDirtyWorktree(manifest: TeamRunManifest, task: TeamTaskState, w
608
611
  try {
609
612
  const abs = path.join(worktreePath, rel);
610
613
  if (!fs.existsSync(abs) || fs.statSync(abs).isDirectory()) continue;
611
- const content = fs.readFileSync(abs, "utf-8");
612
- parts.push(`## Untracked file: ${rel}`, "```", content, "```", "");
614
+ // ST-1: read raw bytes (not utf-8) so binary files are not corrupted.
615
+ const buf = fs.readFileSync(abs);
616
+ const originalSize = buf.byteLength;
617
+ let data: Buffer = buf;
618
+ let note = "";
619
+ if (originalSize > MAX_FILE_BYTES) {
620
+ data = buf.subarray(0, MAX_FILE_BYTES);
621
+ note = ` (truncated: ${originalSize} → ${MAX_FILE_BYTES} bytes)`;
622
+ }
623
+ // ST-1: detect non-UTF-8 via fatal TextDecoder; base64-encode binary files.
624
+ let isUtf8 = true;
625
+ try {
626
+ new TextDecoder("utf-8", { fatal: true }).decode(data);
627
+ } catch {
628
+ isUtf8 = false;
629
+ }
630
+ if (isUtf8) {
631
+ parts.push(`## Untracked file: ${rel}${note}`, "```", data.toString("utf-8"), "```", "");
632
+ } else {
633
+ parts.push(`## Untracked file: ${rel}${note} (base64-encoded binary)`, "```base64", data.toString("base64"), "```", "");
634
+ }
613
635
  } catch {
614
636
  /* skip unreadable/unstat-able entry */
615
637
  }
@@ -744,8 +766,8 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
744
766
  if (currentBranch !== branch) {
745
767
  throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
746
768
  }
747
- // Check for uncommitted changes from previous run before reusing
748
- const dirtyStatus = git(worktreePath, ["status", "--porcelain"]);
769
+ // ST-1: use -uall so untracked directories are expanded to individual files.
770
+ const dirtyStatus = git(worktreePath, ["status", "--porcelain", "-uall"]);
749
771
  if (dirtyStatus.trim()) {
750
772
  // Snapshot uncommitted work to a recovery artifact BEFORE discarding, so the
751
773
  // previous run's changes are never silently destroyed on reuse.
@@ -904,7 +926,8 @@ export async function prepareTaskWorkspaceAsync(
904
926
  if (currentBranch !== branch) {
905
927
  throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
906
928
  }
907
- const dirtyStatus = await gitAsync(worktreePath, ["status", "--porcelain"]);
929
+ // ST-1: use -uall so untracked directories are expanded to individual files.
930
+ const dirtyStatus = await gitAsync(worktreePath, ["status", "--porcelain", "-uall"]);
908
931
  if (dirtyStatus.trim()) {
909
932
  const snapshotOk = snapshotDirtyWorktree(manifest, task, worktreePath, dirtyStatus);
910
933
  if (snapshotOk) {
@@ -1060,52 +1083,78 @@ export async function captureWorktreeDiffAsync(worktreePath: string): Promise<st
1060
1083
  * assertCleanLeader, pruneStaleWorktrees, sanitizeBranchPart,
1061
1084
  * linkNodeModulesIfPresent) but with a minimal, task-free signature.
1062
1085
  *
1063
- * Returns `undefined` when worktree creation is unavailable (no git repo, dirty
1064
- * leader, git error) so the caller (`ctx.agent`) can fall back gracefully.
1086
+ * Returns `undefined` when worktree isolation is genuinely unavailable (no git
1087
+ * repo, dirty leader) so the caller (`ctx.agent`) can fall back gracefully.
1088
+ * ST-15: when creation was attempted but failed (e.g. disk full), this THROWS
1089
+ * instead of returning undefined so the caller never silently runs in the
1090
+ * leader repo. The branch + path suffix is non-deterministic so a 2nd call for
1091
+ * the same agentId always produces a distinct worktree.
1065
1092
  */
1066
1093
  export function prepareAgentWorktree(manifest: TeamRunManifest, agentId: string): PreparedTaskWorkspace | undefined {
1094
+ // ST-15: Split preconditions from worktree creation.
1095
+ // Precondition failures (no git repo, dirty leader) → return undefined so the
1096
+ // caller falls back gracefully (worktree isolation is genuinely unavailable).
1097
+ let repoRoot: string;
1098
+ let loadedConfig: ReturnType<typeof loadConfig>;
1067
1099
  try {
1068
- const repoRoot = findGitRoot(manifest.cwd);
1069
- const loadedConfig = loadConfig(manifest.cwd);
1100
+ repoRoot = findGitRoot(manifest.cwd);
1101
+ loadedConfig = loadConfig(manifest.cwd);
1070
1102
  if (loadedConfig.config.requireCleanWorktreeLeader !== false) assertCleanLeader(repoRoot);
1071
- const sanitizedRunId = manifest.runId.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "run";
1072
- const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, sanitizedRunId);
1073
- fs.mkdirSync(worktreeRoot, { recursive: true });
1074
- const sanitizedAgentId = sanitizeBranchPart(agentId);
1075
- const worktreePath = path.join(worktreeRoot, sanitizedAgentId);
1076
- const branch = `pi-crew/${sanitizedRunId}/${sanitizedAgentId}`;
1077
- pruneStaleWorktrees(repoRoot);
1078
- git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
1079
- const nodeModulesLinked =
1080
- loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
1081
- return { cwd: worktreePath, worktreePath, branch, nodeModulesLinked };
1082
1103
  } catch {
1083
- // Graceful fallback: no git repo, dirty leader, or git error → run normally.
1104
+ // Graceful fallback: no git repo or dirty leader → run normally in cwd.
1084
1105
  return undefined;
1085
1106
  }
1107
+
1108
+ // ST-15: NON-DETERMINISTIC branch + path suffix. The OLD deterministic name
1109
+ // (`pi-crew/<runId>/<agentId>`) made a 2nd call for the same agent collide
1110
+ // ("already exists") → catch → returned undefined → the run silently fell
1111
+ // back to the leader repo (isolation lost, no error). The timestamp +
1112
+ // short-uuid suffix makes every call produce a distinct worktree.
1113
+ // Creation failures here THROW (not undefined) so callers never silently lose
1114
+ // isolation when worktree creation was requested but failed.
1115
+ const sanitizedRunId = manifest.runId.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "run";
1116
+ const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, sanitizedRunId);
1117
+ fs.mkdirSync(worktreeRoot, { recursive: true });
1118
+ const sanitizedAgentId = sanitizeBranchPart(agentId);
1119
+ const stamp = `${Date.now()}-${randomBytes(4).toString("hex")}`;
1120
+ const worktreePath = path.join(worktreeRoot, `${sanitizedAgentId}-${stamp}`);
1121
+ const branch = `pi-crew/${sanitizedRunId}/${sanitizedAgentId}-${stamp}`;
1122
+ pruneStaleWorktrees(repoRoot);
1123
+ git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
1124
+ const nodeModulesLinked =
1125
+ loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
1126
+ return { cwd: worktreePath, worktreePath, branch, nodeModulesLinked };
1086
1127
  }
1087
1128
 
1088
1129
  /** Async version of prepareAgentWorktree — yields the event loop during git operations. */
1089
1130
  export async function prepareAgentWorktreeAsync(manifest: TeamRunManifest, agentId: string): Promise<PreparedTaskWorkspace | undefined> {
1131
+ // ST-15: Split preconditions from worktree creation (see prepareAgentWorktree).
1132
+ let repoRoot: string;
1133
+ let loadedConfig: ReturnType<typeof loadConfig>;
1090
1134
  try {
1091
- const repoRoot = await findGitRootAsync(manifest.cwd);
1092
- const loadedConfig = loadConfig(manifest.cwd);
1135
+ repoRoot = await findGitRootAsync(manifest.cwd);
1136
+ loadedConfig = loadConfig(manifest.cwd);
1093
1137
  if (loadedConfig.config.requireCleanWorktreeLeader !== false) await assertCleanLeaderAsync(repoRoot);
1094
- const sanitizedRunId = manifest.runId.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "run";
1095
- const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, sanitizedRunId);
1096
- fs.mkdirSync(worktreeRoot, { recursive: true });
1097
- const sanitizedAgentId = sanitizeBranchPart(agentId);
1098
- const worktreePath = path.join(worktreeRoot, sanitizedAgentId);
1099
- const branch = `pi-crew/${sanitizedRunId}/${sanitizedAgentId}`;
1100
- await pruneStaleWorktreesAsync(repoRoot);
1101
- await gitAsync(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
1102
- const nodeModulesLinked =
1103
- loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
1104
- return { cwd: worktreePath, worktreePath, branch, nodeModulesLinked };
1105
1138
  } catch {
1106
- // Graceful fallback: no git repo, dirty leader, or git error → run normally.
1139
+ // Graceful fallback: no git repo or dirty leader → run normally in cwd.
1107
1140
  return undefined;
1108
1141
  }
1142
+
1143
+ // ST-15: NON-DETERMINISTIC branch + path suffix — a 2nd call for the same
1144
+ // agentId creates a DISTINCT worktree instead of colliding. Creation failures
1145
+ // here THROW so callers never silently fall back to the leader repo.
1146
+ const sanitizedRunId = manifest.runId.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "run";
1147
+ const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, sanitizedRunId);
1148
+ fs.mkdirSync(worktreeRoot, { recursive: true });
1149
+ const sanitizedAgentId = sanitizeBranchPart(agentId);
1150
+ const stamp = `${Date.now()}-${randomBytes(4).toString("hex")}`;
1151
+ const worktreePath = path.join(worktreeRoot, `${sanitizedAgentId}-${stamp}`);
1152
+ const branch = `pi-crew/${sanitizedRunId}/${sanitizedAgentId}-${stamp}`;
1153
+ await pruneStaleWorktreesAsync(repoRoot);
1154
+ await gitAsync(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
1155
+ const nodeModulesLinked =
1156
+ loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
1157
+ return { cwd: worktreePath, worktreePath, branch, nodeModulesLinked };
1109
1158
  }
1110
1159
 
1111
1160
  /**
@@ -6,7 +6,7 @@ topology: dynamic
6
6
 
7
7
  # Chain Workflow - Sequential execution with context passing
8
8
 
9
- **Source:** `docs/pi-boomerang-integration-plan.md`
9
+ **Source:** pi-boomerang integration plan
10
10
  **Syntax:** `step1 -> step2 -> step3`
11
11
  **Version:** 1.0.0
12
12
 
@@ -255,4 +255,4 @@ pi team run --chain "@step1 -> @step2" --continue-on-error
255
255
 
256
256
  ---
257
257
 
258
- *Generated from pi-boomerang integration plan. See `docs/pi-boomerang-integration-plan.md` for full specification.*
258
+ *Generated from pi-boomerang integration plan.*
package/index.bundle.ts DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * Bundle entry point — NO shell logic.
3
- *
4
- * Bundled by `scripts/build-bundle.mjs` into `dist/index.mjs`. Keep this
5
- * file minimal — the bundle should NOT try to recursively load itself
6
- * or do path resolution. The shell (load-with-fallback) lives in
7
- * `index.ts`; this file just re-exports the inner extension API so the
8
- * bundle is a single self-contained module the shell can `import()`.
9
- *
10
- * Why a separate file from `index.ts`:
11
- * - `index.ts` runs at the package root and resolves `dist/index.mjs`
12
- * relative to its own location.
13
- * - If the bundle were built FROM `index.ts`, the bundled code would
14
- * resolve `dist/index.mjs` relative to ITS OWN location (inside
15
- * `dist/`), producing `dist/dist/index.mjs` and a recursion error.
16
- * - Building from `index.bundle.ts` (which has no shell) sidesteps
17
- * this chicken-and-egg entirely.
18
- */
19
- import { registerPiTeams } from "./src/extension/register.ts";
20
- import { waitForRun } from "./src/runtime/run-tracker.ts";
21
-
22
- export { waitForRun, registerPiTeams };
23
- export default function (pi: Parameters<typeof registerPiTeams>[0]): void {
24
- registerPiTeams(pi);
25
- }
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Compare test/bench/results.json against test/bench/baseline.json.
4
- *
5
- * Fails if any p95 metric regresses by more than the allowed threshold.
6
- *
7
- * Usage:
8
- * npm run bench:check # default 15% threshold
9
- * THRESHOLD_PCT=10 npm run bench:check # tighter
10
- * BASELINE=test/bench/baseline.json npm run bench:check
11
- */
12
- import * as fs from "node:fs";
13
- import * as path from "node:path";
14
-
15
- const root = path.resolve(import.meta.dirname, "..");
16
- const benchDir = path.join(root, "test", "bench");
17
- const resultsPath = path.join(benchDir, "results.json");
18
- const baselinePath = process.env.BASELINE ? path.resolve(process.env.BASELINE) : path.join(benchDir, "baseline.json");
19
- const threshold = Number(process.env.THRESHOLD_PCT ?? 15) / 100;
20
-
21
- if (!fs.existsSync(resultsPath)) {
22
- console.error(`[bench:check] missing ${resultsPath}; run \`npm run bench\` first.`);
23
- process.exit(2);
24
- }
25
- if (!fs.existsSync(baselinePath)) {
26
- console.error(`[bench:check] missing baseline ${baselinePath}.`);
27
- console.error(" Capture baseline with: npm run bench && cp test/bench/results.json test/bench/baseline.json");
28
- process.exit(2);
29
- }
30
-
31
- const results = JSON.parse(fs.readFileSync(resultsPath, "utf-8"));
32
- const baseline = JSON.parse(fs.readFileSync(baselinePath, "utf-8"));
33
-
34
- /**
35
- * Walk each bench's nested numeric metrics and pair with baseline.
36
- * Reports any p95 metric where current > baseline * (1 + threshold).
37
- */
38
- const regressions = [];
39
- const improvements = [];
40
- for (const [benchName, current] of Object.entries(results.results)) {
41
- const base = baseline.results?.[benchName];
42
- if (!base) {
43
- console.log(`[bench:check] no baseline for ${benchName} — skipping`);
44
- continue;
45
- }
46
- for (const metric of metricKeys(current)) {
47
- const cur = readPath(current, metric);
48
- const ref = readPath(base, metric);
49
- if (typeof cur !== "number" || typeof ref !== "number" || ref === 0) continue;
50
- // Sub-ms metrics are dominated by GC / OS jitter — applying a strict
51
- // percentage gate causes false positives. Use absolute delta cap instead.
52
- if (ref < 1.0) {
53
- const absDelta = cur - ref;
54
- if (absDelta > 0.5) regressions.push({ bench: benchName, metric: metric.join("."), baseline: ref, current: cur, deltaPct: round(((cur - ref) / ref) * 100), reason: "abs > 0.5ms" });
55
- else if (absDelta < -0.05) improvements.push({ bench: benchName, metric: metric.join("."), baseline: ref, current: cur, deltaPct: round(((cur - ref) / ref) * 100) });
56
- continue;
57
- }
58
- const delta = (cur - ref) / ref;
59
- if (delta > threshold) regressions.push({ bench: benchName, metric: metric.join("."), baseline: ref, current: cur, deltaPct: round(delta * 100) });
60
- else if (delta < -0.05) improvements.push({ bench: benchName, metric: metric.join("."), baseline: ref, current: cur, deltaPct: round(delta * 100) });
61
- }
62
- }
63
-
64
- if (improvements.length) {
65
- console.log("[bench:check] improvements:");
66
- for (const item of improvements) console.log(` ${item.bench}.${item.metric}: ${item.baseline} → ${item.current} (${item.deltaPct}%)`);
67
- }
68
- if (regressions.length) {
69
- console.error(`[bench:check] regressions (>${(threshold * 100).toFixed(0)}%):`);
70
- for (const item of regressions) console.error(` ${item.bench}.${item.metric}: ${item.baseline} → ${item.current} (+${item.deltaPct}%)`);
71
- process.exit(1);
72
- }
73
- console.log(`[bench:check] all benches within ${(threshold * 100).toFixed(0)}% of baseline.`);
74
-
75
- function metricKeys(obj, prefix = []) {
76
- const skip = new Set(["name", "unit", "iters", "tasks", "events", "eventsPerIter"]);
77
- const keys = [];
78
- for (const [k, v] of Object.entries(obj)) {
79
- if (skip.has(k)) continue;
80
- if (k === "p50" || k === "p95" || k === "p99" || k === "min" || k === "max") {
81
- // We only gate on p95; min/max printed but not enforced.
82
- if (k === "p95") keys.push([...prefix, k]);
83
- continue;
84
- }
85
- if (v && typeof v === "object" && !Array.isArray(v)) keys.push(...metricKeys(v, [...prefix, k]));
86
- }
87
- return keys;
88
- }
89
-
90
- function readPath(obj, parts) {
91
- let cur = obj;
92
- for (const p of parts) cur = cur?.[p];
93
- return cur;
94
- }
95
-
96
- function round(n) { return Math.round(n * 100) / 100; }