@huseynsnmz/pi-board 0.15.3 → 0.16.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 (211) hide show
  1. package/CHANGELOG.md +13 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +53 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +276 -0
  7. package/package.json +5 -11
  8. package/src/index.ts +1 -1
  9. package/src/runtime/service.mjs +12 -0
  10. package/src/ui/dashboard.ts +14 -8
  11. package/IMPLEMENTATION_PLAN.md +0 -920
  12. package/PRD.md +0 -484
  13. package/PROGRESS.md +0 -142
  14. package/VERIFY.md +0 -115
  15. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  16. package/docs/EXPLORATION.md +0 -187
  17. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  18. package/docs/RELEASE.md +0 -148
  19. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  20. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  21. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  22. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  23. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  24. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  25. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  26. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  27. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  28. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  29. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  30. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  31. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  32. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  33. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  34. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  35. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  36. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  37. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  38. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  39. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  40. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  41. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  42. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  43. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  44. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  45. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  46. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  47. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  48. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  49. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  50. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  51. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  52. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  53. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  54. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  55. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  56. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  57. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  58. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  59. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  60. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  61. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  62. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  63. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  64. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  65. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  66. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  67. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  68. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  69. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  70. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  71. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  72. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  73. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  74. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  75. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  76. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  77. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  78. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  79. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  80. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  81. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  82. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  83. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  84. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  85. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  86. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  87. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  88. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  89. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  90. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  91. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  92. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  93. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  94. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  95. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  96. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  97. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  98. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  99. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  100. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  101. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  102. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  103. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  104. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  105. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  106. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  107. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  108. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  109. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  110. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  111. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  112. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  113. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  114. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  115. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  116. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  117. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  118. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  119. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  120. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  121. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  122. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  123. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  124. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  125. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  126. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  127. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  128. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  129. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  130. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  131. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  132. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  133. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  134. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  135. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  136. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  137. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  138. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  139. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  140. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  141. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  142. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  143. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  144. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  145. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  146. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  147. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  148. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  149. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  150. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  151. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  152. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  153. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  154. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  155. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  156. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  157. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  158. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  159. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  160. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  161. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  162. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  163. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  164. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  165. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  166. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  167. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  168. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  169. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  170. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  171. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  172. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  173. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  174. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  175. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  176. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  177. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  178. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  179. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  180. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  181. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  182. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  183. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  184. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  185. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  186. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  187. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  188. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  189. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  190. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  191. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  192. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  193. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  194. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  195. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  196. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  197. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  198. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  199. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  200. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  201. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  202. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  203. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  204. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  205. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  206. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  207. package/scripts/budget-audit.mjs +0 -135
  208. package/scripts/release.mjs +0 -446
  209. package/scripts/release_helper.mjs +0 -309
  210. package/scripts/run-perf-gate.mjs +0 -40
  211. 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
- }