@huseynsnmz/pi-board 0.15.1 → 0.16.0

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