@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,209 +0,0 @@
1
- # SELECT In-App Copy Alignment Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Make the two in-app SELECT surfaces (`listHints()`'s SELECT case and the `?` overlay's `In select` row) name every binding `handleSelectKey` implements, add the missing `?` help row, and unify the `!` wording on "fix steps" across README + overlay (issue #252).
6
-
7
- **Architecture:** Four string/row edits inside `src/ui/dashboard.ts` (`listHints()` tokens, two `renderHelp` rows, one new row), six README word swaps, and one added probe capture (`selectWide`) with one new `node:test` case. No handler logic, no new files, no render-pipeline change.
8
-
9
- **Tech Stack:** TypeScript (runtime), `node --test` + `--experimental-transform-types` probe (test), Markdown (docs).
10
-
11
- **Spec:** `docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md` (decisions D1–D5; convergence recorded in the issue's step-4 comments)
12
-
13
- ## Global Constraints
14
-
15
- - Tabs for indentation in `.ts`; English comments and commit messages.
16
- - **Do not touch handler logic**: `handleSelectKey` and every other mode handler stay byte-identical (spec D5).
17
- - **Do not touch `renderPtyHelp`'s labels** (`Fix:` / `Suggested steps`) — D4 treats them as the canonical vocabulary.
18
- - **Do not touch `#246`'s height handling**: no `fitToHeight` / `renderCenteredBox` changes; the help box simply grows from 32 to 33 lines (D3).
19
- - Exact strings are part of the acceptance: the three in-app strings below are literal contract, not paraphrase.
20
- - `npm run verify` must pass before the PR (AGENTS.md).
21
- - Worktree discipline: every path under `/home/elling/work/git-repo/pi-agent-board/.pi/worktrees/issue-252-select-key-hints` (= `$WT`); git via `git -C $WT`. Branch `issue-252-select-key-hints`; commit subjects end with `(issue #252)` and, once known, the PR-number suffix `(#N)`.
22
-
23
- ## Review Focus
24
-
25
- Conditions the spec implies but no single test step can cover; each is pinned where noted.
26
-
27
- 1. **The SELECT hint line is at its width budget.** 131 visible columns is the target (A3 allows ≤132). Adding a token without compressing another breaks A3 and pushes `m/esc cancel` out of view on a 120-column terminal. Pinned by Task 1, Step 2 (A3 assertion) and the exact token list in Step 4.
28
- 2. **`selectWide` must be captured with `rows = 40` and width 160.** At the probe's default 24 rows / 120 columns, `clip()` truncates the tail (the line is 131 columns), so A1 would fail for the wrong reason. Pinned by Task 1, Step 1.
29
- 3. **The new `?` row goes between the `/` row and the `!` row.** Row order controls what survives on short terminals: `fitToHeight` slices the tail, so rows near the bottom (the `In *` family) are the first to disappear. Keeping `?` mid-list means a 24-row terminal still shows it. Pinned by Task 1, Step 4 and Review Focus 3's manual check in Task 3.
30
- 4. **The `In select` row must fit the box without widening it past the 120-column cap.** It renders 101 columns (of the 114-column `width - 6` cap), so it stays visible at 120 columns; a longer row would be clipped there and A4 would fail. Pinned by Task 1, Step 2 (A4 regex runs against the 120-column overlay capture).
31
- 5. **README wording is not test-covered** (no test reads `README.md`). Task 2's `grep -c` counts are the only automated guard; the user test U1 covers the rest.
32
-
33
- ---
34
-
35
- ### Task 1: In-app surfaces (hint line + help overlay) with probe assertions
36
-
37
- **Files:**
38
- - Modify: `src/ui/dashboard.ts` — `listHints()` SELECT branch (currently lines 1348-1357), `renderHelp()` rows array (currently line 1723 `In select`, insert new row after line 1715 `/`)
39
- - Modify: `test-support/vim-keys-wrap.ts` (capture after line 130; `out.copy` at line 146)
40
- - Modify: `test/dashboard-vim-keys.test.mjs` (append one new `test(...)`)
41
-
42
- **Interfaces:**
43
- - Consumes: existing `render(width)` / `listHints()` / `renderHelp(width)`; the probe's white-box `dash.mode` handle and `tui.terminal.rows` handle already used at `vim-keys-wrap.ts:122-126`.
44
- - Produces: probe field `out.copy.selectWide` (string) — the SELECT screen at 160 columns, consumed by Task 3's verification and by future key-copy assertions.
45
-
46
- **Spec acceptance IDs:** A1, A2, A3, A4, A5, A6, A7 (Task 3 covers A8, A9).
47
-
48
- - [ ] **Step 1: Add the failing probe capture**
49
-
50
- In `test-support/vim-keys-wrap.ts`, immediately after `const selectRender = dash.render(120).join("\n");` (line 130) add:
51
-
52
- ```ts
53
- // The SELECT hint line is >120 columns; render wider so the tail is observable.
54
- tui.terminal.rows = 40;
55
- const selectWide = dash.render(160).join("\n");
56
- tui.terminal.rows = 24;
57
- ```
58
-
59
- Then extend the single-line `out.copy` initializer (line 146) to include `selectWide`:
60
-
61
- ```ts
62
- out.copy = { help120, listRender, selectRender, selectWide, sessionRender, evidenceRender, peekRender, help80Lines: help80.length };
63
- ```
64
-
65
- - [ ] **Step 2: Add the failing test**
66
-
67
- Append to `test/dashboard-vim-keys.test.mjs`:
68
-
69
- ```js
70
- test("SELECT surfaces document every binding and the help overlay gains a ? row (issue #252)", () => {
71
- const out = JSON.parse(execFileSync(process.execPath, ["--experimental-transform-types", PROBE], { encoding: "utf8", timeout: 30_000 }));
72
- const { selectWide, help120 } = out.copy;
73
- // A1: the SELECT hint line names every binding, not just five.
74
- for (const token of ["↑↓/j/k move", "space toggle", "a all visible", "u clear", "d done", "ctrl+x delete", "m/esc cancel", "? help", "! pty"]) {
75
- assert.ok(selectWide.includes(token), `SELECT hint line must name ${token}`);
76
- }
77
- assert.match(selectWide, /· \? help · ! pty/);
78
- // A2: the stale wording is gone.
79
- assert.doesNotMatch(selectWide, /d move done/);
80
- assert.doesNotMatch(selectWide, /ctrl\+x delete done/);
81
- // A3: the line stays inside its width budget (131 columns today).
82
- const selectLine = selectWide.split("\n").find((line) => line.includes("[SELECT]"));
83
- assert.ok(selectLine, "SELECT hint line must be present");
84
- assert.ok(selectLine.trimEnd().length <= 132, `SELECT hint line grew to ${selectLine.trimEnd().length}`);
85
- // A4: the overlay's In select row catches up with the handler.
86
- assert.match(help120, /In select\s+↑↓\/j\/k move · space toggle · a all · u clear · d done · ctrl\+x delete · m\/esc cancel/);
87
- assert.doesNotMatch(help120, /a select all/);
88
- // A5: the overlay finally explains ?.
89
- assert.match(help120, /\?\s+Open the key help overlay \(list and multi-select\)/);
90
- // A6: one canonical ! wording in-app.
91
- assert.match(help120, /Open node-pty diagnostics and fix steps/);
92
- assert.doesNotMatch(help120, /repair hints/);
93
- });
94
- ```
95
-
96
- - [ ] **Step 3: Run it and confirm it fails for the right reasons**
97
-
98
- Run: `node --test test/dashboard-vim-keys.test.mjs`
99
- Expected: FAIL. A1 reports a missing token (the first is `d done`), A4/A5 fail, A6's `repair hints` negative already passes today. No TypeScript or probe crash.
100
-
101
- - [ ] **Step 4: Implement the three in-app edits**
102
-
103
- `src/ui/dashboard.ts`, `listHints()` SELECT branch — replace the token array so it reads exactly:
104
-
105
- ```ts
106
- if (this.mode === "select") {
107
- return this.hintLine("SELECT", "accent", [
108
- `${this.selectionCount()} selected`,
109
- ...(unread > 0 ? [`•${unread} unread`] : []),
110
- "↑↓/j/k move",
111
- "space toggle",
112
- "a all visible",
113
- "u clear",
114
- "d done",
115
- "ctrl+x delete",
116
- "m/esc cancel",
117
- "? help",
118
- "! pty",
119
- ]);
120
- }
121
- ```
122
-
123
- In the same file, `renderHelp()` rows array: replace the `In select` row and insert the `?` row after the `/` row:
124
-
125
- ```ts
126
- ["/", "Filter in normal mode; use i then / for slash commands"],
127
- ["?", "Open the key help overlay (list and multi-select)"],
128
- ["!", "Open node-pty diagnostics and fix steps"],
129
- ```
130
-
131
- ```ts
132
- ["In select", "↑↓/j/k move · space toggle · a all · u clear · d done · ctrl+x delete · m/esc cancel"],
133
- ```
134
-
135
- - [ ] **Step 5: Run the tests and confirm they pass**
136
-
137
- Run: `node --test test/dashboard-vim-keys.test.mjs`
138
- Expected: PASS, both the pre-existing `#229`/`#245` cases and the new `#252` case.
139
-
140
- - [ ] **Step 6: Run the full suite**
141
-
142
- Run: `npm test`
143
- Expected: green (perf gate is opt-in and stays skipped here).
144
-
145
- - [ ] **Step 7: Commit**
146
-
147
- ```bash
148
- git -C "$WT" add src/ui/dashboard.ts test-support/vim-keys-wrap.ts test/dashboard-vim-keys.test.mjs
149
- git -C "$WT" commit -m "docs(ui): document every SELECT binding in hints and help; add the ? row (issue #252)"
150
- ```
151
-
152
- ---
153
-
154
- ### Task 2: README `!` wording
155
-
156
- **Files:**
157
- - Modify: `README.md` — lines 148, 166, 180, 200 (tables), 289, 368 (prose)
158
-
159
- **Interfaces:**
160
- - Consumes: nothing.
161
- - Produces: nothing consumed by code; the canonical wording decision (D4) lives here.
162
-
163
- **Spec acceptance IDs:** A8.
164
-
165
- - [ ] **Step 1: Swap the six occurrences**
166
-
167
- ```bash
168
- grep -n 'repair hints' README.md
169
- ```
170
-
171
- Expect exactly six lines: 148, 166, 180, 200 (`| \`!\` | Open node-pty diagnostics and repair hints. |`) and 289, 368 (prose). Replace `repair hints` with `fix steps` in all six; leave the surrounding sentence structure intact, so line 289 reads `Press \`!\` in the dashboard for diagnosis and fix steps.` and line 368 reads `… open the diagnostic panel and follow its fix steps.`
172
-
173
- - [ ] **Step 2: Verify the counts**
174
-
175
- Run: `grep -c 'repair hints' README.md` → expect `0` (and a non-zero exit status; that is expected here).
176
- Run: `grep -c 'fix steps' README.md` → expect `6`.
177
-
178
- - [ ] **Step 3: Commit**
179
-
180
- ```bash
181
- git -C "$WT" add README.md
182
- git -C "$WT" commit -m "docs: unify the ! panel wording on fix steps (issue #252)"
183
- ```
184
-
185
- ---
186
-
187
- ### Task 3: Verification gate (coordinator)
188
-
189
- **Files:** none.
190
-
191
- **Spec acceptance IDs:** A7 (re-run), A8 (re-check), A9, plus the U1 record.
192
-
193
- - [ ] **Step 1: Full repository gate**
194
-
195
- Run: `npm run verify` (from `$WT`)
196
- Expected: typecheck, perf gate 3/3, full test suite, coverage thresholds (lines 85 / funcs 80 / branches 70), and `pack:dry` all pass.
197
-
198
- - [ ] **Step 2: Re-check the README counts and the in-app strings**
199
-
200
- Run: `grep -c 'repair hints' README.md` (0) and `grep -c 'fix steps' README.md` (6).
201
- Run: `grep -n 'm/esc cancel' src/ui/dashboard.ts` → expect two hits (hint line + `In select` row).
202
-
203
- - [ ] **Step 3: Record U1 as `pending`**
204
-
205
- U1 needs the merged code loaded by a fresh pi session, so it cannot run before merge. Record it on the issue as `pending` with the spec's prerequisites (pull, checkout auto-discovery, restart pi) instead of claiming it passed.
206
-
207
- - [ ] **Step 4: Manual width spot-check (Review Focus 3)**
208
-
209
- Render the overlay at 24 rows and confirm the new `?` row is still visible while the `In *` rows are clipped — i.e. `?` sits above the cut line. A one-off probe script outside the repo is the cheapest way; do not commit it.
@@ -1,442 +0,0 @@
1
- # Issue #255 — Coordinator endpoint zombie: 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:** A coordinator that loses its endpoint steps down within one heartbeat tick, and a
6
- deposed coordinator can no longer unlink an endpoint it does not own.
7
-
8
- **Architecture:** Two changes. (1) `classifyEndpointHealth` + `socketIdentityObservation` +
9
- `shouldUnlinkEndpoint` become pure exports in `src/core/host-coordination.mjs`; (2) the
10
- coordinator runner wires them into the existing 1 s heartbeat tick (self-supervision) and into
11
- `shutdown()` (owner-token cleanup gate). All I/O stays in `runner/state-coordinator.mjs`; no
12
- new timers, no protocol changes.
13
-
14
- **Tech Stack:** Node ESM (`.mjs`), `node:test` + `assert/strict`, existing repo fixtures.
15
-
16
- **Spec:** `docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md`
17
-
18
- ## Global Constraints
19
-
20
- - POSIX-only behaviour: all endpoint assertions are guarded by `process.platform !== "win32"`.
21
- - No new timers: reuse `heartbeatTimer` (state-coordinator.mjs:344).
22
- - Diagnostic appends from a step-down path MUST be `try/catch`-fenced (#224 D4).
23
- - No wire-format change; no new protocol message types.
24
- - Tabs for indentation; English comments.
25
- - Commit subjects: conventional (`test:`, `feat:`, `fix:`), with `(issue #255)` suffix.
26
- - New integration case titles MUST start with `#255 A<n>: ` so `--test-name-pattern` can select them.
27
-
28
- ## Review Focus
29
-
30
- - A transient `statSync` error (EACCES/EBUSY) must never step a healthy coordinator down —
31
- pinned by A2 (`{kind:"unknown"}` row) and by the `unknown ⇒ never step down` classify row.
32
- - A failing diagnostic append must not turn a clean step-down into a crash — pinned by A4b.
33
- - A deposed owner must not delete its own (or a successor's) endpoint — pinned by A3.
34
- - The replacement endpoint must survive the step-down that it caused — pinned by A5.
35
- - A14's single-owner cleanup must stay green — pinned by A6/A14 regression run.
36
-
37
- ---
38
-
39
- ### Task 1: Pure decision functions + unit tests
40
-
41
- **Files:**
42
- - Modify: `src/core/host-coordination.mjs` (append three exports after `ownsEndpoint`, ~:162)
43
- - Test: `test/host-coordination.test.mjs` (import them; add two tests after the `ownsEndpoint` test at :125)
44
-
45
- **Interfaces:**
46
- - Consumes: `ownsEndpoint(bound, current)` (already exported at host-coordination.mjs:160).
47
- - Produces:
48
- - `shouldUnlinkEndpoint({ stillOwner, compromised, bound, current }) -> boolean`
49
- - `socketIdentityObservation(stat: {dev:number,ino:number}|null, errorCode: string|null) -> {kind:"ok",dev,ino}|{kind:"missing"}|{kind:"unknown"}`
50
- - `classifyEndpointHealth(bound: {dev:number,ino:number}|null, observed: observation|null) -> "ok"|"missing"|"replaced"|"unknown"`
51
-
52
- - [ ] **Step 1: Write the failing tests**
53
-
54
- In `test/host-coordination.test.mjs`, extend the import list with the three names and append:
55
-
56
- ```js
57
- test("shouldUnlinkEndpoint gates on lease ownership and compromise", () => {
58
- const same = { dev: 1, ino: 2 };
59
- assert.equal(shouldUnlinkEndpoint({ stillOwner: true, compromised: false, bound: same, current: same }), true);
60
- assert.equal(shouldUnlinkEndpoint({ stillOwner: false, compromised: false, bound: same, current: same }), false);
61
- assert.equal(shouldUnlinkEndpoint({ stillOwner: true, compromised: true, bound: same, current: same }), false);
62
- assert.equal(shouldUnlinkEndpoint({ stillOwner: true, compromised: false, bound: same, current: { dev: 1, ino: 3 } }), false);
63
- assert.equal(shouldUnlinkEndpoint({ stillOwner: false, compromised: false, bound: same, current: { dev: 1, ino: 3 } }), false);
64
- assert.equal(shouldUnlinkEndpoint({ stillOwner: true, compromised: true, bound: same, current: { dev: 1, ino: 3 } }), false);
65
- assert.equal(shouldUnlinkEndpoint({ stillOwner: false, compromised: true, bound: same, current: same }), false);
66
- assert.equal(shouldUnlinkEndpoint({ stillOwner: false, compromised: true, bound: same, current: { dev: 1, ino: 3 } }), false);
67
- // degenerate rows: without both identities nothing may be unlinked
68
- assert.equal(shouldUnlinkEndpoint({ stillOwner: true, compromised: false, bound: null, current: same }), false);
69
- assert.equal(shouldUnlinkEndpoint({ stillOwner: true, compromised: false, bound: same, current: null }), false);
70
- });
71
-
72
- test("socketIdentityObservation maps one stat attempt", () => {
73
- assert.deepEqual(socketIdentityObservation({ dev: 7, ino: 8 }, null), { kind: "ok", dev: 7, ino: 8 });
74
- assert.deepEqual(socketIdentityObservation(null, "ENOENT"), { kind: "missing" });
75
- assert.deepEqual(socketIdentityObservation(null, "EACCES"), { kind: "unknown" });
76
- assert.deepEqual(socketIdentityObservation(null, "EBUSY"), { kind: "unknown" });
77
- assert.deepEqual(socketIdentityObservation(null, "ELOOP"), { kind: "unknown" });
78
- });
79
-
80
- test("classifyEndpointHealth never steps down without an identity", () => {
81
- const bound = { dev: 1, ino: 2 };
82
- const ok = { kind: "ok", dev: 1, ino: 2 };
83
- assert.equal(classifyEndpointHealth(bound, ok), "ok");
84
- assert.equal(classifyEndpointHealth(bound, { kind: "ok", dev: 1, ino: 3 }), "replaced");
85
- assert.equal(classifyEndpointHealth(bound, { kind: "missing" }), "missing");
86
- assert.equal(classifyEndpointHealth(bound, { kind: "unknown" }), "unknown");
87
- assert.equal(classifyEndpointHealth(null, ok), "unknown");
88
- assert.equal(classifyEndpointHealth(bound, null), "unknown");
89
- });
90
- ```
91
-
92
- - [ ] **Step 2: Run the tests to verify they fail**
93
-
94
- Run: `node --test test/host-coordination.test.mjs`
95
- Expected: ERR_MODULE_NOT_FOUND / import error naming `shouldUnlinkEndpoint` (or `undefined`).
96
-
97
- - [ ] **Step 3: Implement the three exports**
98
-
99
- In `src/core/host-coordination.mjs`, after `ownsEndpoint`, add the three functions exactly as
100
- specified in the design doc §4.2/§4.3, with JSDoc. Behaviour:
101
- `shouldUnlinkEndpoint` returns `false` unless `stillOwner && !compromised && ownsEndpoint(bound, current)`;
102
- `socketIdentityObservation` returns the ok object on `stat != null`, `missing` iff
103
- `errorCode === "ENOENT"`, else `unknown`; `classifyEndpointHealth` returns `unknown` when
104
- `!bound || !observed || observed.kind === "unknown"`, `missing` on `kind === "missing"`, else
105
- `ownsEndpoint(bound, observed)` → `ok`/`replaced`.
106
-
107
- - [ ] **Step 4: Run the tests to verify they pass**
108
-
109
- Run: `node --test test/host-coordination.test.mjs`
110
- Expected: all tests pass (3 new + existing).
111
-
112
- - [ ] **Step 5: Commit**
113
-
114
- ```bash
115
- git add src/core/host-coordination.mjs test/host-coordination.test.mjs
116
- git commit -m "feat(coordination): add endpoint health and cleanup-gate decisions (issue #255)"
117
- ```
118
-
119
- ---
120
-
121
- ### Task 2: R1 — owner-token cleanup gate in shutdown()
122
-
123
- **Files:**
124
- - Modify: `runner/state-coordinator.mjs` (import `shouldUnlinkEndpoint`; add `endpointCompromised`; rewrite the shutdown-time-site block at :411-418; add `safeIsOwner`)
125
- - Test: `test/state-coordinator.integration.test.mjs` (add `#255 A3` and `#255 A6`)
126
-
127
- **Interfaces:**
128
- - Consumes: `shouldUnlinkEndpoint` from Task 1; `lease.isOwner()` (locks.mjs:176); existing
129
- `startCoordinator`/`readyClient`/`waitForExit`/`freshRoot`/`createView`/`readDiagnostics` helpers.
130
- - Produces: `endpointCompromised` flag (module-scope `let`, set by Task 3's tick) and
131
- `safeIsOwner()` shell helper (returns false on throw).
132
-
133
- - [ ] **Step 1: Write the failing tests**
134
-
135
- In `test/state-coordinator.integration.test.mjs` (POSIX guards), add:
136
-
137
- ```js
138
- test("#255 A3: a deposed coordinator keeps its own endpoint", async (t) => {
139
- if (process.platform === "win32") return t.skip("POSIX AF_UNIX only");
140
- const root = freshRoot();
141
- let child = null;
142
- t.after(async () => {
143
- if (child && isAlive(child.pid)) { child.kill("SIGTERM"); await waitForExit(child); }
144
- rmSync(root, { recursive: true, force: true });
145
- });
146
- createView(root, { id: "v1", name: "x", cwd: root });
147
- child = startCoordinator(root);
148
- await readyClient(root);
149
- const socketPath = P.coordinatorEndpointPathFor(process.platform, root);
150
- const lockPath = P.viewLockPath(root, "_coordinator", "state-coordinator");
151
- assert.ok(existsSync(socketPath), "precondition: endpoint exists");
152
- // Simulate "the lease moved elsewhere": remove the lease dir (quarantine shape).
153
- rmSync(lockPath, { recursive: true, force: true });
154
- assert.ok(existsSync(socketPath), "anti-vacuous: endpoint still exists after lease removal");
155
- const code = await waitForExit(child, 6000);
156
- assert.equal(code, 0, "clean step-down");
157
- assert.ok(existsSync(socketPath), "a deposed owner must not unlink its own endpoint");
158
- });
159
-
160
- test("#255 A6: SIGTERM on a healthy owner still unlinks the endpoint", async (t) => {
161
- if (process.platform === "win32") return t.skip("POSIX AF_UNIX only");
162
- const root = freshRoot();
163
- let child = null;
164
- t.after(async () => { rmSync(root, { recursive: true, force: true }); });
165
- createView(root, { id: "v1", name: "x", cwd: root });
166
- child = startCoordinator(root);
167
- await readyClient(root);
168
- const socketPath = P.coordinatorEndpointPathFor(process.platform, root);
169
- assert.ok(existsSync(socketPath), "precondition: endpoint exists");
170
- child.kill("SIGTERM");
171
- await waitForExit(child);
172
- assert.equal(existsSync(socketPath), false, "healthy owner cleans its own endpoint");
173
- });
174
- ```
175
-
176
- - [ ] **Step 2: Run them to verify they fail**
177
-
178
- Run: `node --test --test-name-pattern="#255 A3|#255 A6" test/state-coordinator.integration.test.mjs`
179
- Expected: A3 fails (`a deposed owner must not unlink its own endpoint` — old code unlinks it);
180
- A6 passes already (regression guard).
181
-
182
- - [ ] **Step 3: Implement the shutdown gate**
183
-
184
- In `runner/state-coordinator.mjs`:
185
- - import `shouldUnlinkEndpoint` from `../src/core/host-coordination.mjs`;
186
- - add `let endpointCompromised = false;` next to `boundSocketIdentity`;
187
- - add the shell helper above `shutdown()`:
188
- ```js
189
- function safeIsOwner() {
190
- try { return lease.isOwner(); } catch { return false; }
191
- }
192
- ```
193
- - replace the shutdown-time-site unlink block with:
194
- ```js
195
- const stillOwner = safeIsOwner();
196
- if (process.platform !== "win32" && stillOwner && !endpointCompromised && boundSocketIdentity) {
197
- try {
198
- const st = statSync(socketPath);
199
- if (shouldUnlinkEndpoint({ stillOwner, compromised: endpointCompromised,
200
- bound: boundSocketIdentity, current: { dev: st.dev, ino: st.ino } })) {
201
- try { unlinkSync(socketPath); } catch { /* best effort */ }
202
- }
203
- } catch { /* path gone — nothing to clean */ }
204
- }
205
- ```
206
-
207
- - [ ] **Step 4: Run the tests to verify they pass**
208
-
209
- Run: `node --test --test-name-pattern="#255 A3|#255 A6|#224 A14" test/state-coordinator.integration.test.mjs`
210
- Expected: A3, A6, A14, A14b all pass.
211
-
212
- - [ ] **Step 5: Commit**
213
-
214
- ```bash
215
- git add runner/state-coordinator.mjs test/state-coordinator.integration.test.mjs
216
- git commit -m "fix(coordinator): gate endpoint cleanup on lease ownership (issue #255)"
217
- ```
218
-
219
- ---
220
-
221
- ### Task 3: R2 — supervised endpoint health in the heartbeat tick
222
-
223
- **Files:**
224
- - Modify: `runner/state-coordinator.mjs` (import the two pure functions; add `statSocketObservation`; insert the endpoint check in the `heartbeatTimer` callback between `rootVanishedOrReplaced` and `lease.touch()`; extend the identity-capture catch with a stderr line)
225
- - Test: `test/state-coordinator.integration.test.mjs` (add `#255 A4`, `#255 A4b`, `#255 A5`; optional `#255 A8`)
226
-
227
- **Interfaces:**
228
- - Consumes: `socketIdentityObservation`, `classifyEndpointHealth` (Task 1);
229
- `endpointCompromised` (Task 2); `appendCoordinatorDiagnostic` (:118-123);
230
- `AGENT_BOARD_TEST_FAIL_DIAGNOSTIC` and `AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS` knobs.
231
- - Produces: `statSocketObservation(socketPath) -> observation` shell helper;
232
- diagnostic code `coordinator_endpoint_lost` with `details.state ∈ {"missing","replaced"}`.
233
-
234
- - [ ] **Step 1: Write the failing tests**
235
-
236
- Add to `test/state-coordinator.integration.test.mjs`:
237
-
238
- ```js
239
- test("#255 A4: a missing endpoint steps the coordinator down", async (t) => {
240
- if (process.platform === "win32") return t.skip("POSIX AF_UNIX only");
241
- const root = freshRoot();
242
- let child = null;
243
- t.after(async () => {
244
- if (child && isAlive(child.pid)) { child.kill("SIGTERM"); await waitForExit(child); }
245
- rmSync(root, { recursive: true, force: true });
246
- });
247
- createView(root, { id: "v1", name: "x", cwd: root });
248
- process.env.AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS = "100";
249
- child = startCoordinator(root);
250
- await readyClient(root);
251
- const socketPath = P.coordinatorEndpointPathFor(process.platform, root);
252
- unlinkSync(socketPath);
253
- const code = await waitForExit(child, 5000);
254
- delete process.env.AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS;
255
- assert.equal(code, 0, "clean step-down");
256
- const lost = readDiagnostics(root, "_coordinator").find((e) => e.code === "coordinator_endpoint_lost");
257
- assert.ok(lost, "endpoint loss is recorded");
258
- assert.equal(lost.details.state, "missing");
259
- });
260
-
261
- test("#255 A4b: a failing diagnostic write does not block the step-down", async (t) => {
262
- if (process.platform === "win32") return t.skip("POSIX AF_UNIX only");
263
- const root = freshRoot();
264
- let child = null;
265
- t.after(async () => {
266
- if (child && isAlive(child.pid)) { child.kill("SIGTERM"); await waitForExit(child); }
267
- rmSync(root, { recursive: true, force: true });
268
- });
269
- createView(root, { id: "v1", name: "x", cwd: root });
270
- process.env.AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS = "100";
271
- process.env.AGENT_BOARD_TEST_FAIL_DIAGNOSTIC = "1";
272
- child = startCoordinator(root);
273
- await readyClient(root);
274
- unlinkSync(P.coordinatorEndpointPathFor(process.platform, root));
275
- const code = await waitForExit(child, 5000);
276
- delete process.env.AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS;
277
- delete process.env.AGENT_BOARD_TEST_FAIL_DIAGNOSTIC;
278
- assert.equal(code, 0, "exit 0 even when the diagnostic append throws");
279
- assert.equal(existsSync(P.viewLockPath(root, "_coordinator", "state-coordinator")), false, "lease released");
280
- });
281
-
282
- test("#255 A5: a replaced endpoint steps the coordinator down and the replacement survives", async (t) => {
283
- if (process.platform === "win32") return t.skip("POSIX AF_UNIX only");
284
- const root = freshRoot();
285
- const standinPath = join(root, `.standin-${process.pid}.sock`);
286
- let child = null;
287
- let standin = null;
288
- t.after(async () => {
289
- if (child && isAlive(child.pid)) { child.kill("SIGTERM"); await waitForExit(child); }
290
- if (standin) await new Promise((r) => standin.close(r));
291
- rmSync(root, { recursive: true, force: true });
292
- });
293
- createView(root, { id: "v1", name: "x", cwd: root });
294
- process.env.AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS = "100";
295
- child = startCoordinator(root);
296
- await readyClient(root);
297
- const socketPath = P.coordinatorEndpointPathFor(process.platform, root);
298
- const before = statSync(socketPath).ino;
299
- standin = createServer(() => {});
300
- await new Promise((r) => standin.listen(standinPath, r));
301
- renameSync(standinPath, socketPath); // atomic replacement, same filesystem
302
- assert.notEqual(statSync(socketPath).ino, before, "stand-in really replaced the inode");
303
- const code = await waitForExit(child, 5000);
304
- delete process.env.AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS;
305
- assert.equal(code, 0, "clean step-down");
306
- const lost = readDiagnostics(root, "_coordinator").find((e) => e.code === "coordinator_endpoint_lost");
307
- assert.ok(lost, "replacement is recorded");
308
- assert.equal(lost.details.state, "replaced");
309
- assert.ok(existsSync(socketPath), "the replacement endpoint must survive the step-down");
310
- });
311
- ```
312
-
313
- Required new imports in the test file: `unlinkSync`, `renameSync` (already imports `existsSync`,
314
- `mkdtempSync`, `mkdirSync`, `appendFileSync`, `rmSync`, `statSync`, `writeFileSync`), and
315
- `createServer` from `node:net` (the file currently imports `createConnection` only).
316
-
317
- - [ ] **Step 2: Run them to verify they fail**
318
-
319
- Run: `node --test --test-name-pattern="#255 A4|#255 A4b|#255 A5" test/state-coordinator.integration.test.mjs`
320
- Expected: all three time out/fail because the coordinator never steps down (old behaviour).
321
-
322
- - [ ] **Step 3: Implement R2**
323
-
324
- In `runner/state-coordinator.mjs`:
325
- - import `classifyEndpointHealth`, `socketIdentityObservation` from `../src/core/host-coordination.mjs`;
326
- - add the shell helper near `safeIsOwner`:
327
- ```js
328
- function statSocketObservation(socketPath) {
329
- let stat = null, errorCode = null;
330
- try { stat = statSync(socketPath); } catch (err) { errorCode = err?.code ?? "UNKNOWN"; }
331
- return socketIdentityObservation(stat, errorCode);
332
- }
333
- ```
334
- - extend the identity-capture catch inside `server.listen` with one stderr line:
335
- `state-coordinator: endpoint identity unavailable (<err>); self-supervision disabled`;
336
- - inside the `heartbeatTimer` callback, after the `rootVanishedOrReplaced` block and before the
337
- `lease.touch()` try/catch, insert:
338
- ```js
339
- const endpointState = classifyEndpointHealth(boundSocketIdentity, statSocketObservation(socketPath));
340
- if (endpointState === "missing" || endpointState === "replaced") {
341
- endpointCompromised = true; // veto cleanup in shutdown (R1)
342
- try {
343
- appendCoordinatorDiagnostic("coordinator_endpoint_lost", { state: endpointState }, "warn");
344
- } catch { /* a failing diagnostic must not block the step-down (#224 D4) */ }
345
- process.stderr.write(`state-coordinator: endpoint ${endpointState} (${socketPath}); stepping down\n`);
346
- shutdown(0);
347
- return;
348
- }
349
- ```
350
-
351
- - [ ] **Step 4: Run the tests to verify they pass**
352
-
353
- Run: `node --test --test-name-pattern="#255" test/state-coordinator.integration.test.mjs`
354
- Expected: A3, A4, A4b, A5, A6 all pass.
355
-
356
- - [ ] **Step 5: Commit**
357
-
358
- ```bash
359
- git add runner/state-coordinator.mjs test/state-coordinator.integration.test.mjs
360
- git commit -m "feat(coordinator): supervise the bound endpoint and step down when it is lost (issue #255)"
361
- ```
362
-
363
- ---
364
-
365
- ### Task 4: Full verification and regression fixes
366
-
367
- **Files:**
368
- - Modify (only if a gate fails): whichever file the failure points at.
369
- - Test: whole suite.
370
-
371
- **Interfaces:**
372
- - Consumes: everything above.
373
- - Produces: a green `npm run verify` on the worktree branch.
374
-
375
- - [ ] **Step 1: Typecheck**
376
-
377
- Run: `npm run typecheck`
378
- Expected: exit 0. Fix any type errors in the new helpers.
379
-
380
- - [ ] **Step 2: Targeted coordinator + coordination suites**
381
-
382
- Run: `node --test test/host-coordination.test.mjs test/locks.test.mjs test/coordinator-lifecycle.test.mjs test/coordinator-client.test.mjs`
383
- Expected: all pass.
384
-
385
- - [ ] **Step 3: Full test suite with coverage**
386
-
387
- Run: `npm run test:coverage`
388
- Expected: all tests pass; thresholds lines 85 / funcs 80 / branches 70. If a legacy test now
389
- fails, inspect whether the change altered intended behaviour (A14/A14b must stay green) before
390
- touching the test.
391
-
392
- - [ ] **Step 4: Perf gate + pack**
393
-
394
- Run: `npm run test:perf && npm run pack:dry`
395
- Expected: both exit 0.
396
-
397
- - [ ] **Step 5: Final gate**
398
-
399
- Run: `npm run verify`
400
- Expected: exit 0.
401
-
402
- - [ ] **Step 6: Commit (only if Step 1-5 changed files)**
403
-
404
- ```bash
405
- git add <changed files>
406
- git commit -m "test(coordinator): harden endpoint-loss cases after full-suite verification (issue #255)"
407
- ```
408
-
409
- ---
410
-
411
- ### Task 5: Local code review before PR
412
-
413
- **Files:** none (review only).
414
-
415
- - [ ] **Step 1: Inspect the branch diff**
416
-
417
- Run: `git log --oneline main..HEAD && git diff main...HEAD --stat`
418
- Expected: spec commit + Tasks 1-3 (+ Task 4 if it changed files); no unrelated files.
419
-
420
- - [ ] **Step 2: Run the review checklist**
421
-
422
- Required: `requesting-code-review` skill (or `workflow` code-review mode). Focus: the Review
423
- Focus list above, the POSIX/win32 guards, and whether the A3/A4/A5 fixtures can flake
424
- (heartbeat pinned, atomic rename, no polling window).
425
-
426
- - [ ] **Step 3: Record the review outcome in the plan**
427
-
428
- Append a short "Review outcome" section (findings + dispositions). Commit if it changes files.
429
-
430
- ---
431
-
432
- ## Task ↔ acceptance traceability
433
-
434
- | Spec acceptance ID | Task |
435
- |---|---|
436
- | A1, A2 | Task 1 |
437
- | A3, A6 | Task 2 |
438
- | A4, A4b, A5 | Task 3 |
439
- | A8 (optional smoke) | Task 3 (optional; non-gating) |
440
- | A7 (#241 regression) | Task 4 Step 2/3 |
441
- | A9–A12 | Task 4 |
442
- | U1, U2 | post-merge manual (recorded as pending until executed) |