@huseynsnmz/pi-board 0.15.3 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +24 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +55 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +277 -0
  7. package/package.json +5 -11
  8. package/src/commands/agent-board.ts +26 -2
  9. package/src/commands/attach-flow.ts +4 -0
  10. package/src/core/double-press.mjs +6 -2
  11. package/src/core/slash-commands.mjs +52 -0
  12. package/src/index.ts +1 -1
  13. package/src/runtime/service.mjs +12 -0
  14. package/src/ui/dashboard.ts +59 -25
  15. package/src/ui/pty-attach.ts +8 -3
  16. package/IMPLEMENTATION_PLAN.md +0 -920
  17. package/PRD.md +0 -484
  18. package/PROGRESS.md +0 -142
  19. package/VERIFY.md +0 -115
  20. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  21. package/docs/EXPLORATION.md +0 -187
  22. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  23. package/docs/RELEASE.md +0 -148
  24. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  25. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  26. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  27. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  28. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  29. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  30. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  31. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  32. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  33. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  34. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  35. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  36. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  37. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  38. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  39. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  40. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  41. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  42. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  43. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  44. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  45. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  46. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  47. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  48. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  49. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  50. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  51. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  52. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  53. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  54. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  55. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  56. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  57. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  58. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  59. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  60. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  61. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  62. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  63. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  64. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  65. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  66. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  67. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  68. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  69. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  70. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  71. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  72. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  73. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  74. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  75. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  76. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  77. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  78. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  79. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  80. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  81. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  82. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  83. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  84. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  85. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  86. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  87. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  88. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  89. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  90. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  91. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  92. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  93. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  94. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  95. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  96. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  97. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  98. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  99. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  100. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  101. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  102. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  103. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  104. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  105. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  106. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  107. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  108. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  109. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  110. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  111. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  112. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  113. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  114. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  115. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  116. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  117. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  118. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  119. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  120. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  121. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  122. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  123. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  124. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  125. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  126. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  127. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  128. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  129. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  130. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  131. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  132. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  133. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  134. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  135. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  136. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  137. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  138. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  139. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  140. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  141. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  142. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  143. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  144. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  145. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  146. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  147. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  148. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  149. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  150. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  151. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  152. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  153. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  154. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  155. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  156. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  157. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  158. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  159. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  160. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  161. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  162. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  163. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  164. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  165. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  166. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  167. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  168. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  169. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  170. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  171. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  172. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  173. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  174. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  175. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  176. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  177. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  178. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  179. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  180. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  181. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  182. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  183. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  184. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  185. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  186. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  187. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  188. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  189. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  190. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  191. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  192. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  193. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  194. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  195. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  196. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  197. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  198. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  199. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  200. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  201. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  202. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  203. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  204. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  205. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  206. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  207. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  208. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  209. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  210. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  211. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  212. package/scripts/budget-audit.mjs +0 -135
  213. package/scripts/release.mjs +0 -446
  214. package/scripts/release_helper.mjs +0 -309
  215. package/scripts/run-perf-gate.mjs +0 -40
  216. package/scripts/run-tests.mjs +0 -375
