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