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