@@ -1,375 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Suite gate for issue #241: run every test file through `node --test` and fail
4
- * the run when a detached state-coordinator leaked past the test process.
5
- *
6
- * Why this exists: every coordinator integration test creates a throwaway board
7
- * root under the OS temp dir and spawns a detached coordinator. A coordinator
8
- * that fails to step down stays alive forever and pins its root, so leaks
9
- * accumulate on a developer box instead of showing up as a test failure.
10
- *
11
- * Design (spec §3.3):
12
- * - Snapshot the matching coordinators BEFORE the run. Pre-existing ones are
13
- * warn-only: they are not this run's fault and are never killed.
14
- * - After the run, poll up to GRACE_MS for every coordinator above the snapshot
15
- * to be gone (the 1 s self-exit window plus load slack).
16
- * - Survivors: print pid + root, SIGTERM, wait 2 s, SIGKILL leftovers, exit 1.
17
- * - A test run killed by a signal (`exitCode === null`) is never a success.
18
- * - Enumeration unavailable: warn and skip the residue check (fail-open). A
19
- * missing `pwsh`/`powershell`/`ps` must not turn into a red suite.
20
- *
21
- * Selection is deliberately narrow: the command line must contain THIS
22
- * checkout's absolute `runner/state-coordinator.mjs` and its root argument must
23
- * resolve under `os.tmpdir()`. Another checkout's test coordinator never
24
- * matches (its script path differs), and a default production board
25
- * (`~/.pi/agent/agent-board`) never matches either. Accepted limit (spec §7): a
26
- * board whose `AGENT_BOARD_ROOT` points under `os.tmpdir()` and whose runner is
27
- * this checkout is indistinguishable from a test coordinator, so the gate would
28
- * kill that live board.
29
- *
30
- * Known limit (final-review m7): POSIX selection is token-based and `ps` output
31
- * is unquoted, so a checkout path containing spaces splits and its coordinators
32
- * are not selected (the gate silently no-ops instead of failing). win32 CIM
33
- * output quotes paths with spaces, so the tokenizer handles it there.
34
- *
35
- * This module must stay import-side-effect free: `test/run-tests-wrapper.test.mjs`
36
- * imports the pure functions and `runSuite`, and a spurious `main()` at import
37
- * time would recursively start the whole suite. Only the entry-point guard at
38
- * the bottom of this file may call `main()`.
39
- */
40
- import { execFileSync, spawn } from "node:child_process";
41
- import { readdirSync } from "node:fs";
42
- import { tmpdir } from "node:os";
43
- import { join, posix, resolve } from "node:path";
44
- import { fileURLToPath, pathToFileURL } from "node:url";
45
-
46
- /** The only coordinator script this checkout's suite spawns. */
47
- const COORDINATOR_RELATIVE_SCRIPT = "runner/state-coordinator.mjs";
48
- const DEFAULT_GRACE_MS = 10_000;
49
- const DEFAULT_POLL_MS = 100;
50
- /** SIGTERM → SIGKILL pause. On win32 SIGTERM is TerminateProcess; POSIX needs the grace. */
51
- const SIGKILL_DELAY_MS = 2_000;
52
- const SIGTERM = "SIGTERM";
53
- const SIGKILL = "SIGKILL";
54
-
55
- /**
56
- * Normalize a path for comparison without depending on the host platform:
57
- * forward slashes, dot segments removed, no trailing separator (a `tmpRoot` like
58
- * `C:\Temp\` or `/tmp/` must select exactly like its un-suffixed form), and (on
59
- * win32) case-folded. Filesystem roots keep their separator (`/` stays `/`,
60
- * `C:/` stays `C:/`): stripping it would turn a drive root into the
61
- * drive-relative `C:`. The platform is explicit so the unit tests can exercise
62
- * win32 samples on POSIX CI.
63
- * @param {string} value
64
- * @param {string} platform
65
- * @returns {string}
66
- */
67
- function canonicalPath(value, platform) {
68
- const text = String(value ?? "").replace(/\\/g, "/");
69
- if (!text) return "";
70
- let normalized = posix.normalize(text);
71
- if (normalized.length > 1 && normalized.endsWith("/")) {
72
- const trimmed = normalized.replace(/\/+$/, "");
73
- normalized = trimmed.endsWith(":") ? `${trimmed}/` : trimmed;
74
- }
75
- return platform === "win32" ? normalized.toLowerCase() : normalized;
76
- }
77
-
78
- /**
79
- * True when `candidate` is `directory` itself or one of its descendants, using
80
- * the already-canonical spellings. The trailing-slash check keeps a sibling
81
- * such as `/tmpfoo` from matching `/tmp`; a filesystem root already ends in the
82
- * separator, so `${directory}/` would become `//` and match nothing there.
83
- * @param {string} candidate
84
- * @param {string} directory
85
- * @returns {boolean}
86
- */
87
- function isUnderDirectory(candidate, directory) {
88
- if (!candidate || !directory) return false;
89
- if (candidate === directory) return true;
90
- return candidate.startsWith(directory.endsWith("/") ? directory : `${directory}/`);
91
- }
92
-
93
- /**
94
- * Split a process command line into argv-shaped tokens, honoring the double
95
- * quotes CIM emits around paths with spaces. Node's own argv never needs
96
- * backslash-escaped quotes inside a path, so this covers the coordinator shape.
97
- * @param {string} commandLine
98
- * @returns {string[]}
99
- */
100
- function tokenizeCommandLine(commandLine) {
101
- const tokens = [];
102
- for (const match of commandLine.matchAll(/"([^"]*)"|(\S+)/g)) tokens.push(match[1] ?? match[2]);
103
- return tokens;
104
- }
105
-
106
- /**
107
- * Parse the platform's process-enumeration output. Never throws: unparsable
108
- * output and unparsable lines are skipped.
109
- * @param {string} raw
110
- * @param {string} platform
111
- * @returns {Array<{pid: number, commandLine: string}>}
112
- */
113
- export function parseProcessList(raw, platform) {
114
- const text = String(raw ?? "").trim();
115
- if (!text) return [];
116
- if (platform === "win32") {
117
- let data;
118
- try {
119
- data = JSON.parse(text);
120
- } catch {
121
- return [];
122
- }
123
- // ConvertTo-Json emits a single object when exactly one process matches.
124
- const rows = Array.isArray(data) ? data : [data];
125
- return rows
126
- .map((row) => ({ pid: Number(row?.ProcessId ?? 0), commandLine: String(row?.CommandLine ?? "") }))
127
- .filter((row) => Number.isFinite(row.pid) && row.pid > 0 && row.commandLine);
128
- }
129
- const out = [];
130
- for (const line of text.split(/\r?\n/)) {
131
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
132
- if (match) out.push({ pid: Number(match[1]), commandLine: match[2] });
133
- }
134
- return out;
135
- }
136
-
137
- /**
138
- * Keep only coordinators this checkout's suite could have spawned: the command
139
- * line runs `<repoRoot>/runner/state-coordinator.mjs` and the token after that
140
- * script (the root argument, quotes stripped by the tokenizer) resolves under
141
- * `tmpRoot`. Production roots and other checkouts never match.
142
- * @param {{
143
- * processes?: Array<{pid: number, commandLine: string}>,
144
- * repoRoot: string,
145
- * tmpRoot: string,
146
- * platform: string,
147
- * }} args
148
- * @returns {Array<{pid: number, root: string}>} deterministic order by pid
149
- */
150
- export function selectOrphanCoordinators({ processes, repoRoot, tmpRoot, platform }) {
151
- const scriptPath = canonicalPath(`${repoRoot}/${COORDINATOR_RELATIVE_SCRIPT}`, platform);
152
- const tmpPath = canonicalPath(tmpRoot, platform);
153
- if (!scriptPath || !tmpPath) return [];
154
- const out = [];
155
- for (const proc of processes ?? []) {
156
- const pid = Number(proc?.pid ?? 0);
157
- if (!Number.isFinite(pid) || pid <= 0) continue;
158
- const tokens = tokenizeCommandLine(String(proc?.commandLine ?? ""));
159
- const scriptIndex = tokens.findIndex((token) => canonicalPath(token, platform) === scriptPath);
160
- if (scriptIndex < 0) continue;
161
- const root = tokens[scriptIndex + 1];
162
- if (!root) continue;
163
- if (!isUnderDirectory(canonicalPath(root, platform), tmpPath)) continue;
164
- out.push({ pid, root });
165
- }
166
- return out.sort((left, right) => left.pid - right.pid);
167
- }
168
-
169
- /**
170
- * @param {unknown} error
171
- * @returns {string}
172
- */
173
- function describeError(error) {
174
- return error instanceof Error ? error.message : String(error);
175
- }
176
-
177
- /**
178
- * Orchestrate one suite run and its residue check. Every side effect is
179
- * injected so the unit tests drive every branch without spawning or killing
180
- * anything; the real shell lives in `main()`.
181
- * @param {{
182
- * spawnTest: () => Promise<{exitCode: number|null, signal?: string|null}>,
183
- * listProcesses: () => Array<{pid: number, commandLine: string}>|Promise<Array<{pid: number, commandLine: string}>>,
184
- * kill: (pid: number, signal: string) => void,
185
- * sleep: (ms: number) => Promise<void>,
186
- * log: (line: string) => void,
187
- * repoRoot: string,
188
- * tmpRoot: string,
189
- * platform: string,
190
- * graceMs?: number,
191
- * pollMs?: number,
192
- * now?: () => number,
193
- * }} deps
194
- * @returns {Promise<{exitCode: number, survivors: number[]}>}
195
- */
196
- export async function runSuite(deps) {
197
- const {
198
- spawnTest,
199
- listProcesses,
200
- kill,
201
- sleep,
202
- log,
203
- repoRoot,
204
- tmpRoot,
205
- platform,
206
- graceMs = DEFAULT_GRACE_MS,
207
- pollMs = DEFAULT_POLL_MS,
208
- now = Date.now,
209
- } = deps;
210
- const select = (processes) => selectOrphanCoordinators({ processes, repoRoot, tmpRoot, platform });
211
-
212
- // Snapshot first: pre-existing coordinators predate this run, so they are
213
- // warned about but never killed and never fail the suite.
214
- /** @type {Set<number>} */
215
- const preexisting = new Set();
216
- let canCheckResidue = true;
217
- try {
218
- for (const proc of select(await listProcesses())) preexisting.add(proc.pid);
219
- } catch (error) {
220
- canCheckResidue = false;
221
- log(`run-tests: WARNING: cannot enumerate node processes (${describeError(error)}); skipping the coordinator residue check`);
222
- }
223
- if (preexisting.size > 0) {
224
- log(`run-tests: WARNING: ${preexisting.size} pre-existing state-coordinator process(es) before the run; not killed, not a failure`);
225
- }
226
-
227
- const run = await spawnTest();
228
-
229
- // Poll for post-run residue. The last successful enumeration is kept when a
230
- // later poll fails: an already observed survivor still fails the run, while
231
- // never having enumerated at all stays fail-open.
232
- /** @type {Array<{pid: number, root: string}>} */
233
- let survivors = [];
234
- let residueChecked = false;
235
- if (canCheckResidue) {
236
- const deadline = now() + graceMs;
237
- for (;;) {
238
- let current;
239
- try {
240
- current = select(await listProcesses());
241
- residueChecked = true;
242
- } catch (error) {
243
- canCheckResidue = false;
244
- log(`run-tests: WARNING: cannot enumerate node processes (${describeError(error)}); skipping the coordinator residue check`);
245
- break;
246
- }
247
- survivors = current.filter((proc) => !preexisting.has(proc.pid));
248
- if (survivors.length === 0) break;
249
- if (now() >= deadline) break;
250
- await sleep(pollMs);
251
- }
252
- }
253
-
254
- /** @type {number[]} */
255
- const survivorPids = [];
256
- if (survivors.length > 0) {
257
- for (const proc of survivors) log(`run-tests: leaked state-coordinator pid=${proc.pid} root=${proc.root}`);
258
- for (const proc of survivors) kill(proc.pid, SIGTERM);
259
- await sleep(SIGKILL_DELAY_MS);
260
- let leftovers = survivors;
261
- if (canCheckResidue) {
262
- try {
263
- leftovers = select(await listProcesses()).filter((proc) => !preexisting.has(proc.pid));
264
- } catch (error) {
265
- log(`run-tests: WARNING: cannot enumerate node processes after SIGTERM (${describeError(error)}); sending SIGKILL to the known survivors`);
266
- }
267
- }
268
- for (const proc of leftovers) {
269
- log(`run-tests: survivor pid=${proc.pid} ignored SIGTERM; sending SIGKILL`);
270
- kill(proc.pid, SIGKILL);
271
- }
272
- survivorPids.push(...survivors.map((proc) => proc.pid));
273
- } else if (residueChecked) {
274
- log("run-tests: no leaked coordinators");
275
- }
276
-
277
- let exitCode;
278
- if (run.exitCode === null) {
279
- log(`run-tests: the test run ended by signal ${run.signal ?? "unknown"}; treating it as a failure`);
280
- exitCode = 1;
281
- } else {
282
- exitCode = run.exitCode;
283
- }
284
- if (survivors.length > 0) exitCode = 1;
285
- return { exitCode, survivors: survivorPids };
286
- }
287
-
288
- /**
289
- * Enumerate node processes with their command lines. Throws when the platform's
290
- * enumeration tool is unavailable; `runSuite` turns that into a warning and
291
- * skips the residue check (fail-open).
292
- * @returns {Array<{pid: number, commandLine: string}>}
293
- */
294
- function listNodeProcesses() {
295
- if (process.platform === "win32") {
296
- // Forced UTF-8: ConvertTo-Json otherwise emits code-page text that mangles
297
- // non-ASCII root paths, and CIM may quote paths containing spaces.
298
- const prefix = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; $OutputEncoding=[Text.Encoding]::UTF8;";
299
- const query = "Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress";
300
- const args = ["-NoProfile", "-NonInteractive", "-Command", `${prefix} ${query}`];
301
- for (const shell of ["pwsh", "powershell"]) {
302
- try {
303
- return parseProcessList(execFileSync(shell, args, { encoding: "utf8" }), process.platform);
304
- } catch (error) {
305
- // Only a missing shell falls through to the next candidate; an
306
- // enumeration failure is not masked by retrying another binary.
307
- if (error?.code !== "ENOENT") throw error;
308
- }
309
- }
310
- throw new Error("neither pwsh nor powershell is available");
311
- }
312
- // -ww matters: without unlimited width, ps truncates the command line before
313
- // the root argument and a leaked coordinator silently escapes selection.
314
- return parseProcessList(execFileSync("ps", ["-e", "-ww", "-o", "pid=,args="], { encoding: "utf8" }), process.platform);
315
- }
316
-
317
- /**
318
- * Run the suite files through `node --test` with inherited stdio.
319
- * @param {string} repoRoot
320
- * @param {string[]} files repoRoot-relative test paths
321
- * @returns {Promise<{exitCode: number|null, signal: string|null}>}
322
- */
323
- function spawnSuite(repoRoot, files) {
324
- return new Promise((settle) => {
325
- let settled = false;
326
- const finish = (value) => {
327
- if (settled) return;
328
- settled = true;
329
- settle(value);
330
- };
331
- const child = spawn(process.execPath, ["--test", ...files], { cwd: repoRoot, stdio: "inherit", windowsHide: true });
332
- child.on("error", (error) => {
333
- console.error(`run-tests: failed to spawn the test runner: ${describeError(error)}`);
334
- finish({ exitCode: 1, signal: null });
335
- });
336
- child.on("exit", (code, signal) => finish({ exitCode: code, signal: signal ?? null }));
337
- });
338
- }
339
-
340
- /**
341
- * Real shell: derive this checkout from the module location, enumerate the test
342
- * files, run the suite, grade the residue, and set `process.exitCode`.
343
- * @returns {Promise<{exitCode: number, survivors: number[]}>}
344
- */
345
- export async function main() {
346
- const repoRoot = fileURLToPath(new URL("..", import.meta.url));
347
- // Sorted so a failing run's file ordering is stable across platforms.
348
- const files = readdirSync(join(repoRoot, "test"))
349
- .filter((name) => name.endsWith(".test.mjs"))
350
- .sort()
351
- .map((name) => join("test", name));
352
- const result = await runSuite({
353
- repoRoot,
354
- tmpRoot: tmpdir(),
355
- platform: process.platform,
356
- spawnTest: () => spawnSuite(repoRoot, files),
357
- listProcesses: () => listNodeProcesses(),
358
- kill: (pid, signal) => {
359
- try {
360
- process.kill(pid, signal);
361
- } catch { /* already gone */ }
362
- },
363
- sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
364
- log: (line) => console.log(line),
365
- });
366
- process.exitCode = result.exitCode;
367
- return result;
368
- }
369
-
370
- // Import safety: see the module JSDoc. `main()` must never run while a test
371
- // imports the pure functions, or `node --test` would recurse into itself.
372
- const invokedPath = process.argv[1];
373
- if (invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href) {
374
- await main();
375
- }