@huseynsnmz/pi-board 0.15.3 → 0.17.0

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