@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,504 +0,0 @@
1
- # Coordinator Replacement Identity Verification — Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use subagent-driven-development (recommended) or executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Make `replaceStaleCoordinator` signal a lease-owner pid only after proving, from fresh evidence, that the lease record belongs to the coordinator actually serving the endpoint — and never let a diagnostic or signal failure break `sendStateCommand`.
6
-
7
- **Architecture:** The coordinator records its in-process `instanceId` inside the already-passthrough lease `identity` object. The client reads the lease, judges same-ness with a pure function (instanceId equality, else a live `/proc` startToken compare for Linux legacy records), re-confirms with a second endpoint probe plus a lease re-read (TOCTOU), then sends SIGTERM and waits for process exit. The residual lease is left to the existing #224 reclaim — no client-side lease deletion. Skipped signals write one deduped diagnostic.
8
-
9
- **Tech Stack:** Node ≥20 ESM (`.mjs`), `node:test` + `assert/strict`, no new dependencies.
10
-
11
- **Spec:** `docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md` (issue #239, sha256 `a6e578bf…`; worktree is based on `main` @ `60083f6` — the two upstream commits since the spec baseline touch only UI files).
12
-
13
- ## Global Constraints
14
-
15
- - Tabs for indentation in `.mjs`/`.ts` sources; English comments and commit messages; conventional commit subjects with the PR number only after the PR exists (`(#N)` appended later if needed).
16
- - `coordinator-client.mjs` module contract: **never throws** for `ensureCoordinator`/`sendStateCommand` (existing doc at `src/core/coordinator-client.mjs:11-24`). Every new path must preserve it.
17
- - Do not change `src/core/locks.mjs`, do not bump `COORDINATOR_PROTOCOL_VERSION`, do not add client-side lease deletion (spec non-goals N1/N2/N3/N5).
18
- - `EnsureResult` shape stays `{ ok, instanceId?, pid?, error? }`; `coordinator_stale_protocol` stays the failure code (`test/coordinator-client.test.mjs:409` deepEqual must keep passing).
19
- - Lease `identity` stays a passthrough object; `classifyLeaseOwner` behavior must not change.
20
- - Test commands: `node --test test/coordinator-client.test.mjs`; full local suite `npm test`; CI gate is `npm run verify`.
21
- - Local Windows baseline (do not regress): 1375 tests / 62 fail / 5 skip; `coordinator-client` and `locks` files are green. Failure-name list: `~/.github-issue-driven/zhuxixi/pi-agent-board/issue-239/research/baseline-failures.txt` (strip trailing ` (1234.5ms)` before set comparison).
22
-
23
- ## Review Focus
24
-
25
- - **Signal must never reach an unrelated pid**: a lease whose `pid` belongs to a live non-coordinator process must not be SIGTERMed under any branch (J2/J4/J6/J7, and the re-probe paths).
26
- - **Fresh evidence only**: the judge input `probedInstanceId` must be the pong observed in *this* call, never a cached value; the re-probe must catch "old instance already gone" and "another old instance took over".
27
- - **never-throws**: diagnostic writes (`appendDiagnosticOnce`), `kill`, `isAlive`, and `readProcessStartToken` are all fallible; each failure has a defined non-throwing outcome (spec D4/D5).
28
- - **No root resurrection**: a deleted board root must not be recreated by a diagnostic write (spec A3k, #241 class).
29
- - **Diagnostic flood**: repeated `ensureCoordinator` calls (run_progress beats ~4/s) must not grow `_coordinator/diagnostics.jsonl`.
30
- - **Windows SIGTERM is TerminateProcess**: the wait-for-exit is the only thing that stops the next spawn from racing a still-exiting process; `existsSync(socketPath)` is meaningless on win32 (named pipe).
31
-
32
- ## File Structure
33
-
34
- | File | Responsibility |
35
- |---|---|
36
- | `src/core/coordinator-client.mjs` | lease record reader, pure judge, replacement orchestration, deduped skip diagnostic, `ensureCoordinator` wiring |
37
- | `runner/state-coordinator.mjs` | write `instanceId` into the lease `identity` (one-line change at the existing `acquireOwnedViewLock` call) |
38
- | `test/coordinator-client.test.mjs` | unit tests (T1/T2/T3) + integration fixtures (T4/T5/T6) |
39
-
40
- No new source files. The U1 verification script lives outside the repo in the issue research directory (not committed).
41
-
42
- ---
43
-
44
- ### Task 1: Coordinator records `identity.instanceId`
45
-
46
- **Files:**
47
- - Modify: `runner/state-coordinator.mjs:227-231` (the `acquireOwnedViewLock` call in `main()`)
48
- - Test: `test/coordinator-client.test.mjs` (extend the existing real-spawn happy-path test near `:164-186`)
49
-
50
- **Interfaces:**
51
- - Consumes: `instanceId` (already generated at `runner/state-coordinator.mjs:198`)
52
- - Produces: lease `owner.json` field `identity.instanceId` — the value is identical to the pong's `instanceId` (`runner/state-coordinator.mjs:431`)
53
-
54
- - [ ] **Step 1: Write the failing test** — in `test/coordinator-client.test.mjs`, extend the `ensureCoordinator spawns the real coordinator…` test (or add a focused test after it):
55
-
56
- ```js
57
- test("coordinator records its instanceId in the lease identity (issue #239 A4)", async (t) => {
58
- const root = freshRoot();
59
- t.after(async () => { await cleanupRoot(root); });
60
- createView(root, { id: "v1", name: "a4", cwd: root });
61
-
62
- const ensured = await ensureCoordinator(root, { runnerScript: COORDINATOR_SCRIPT });
63
- assert.equal(ensured.ok, true);
64
- if (ensured.pid) track(ensured.pid);
65
-
66
- const owner = JSON.parse(readFileSync(
67
- join(P.viewLockPath(root, "_coordinator", "state-coordinator"), "owner.json"), "utf8"));
68
- assert.equal(typeof owner.identity?.instanceId, "string");
69
- assert.notEqual(owner.identity.instanceId.length, 0);
70
- assert.equal(owner.identity.instanceId, ensured.instanceId);
71
- });
72
- ```
73
-
74
- - [ ] **Step 2: Run it and see it fail**
75
-
76
- Run: `node --test test/coordinator-client.test.mjs`
77
- Expected: FAIL — `owner.identity.instanceId` is `undefined`.
78
-
79
- - [ ] **Step 3: Implement** — in `runner/state-coordinator.mjs`, add `instanceId` to the identity object:
80
-
81
- ```js
82
- identity: { pid: process.pid, startToken: captureStartToken(process.pid), instanceId },
83
- ```
84
-
85
- - [ ] **Step 4: Run the test file**
86
-
87
- Run: `node --test test/coordinator-client.test.mjs`
88
- Expected: PASS (the new test and all existing tests).
89
-
90
- - [ ] **Step 5: Commit**
91
-
92
- ```bash
93
- git add runner/state-coordinator.mjs test/coordinator-client.test.mjs
94
- git commit -m "feat(coordinator): record instanceId in the lease identity (issue #239)"
95
- ```
96
-
97
- ---
98
-
99
- ### Task 2: Lease record reader + pure judge
100
-
101
- **Files:**
102
- - Modify: `src/core/coordinator-client.mjs` (replace `readCoordinatorLeasePid` at `:119-135`; add `judgeReplacementTarget`)
103
- - Test: `test/coordinator-client.test.mjs` (new unit tests; import the two functions)
104
-
105
- **Interfaces:**
106
- - Produces:
107
- - `readCoordinatorLeaseRecord(root): { token: string|null, pid: number|null, instanceId: string|null, startToken: string|null } | null`
108
- - `pid` = `Number(owner.identity.pid ?? owner.pid)`, `null` when not a positive finite number
109
- - `instanceId` = first non-empty string of `owner.identity.instanceId`, `owner.instanceId`
110
- - `startToken` = `owner.identity.startToken` when it is a non-empty string, else `null`
111
- - returns `null` on missing file / bad JSON / non-object
112
- - `judgeReplacementTarget({ recorded, probedInstanceId, currentStartToken })` → `{ action: "signal" } | { action: "skip", reason }`
113
- - order: `recorded`/`pid` invalid → `skip("lease_unreadable")`; `recorded.instanceId !== null` → equal = `signal`, unequal = `skip("lease_owner_mismatch")`; else (legacy) `recorded.startToken === null` → `skip("identity_unverified")`; `currentStartToken` not a string (null/undefined/Promise) → `skip("identity_unverified")`; unequal → `skip("pid_reused")`; equal → `signal`
114
-
115
- - [ ] **Step 1: Write the failing tests** — append to `test/coordinator-client.test.mjs` (update the import line to include both functions):
116
-
117
- ```js
118
- // --- issue #239: lease record + pure judge (T1/T2) ---
119
-
120
- function mkRecord(overrides = {}) {
121
- return { token: "t1", pid: 4242, instanceId: "abc", startToken: null, ...overrides };
122
- }
123
-
124
- test("judgeReplacementTarget: instanceId equality decides (J1/J2)", () => {
125
- assert.deepEqual(judgeReplacementTarget({ recorded: mkRecord(), probedInstanceId: "abc" }), { action: "signal" });
126
- assert.deepEqual(judgeReplacementTarget({ recorded: mkRecord(), probedInstanceId: "other" }),
127
- { action: "skip", reason: "lease_owner_mismatch" });
128
- });
129
-
130
- test("judgeReplacementTarget: legacy startToken fallback (J3/J4/J5/J6)", () => {
131
- const legacy = mkRecord({ instanceId: null, startToken: "777" });
132
- assert.deepEqual(judgeReplacementTarget({ recorded: legacy, probedInstanceId: "x", currentStartToken: "777" }), { action: "signal" });
133
- assert.deepEqual(judgeReplacementTarget({ recorded: legacy, probedInstanceId: "x", currentStartToken: "888" }),
134
- { action: "skip", reason: "pid_reused" });
135
- assert.deepEqual(judgeReplacementTarget({ recorded: legacy, probedInstanceId: "x", currentStartToken: null }),
136
- { action: "skip", reason: "identity_unverified" });
137
- assert.deepEqual(judgeReplacementTarget({ recorded: legacy, probedInstanceId: "x", currentStartToken: Promise.resolve("777") }),
138
- { action: "skip", reason: "identity_unverified" }, "a missed await must not read as pid_reused");
139
- const noToken = mkRecord({ instanceId: null, startToken: null });
140
- assert.deepEqual(judgeReplacementTarget({ recorded: noToken, probedInstanceId: "x" }),
141
- { action: "skip", reason: "identity_unverified" });
142
- });
143
-
144
- test("judgeReplacementTarget: malformed records are never signalled (J7)", () => {
145
- assert.deepEqual(judgeReplacementTarget({ recorded: null, probedInstanceId: "x" }),
146
- { action: "skip", reason: "lease_unreadable" });
147
- assert.deepEqual(judgeReplacementTarget({ recorded: mkRecord({ pid: 0 }), probedInstanceId: "abc" }),
148
- { action: "skip", reason: "lease_unreadable" });
149
- });
150
-
151
- test("judgeReplacementTarget: empty-string fields count as missing", () => {
152
- assert.deepEqual(judgeReplacementTarget({ recorded: mkRecord({ instanceId: "" }), probedInstanceId: "abc" }),
153
- { action: "skip", reason: "identity_unverified" });
154
- });
155
-
156
- test("readCoordinatorLeaseRecord parses identity / legacy / top-level instanceId (A2)", () => {
157
- const root = freshRoot();
158
- const lockPath = P.viewLockPath(root, "_coordinator", "state-coordinator");
159
- mkdirSync(lockPath, { recursive: true });
160
- const ownerPath = join(lockPath, "owner.json");
161
-
162
- assert.equal(readCoordinatorLeaseRecord(root), null, "missing owner.json");
163
- writeFileSync(ownerPath, "{not json", "utf8");
164
- assert.equal(readCoordinatorLeaseRecord(root), null, "corrupt json");
165
-
166
- writeFileSync(ownerPath, JSON.stringify({ token: "a", pid: 7, identity: { pid: 7, startToken: "9", instanceId: "i" } }), "utf8");
167
- assert.deepEqual(readCoordinatorLeaseRecord(root), { token: "a", pid: 7, instanceId: "i", startToken: "9" });
168
-
169
- writeFileSync(ownerPath, JSON.stringify({ token: "b", pid: 8, identity: { pid: 8, startToken: null } }), "utf8");
170
- assert.deepEqual(readCoordinatorLeaseRecord(root), { token: "b", pid: 8, instanceId: null, startToken: null });
171
-
172
- writeFileSync(ownerPath, JSON.stringify({ token: "c", pid: 9, instanceId: "top" }), "utf8");
173
- assert.equal(readCoordinatorLeaseRecord(root).instanceId, "top", "top-level compatibility");
174
-
175
- writeFileSync(ownerPath, JSON.stringify({ token: "d", identity: null }), "utf8");
176
- assert.equal(readCoordinatorLeaseRecord(root).pid, null);
177
- rmSync(root, { recursive: true, force: true });
178
- });
179
- ```
180
-
181
- - [ ] **Step 2: Run and see them fail**
182
-
183
- Run: `node --test test/coordinator-client.test.mjs`
184
- Expected: FAIL — `judgeReplacementTarget`/`readCoordinatorLeaseRecord` are not exported.
185
-
186
- - [ ] **Step 3: Implement** — in `src/core/coordinator-client.mjs`, replace `readCoordinatorLeasePid` with the exported `readCoordinatorLeaseRecord` (same read, richer shape) and add the exported pure `judgeReplacementTarget` exactly per the Interfaces block. Delete the now-unused `readCoordinatorLeasePid` and update `replaceStaleCoordinator`'s call site to the new reader (temporary: it may still use `.pid` for now; Task 4 rewrites the function).
187
-
188
- - [ ] **Step 4: Run the test file**
189
-
190
- Run: `node --test test/coordinator-client.test.mjs`
191
- Expected: PASS. `npm run typecheck` also passes (JSDoc types are checked).
192
-
193
- - [ ] **Step 5: Commit**
194
-
195
- ```bash
196
- git add src/core/coordinator-client.mjs test/coordinator-client.test.mjs
197
- git commit -m "feat(coordinator): lease record reader and pure replacement judge (issue #239)"
198
- ```
199
-
200
- ---
201
-
202
- ### Task 3: Deduped skip diagnostic
203
-
204
- **Files:**
205
- - Modify: `src/core/coordinator-client.mjs` (module-level Set + `reportReplacementSkip`)
206
- - Test: `test/coordinator-client.test.mjs` (unit test the diagnostic writer through the exported replacement path — see Task 4; here test the dedup contract by calling the exported helper if it is exported, otherwise defer the assertion to Task 4's A8 test)
207
-
208
- **Interfaces:**
209
- - Produces: internal `reportReplacementSkip(root, { reason, recordedInstanceId, probedInstanceId })`
210
- - module-level `Set` key `${root}|${reason}|${recordedInstanceId ?? "null"}|${probedInstanceId}`; a hit returns immediately without touching disk
211
- - on miss: `if (!existsSync(root)) return;` then `appendDiagnosticOnce(root, "_coordinator", patch, { match: ["reason", "recordedInstanceId", "probedInstanceId"] })`; everything in `try/catch`
212
- - patch: `{ source: "coordinator-client", level: "warn", code: "coordinator_replacement_skipped", message: \`coordinator replacement skipped: ${reason}\`, details: { reason, recordedInstanceId: recordedInstanceId ?? null, probedInstanceId } }`
213
-
214
- - [ ] **Step 1:** Implement `reportReplacementSkip` and call it from the replacement function's every skip return (wired fully in Task 4). No new test in this task — its observable contract (`≤1` event for repeated calls, no root resurrection) is asserted by A8/A3k in Task 4, because it is not exported (keeps the public surface small).
215
-
216
- - [ ] **Step 2: Commit**
217
-
218
- ```bash
219
- git add src/core/coordinator-client.mjs
220
- git commit -m "feat(coordinator): deduped replacement-skip diagnostic (issue #239)"
221
- ```
222
-
223
- ---
224
-
225
- ### Task 4: `replaceStaleCoordinator` orchestration
226
-
227
- **Files:**
228
- - Modify: `src/core/coordinator-client.mjs` (`replaceStaleCoordinator` body; export it)
229
- - Test: `test/coordinator-client.test.mjs` (T3 unit tests A3a-A3l + A8 dedup)
230
-
231
- **Interfaces:**
232
- - `replaceStaleCoordinator(root, socketPath, expectedInstanceId, opts = {}) → Promise<ReplaceOutcome>`
233
- - `ReplaceOutcome = { replaced: true } | { replaced: false, reason }`
234
- - `SkipReason = "lease_unreadable"|"lease_owner_mismatch"|"pid_reused"|"identity_unverified"|"lease_changed"|"instance_unresponsive"|"replacement_raced"|"replace_timeout"`
235
- - opts (all optional, defaults in spec §4): `probe` (default `probeOnce`, signature `(socketPath, connect, timeoutMs)`), `connect`, `probeTimeoutMs`, `readLease`, `kill` (default `process.kill`), `isAlive`, `readStartToken` (default `readProcessStartToken`, async), `clock`, `sleep`, `replaceTimeoutMs`, `reportSkip`
236
- - flow (spec D3/D4, exact order): read lease → legacy live-token read only when `instanceId` is null and `startToken` is a string → judge → skip or continue → **re-probe**: pong & same instanceId → continue; pong & different instanceId → `protocolVersion >= COORDINATOR_PROTOCOL_VERSION` ? `{replaced:true}` : `skip("replacement_raced")`; `null` → re-read lease (null/invalid → `skip("lease_unreadable")`; `isAlive(pid)` → `skip("instance_unresponsive")`; else `{replaced:true}`) → **re-read lease**: token/instanceId changed → `skip("lease_changed")` → `kill(pid, "SIGTERM")` in try/catch (`ESRCH` → `{replaced:true}`) → poll `isAlive` every 50ms until `replaceTimeoutMs` (`{replaced:true}` on exit, else `skip("replace_timeout")`)
237
- - every `skip` return calls `reportSkip` inside its own try/catch
238
-
239
- - [ ] **Step 1: Write the failing unit tests** — append to `test/coordinator-client.test.mjs`:
240
-
241
- ```js
242
- // --- issue #239: replacement orchestration (T3) ---
243
-
244
- /** Build injected dependencies for replaceStaleCoordinator; `calls` records kills. */
245
- function mkReplacementDeps(overrides = {}) {
246
- const calls = { kill: [], probe: 0, report: [] };
247
- const deps = {
248
- readLease: () => mkRecord(),
249
- probe: async () => { calls.probe += 1; return { instanceId: "abc", protocolVersion: COORDINATOR_PROTOCOL_VERSION }; },
250
- kill: (pid, sig) => { calls.kill.push([pid, sig]); },
251
- isAlive: () => false,
252
- readStartToken: async () => null,
253
- clock: () => 0,
254
- sleep: async () => {},
255
- replaceTimeoutMs: 3000,
256
- reportSkip: (details) => { calls.report.push(details); },
257
- ...overrides,
258
- };
259
- return { deps, calls };
260
- }
261
-
262
- test("A3a: matching instanceId signals once and reports replaced", async () => {
263
- const { deps, calls } = mkReplacementDeps();
264
- const out = await replaceStaleCoordinator("/root", "/sock", "abc", deps);
265
- assert.deepEqual(out, { replaced: true });
266
- assert.deepEqual(calls.kill, [[4242, "SIGTERM"]]);
267
- });
268
-
269
- test("A3b: mismatched instanceId never signals", async () => {
270
- const { deps, calls } = mkReplacementDeps({ readLease: () => mkRecord({ instanceId: "other" }) });
271
- const out = await replaceStaleCoordinator("/root", "/sock", "abc", deps);
272
- assert.deepEqual(out, { replaced: false, reason: "lease_owner_mismatch" });
273
- assert.deepEqual(calls.kill, []);
274
- assert.equal(calls.report.at(-1).reason, "lease_owner_mismatch");
275
- });
276
-
277
- test("A3c/A3d: re-probe null with dead pid replaces; with live pid skips", async () => {
278
- const dead = mkReplacementDeps({ probe: async () => null, isAlive: () => false });
279
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", dead.deps), { replaced: true });
280
- assert.deepEqual(dead.calls.kill, []);
281
-
282
- const live = mkReplacementDeps({ probe: async () => null, isAlive: () => true });
283
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", live.deps),
284
- { replaced: false, reason: "instance_unresponsive" });
285
- assert.deepEqual(live.calls.kill, []);
286
- });
287
-
288
- test("A3e: another old-protocol instance taking over skips; a healthy one replaces", async () => {
289
- const stale = mkReplacementDeps({ probe: async () => ({ instanceId: "newer-old", protocolVersion: 1 }) });
290
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", stale.deps),
291
- { replaced: false, reason: "replacement_raced" });
292
-
293
- const healthy = mkReplacementDeps({ probe: async () => ({ instanceId: "new", protocolVersion: COORDINATOR_PROTOCOL_VERSION }) });
294
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", healthy.deps), { replaced: true });
295
- assert.deepEqual(healthy.calls.kill, []);
296
- });
297
-
298
- test("A3f: lease changing between judge and signal skips", async () => {
299
- let reads = 0;
300
- const { deps, calls } = mkReplacementDeps({ readLease: () => (++reads === 1 ? mkRecord() : mkRecord({ token: "t2" })) });
301
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", deps),
302
- { replaced: false, reason: "lease_changed" });
303
- assert.deepEqual(calls.kill, []);
304
- });
305
-
306
- test("A3g: a process that never exits times out after the kill", async () => {
307
- let now = 0;
308
- const { deps, calls } = mkReplacementDeps({ isAlive: () => true, clock: () => now, sleep: async () => { now += 100; } });
309
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", deps),
310
- { replaced: false, reason: "replace_timeout" });
311
- assert.equal(calls.kill.length, 1);
312
- });
313
-
314
- test("A3h: a throwing reportSkip does not throw or change the outcome", async () => {
315
- const { deps } = mkReplacementDeps({
316
- readLease: () => mkRecord({ instanceId: "other" }),
317
- reportSkip: () => { throw new Error("disk full"); },
318
- });
319
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", deps),
320
- { replaced: false, reason: "lease_owner_mismatch" });
321
- });
322
-
323
- test("A3j: unreadable lease on the null-probe re-read skips", async () => {
324
- let reads = 0;
325
- const { deps, calls } = mkReplacementDeps({
326
- probe: async () => null,
327
- readLease: () => (++reads === 1 ? mkRecord() : null),
328
- });
329
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", deps),
330
- { replaced: false, reason: "lease_unreadable" });
331
- assert.deepEqual(calls.kill, []);
332
- });
333
-
334
- test("A3l: kill errors are contained (ESRCH replaces, EPERM times out)", async () => {
335
- const esrch = Object.assign(new Error("gone"), { code: "ESRCH" });
336
- const a = mkReplacementDeps({ kill: () => { throw esrch; } });
337
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", a.deps), { replaced: true });
338
-
339
- const eperm = Object.assign(new Error("denied"), { code: "EPERM" });
340
- const b = mkReplacementDeps({ kill: () => { throw eperm; }, isAlive: () => true, clock: () => 0, sleep: async () => {} });
341
- assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", b.deps),
342
- { replaced: false, reason: "replace_timeout" });
343
- });
344
-
345
- test("A3k: a removed root is not resurrected by the skip diagnostic", async () => {
346
- const root = join(freshRoot(), "removed-root"); // never created on disk
347
- const { deps } = mkReplacementDeps({
348
- readLease: () => mkRecord({ instanceId: "other" }),
349
- reportSkip: undefined, // default writer -> existsSync guard
350
- });
351
- // default reportSkip is not injectable here; instead assert the default writer's guard:
352
- const out = await replaceStaleCoordinator(root, "/sock", "abc", { ...deps, reportSkip: undefined });
353
- assert.deepEqual(out, { replaced: false, reason: "lease_owner_mismatch" });
354
- assert.equal(existsSync(root), false, "a deleted/missing root must not be recreated");
355
- });
356
- ```
357
-
358
- (Imports to extend: `readCoordinatorLeaseRecord`, `judgeReplacementTarget`, `replaceStaleCoordinator`; fs: `existsSync`.)
359
-
360
- - [ ] **Step 2: Run and see them fail**
361
-
362
- Run: `node --test test/coordinator-client.test.mjs`
363
- Expected: FAIL — `replaceStaleCoordinator` not exported / old body lacks the branches.
364
-
365
- - [ ] **Step 3: Implement** — rewrite `replaceStaleCoordinator` per the Interfaces flow. Keep `REPLACE_TIMEOUT_MS = 3_000` and add `REPLACE_POLL_MS = 50`. The old `existsSync(socketPath)` wait is deleted.
366
-
367
- - [ ] **Step 4: Run and confirm green**
368
-
369
- Run: `node --test test/coordinator-client.test.mjs` → PASS.
370
-
371
- - [ ] **Step 5: Commit**
372
-
373
- ```bash
374
- git add src/core/coordinator-client.mjs test/coordinator-client.test.mjs
375
- git commit -m "feat(coordinator): verify lease identity before signalling (issue #239)"
376
- ```
377
-
378
- ---
379
-
380
- ### Task 5: `ensureCoordinator` wiring + integration fixtures
381
-
382
- **Files:**
383
- - Modify: `src/core/coordinator-client.mjs` (`ensureCoordinator` replacement branch + opts pass-through)
384
- - Modify: `test/coordinator-client.test.mjs` (`STALE_COORDINATOR_CJS` fixture + new scenario fixtures/tests)
385
-
386
- **Interfaces:**
387
- - Consumes: Task 4 `replaceStaleCoordinator`, Task 3 `reportReplacementSkip`
388
- - Produces: `ensureCoordinator` accepts `probe`, `kill`, `isAlive`, `readStartToken`, `clock`, `sleep`, `replaceTimeoutMs`, `reportSkip` and forwards them plus `connect`/`probeTimeoutMs` into `replaceStaleCoordinator`; result shape unchanged.
389
- - Fixture contract: `spawnStaleV1Coordinator(root, ownerOverrides, pongInstanceId)` — the CJS writes `identity: { pid, startToken: "1", ...ownerOverrides.identity }`; when `ownerOverrides.instanceId` is set it is merged into `identity`; its pong instanceId is parameterized (default `"stale-v1"`).
390
-
391
- - [ ] **Step 1: Write the failing integration tests** — update the fixture and add:
392
-
393
- ```js
394
- test("A5: a matching-instanceId stale coordinator is replaced (end-to-end)", async (t) => {
395
- const root = freshRoot();
396
- t.after(async () => { await cleanupRoot(root); });
397
- const stale = await spawnStaleV1Coordinator(root); // fixture now records identity.instanceId = "stale-v1"
398
- t.after(() => stale.cleanup());
399
- createView(root, { id: "v1", name: "a5", cwd: root });
400
- assert.equal((await rawPing(stale.socketPath))?.instanceId, "stale-v1");
401
-
402
- const ensured = await ensureCoordinator(root, { runnerScript: COORDINATOR_SCRIPT });
403
- assert.equal(ensured.ok, true);
404
- assert.notEqual(ensured.instanceId, "stale-v1");
405
- if (ensured.pid) track(ensured.pid);
406
- assert.equal(stale.alive(), false);
407
- assert.equal((await sendStateCommand(root, markCompleted(), { timeoutMs: 5000 })).status, "applied");
408
- });
409
-
410
- test("A6: a mismatched lease identity never kills the unrelated pid (core)", async (t) => {
411
- const root = freshRoot();
412
- const unrelated = spawn(process.execPath, ["-e", "setInterval(()=>{},1000)"], { stdio: "ignore" });
413
- t.after(() => { try { unrelated.kill("SIGKILL"); } catch {} });
414
- const stale = await spawnStaleV1Coordinator(root, { instanceId: "not-the-pong-instance", pid: unrelated.pid });
415
- t.after(() => stale.cleanup());
416
- const diagnostics = join(P.diagnosticsPath(root, "_coordinator"));
417
-
418
- const ensured = await ensureCoordinator(root, { runnerScript: COORDINATOR_SCRIPT });
419
- assert.deepEqual(ensured, { ok: false, error: "coordinator_stale_protocol" });
420
- assert.equal(isAlive(unrelated.pid), true, "the unrelated process must survive");
421
- assert.equal(stale.alive(), true);
422
- const events = readJsonl(diagnostics).filter((e) => e.code === "coordinator_replacement_skipped");
423
- assert.equal(events.length, 1);
424
- assert.equal(events[0].details.reason, "lease_owner_mismatch");
425
- });
426
-
427
- test("A7: a legacy record without instanceId/token is not signalled", async (t) => {
428
- // same fixture with identity: { pid, startToken: null }, no instanceId; pong instanceId "stale-v1"
429
- // expect: stale alive, stale_protocol, reason identity_unverified
430
- });
431
-
432
- test("A8: repeated ensures keep exactly one skip diagnostic", async (t) => {
433
- // mismatched identity lease + 5 sequential ensureCoordinator calls -> 1 event total
434
- });
435
-
436
- test("A7b: Linux legacy live-token wiring kills the real lease owner", { skip: process.platform !== "linux" }, async (t) => {
437
- // fixture whose owner.json records the CHILD's real startToken (captureStartToken(child.pid)),
438
- // no instanceId; pong instanceId arbitrary -> J3 -> child killed, new coordinator takes over
439
- });
440
- ```
441
-
442
- (Exact bodies: follow the existing `spawnStaleV1Coordinator` helper shape at `test/coordinator-client.test.mjs:330-357`; for A6/A7/A8 spawn the fixture with `ownerOverrides` so `pid`/`identity`/`instanceId` are explicit. Import `isAlive`, `readJsonl`, `P.diagnosticsPath`, `captureStartToken` as needed.)
443
-
444
- - [ ] **Step 2: Run and see them fail**
445
-
446
- Run: `node --test test/coordinator-client.test.mjs`
447
- Expected: FAIL — the fixture does not parameterize identity yet; `ensureCoordinator` does not pass `first.instanceId`.
448
-
449
- - [ ] **Step 3: Implement**
450
- 1. Fixture: add arguments to `spawnStaleV1Coordinator(root, ownerOverrides = {}, pongInstanceId = "stale-v1")`; build the `owner.json` object in JS and embed it into `STALE_COORDINATOR_CJS` via `JSON.stringify` (replace the hardcoded write at `:307`), and parameterize the pong instanceId at `:317`.
451
- 2. `ensureCoordinator`: replace the `replaceStaleCoordinator(root, socketPath)` call with the outcome-based call forwarding `first.instanceId` and all injectable opts; `!outcome.replaced` → `{ ok: false, error: "coordinator_stale_protocol" }`.
452
-
453
- - [ ] **Step 4: Run the file, then the local suite**
454
-
455
- ```bash
456
- node --test test/coordinator-client.test.mjs
457
- npm test *> after-239.log
458
- # compare ✖ names (strip trailing " (123.4ms)") against baseline-failures.txt: after ⊆ baseline
459
- ```
460
-
461
- - [ ] **Step 5: Commit**
462
-
463
- ```bash
464
- git add src/core/coordinator-client.mjs test/coordinator-client.test.mjs
465
- git commit -m "fix(coordinator): only replace a coordinator whose lease identity matches (issue #239)"
466
- ```
467
-
468
- ---
469
-
470
- ### Task 6: U1 verification script + final gates
471
-
472
- **Files:**
473
- - Create (outside repo, issue research dir): `~/.github-issue-driven/zhuxixi/pi-agent-board/issue-239/research/verify-u1-legacy-no-kill.mjs`
474
- - Test: run it; no repo files change in this task (except possible goal-driven touch-ups from the gates)
475
-
476
- **Interfaces:**
477
- - CLI: `node verify-u1-legacy-no-kill.mjs --root <isolated-root> [--recover]`
478
- - imports the implementation checkout by absolute file URL passed via `--checkout <path>` (default = the worktree root detected from `process.cwd()`); uses `--root` for the board root; creates a `createView` row; writes the legacy lease with an unrelated live node child pid; binds a v1 pong on `P.coordinatorEndpointPathFor(process.platform, root)`; calls `ensureCoordinator(root)`; prints `{ outcome, unrelatedAlive, diagnostics }`; `--recover` skips scenario construction and runs one more `ensureCoordinator(root)`.
479
-
480
- - [ ] **Step 1: Write the script** (per spec §7; reuse the `STALE_COORDINATOR_CJS` shape from the test file).
481
- - [ ] **Step 2: Run it twice against the worktree checkout**
482
-
483
- ```bash
484
- node "<issue>/research/verify-u1-legacy-no-kill.mjs" --root "$env:TEMP\ab-u1-239" --checkout "<worktree>"
485
- node "<issue>/research/verify-u1-legacy-no-kill.mjs" --root "$env:TEMP\ab-u1-239" --checkout "<worktree>"
486
- ```
487
- Expected: `outcome.error === "coordinator_stale_protocol"`, `unrelatedAlive === true`, one `coordinator_replacement_skipped` with `reason: "identity_unverified"`, second run adds no diagnostic.
488
- - [ ] **Step 3: Recovery demo** — delete the isolated lease dir, run with `--recover` → `outcome.ok === true`; then stop that coordinator / delete the isolated root.
489
- - [ ] **Step 4: Final gates** — `npm run typecheck`; `node --test test/coordinator-client.test.mjs test/locks.test.mjs test/state-coordinator.integration.test.mjs`; `npm test` vs baseline (A10); record all results in the PR body.
490
- - [ ] **Step 5: Commit any final fix; do not commit the script.**
491
-
492
- ---
493
-
494
- ## Self-Review
495
-
496
- **1. Spec coverage** — A1/A2 (Task 2), A3a-A3l/A8 (Task 4; A8 re-asserted at ensure level in Task 5), A4 (Task 1), A5/A6/A7/A7b/A8 (Task 5), A9/A10/A11 (Task 5 Step 4 + Task 6 Step 4), U1 (Task 6). D1 (Task 1), D2 (Task 2), D3/D4/D5 (Task 4), D6 (Task 2 judge legacy + Task 4 live-token read + A7b). Every spec decision has an owning task.
497
-
498
- **2. Step scan** — each step names the exact test, command, function signature, or file; no TBD/skip placeholders except the four integration tests whose bodies are pinned by the fixture contract paragraph in Task 5 (they reuse the existing helper verbatim).
499
-
500
- **3. Type consistency** — `LeaseRecord`, `ReplaceOutcome`, `SkipReason`, `judgeReplacementTarget` args, and `replaceStaleCoordinator` opts names are identical across Tasks 2/4/5; `ensureCoordinator` forwards the same names.
501
-
502
- **4. Review Focus** — each of the six listed failure modes has a test: unrelated-pid survial (A6/A7/A3b), fresh evidence (A3c/A3d/A3e), never-throws (A3h/A3l), root resurrection (A3k), diagnostic flood (A8), Windows SIGTERM semantics (A3g timeout + wait-for-exit; U1 on win32).
503
-
504
- **5. Proportion** — plan ≈ 2.5× the spec's length in lines but mostly test code the spec's matrix demanded; no production body is transcribed except the flow order that the spec fixes.