@huseynsnmz/pi-board 0.15.3 → 0.16.0

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