@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,504 +0,0 @@
|
|
|
1
|
-
# Coordinator Replacement Identity Verification — Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use subagent-driven-development (recommended) or executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Make `replaceStaleCoordinator` signal a lease-owner pid only after proving, from fresh evidence, that the lease record belongs to the coordinator actually serving the endpoint — and never let a diagnostic or signal failure break `sendStateCommand`.
|
|
6
|
-
|
|
7
|
-
**Architecture:** The coordinator records its in-process `instanceId` inside the already-passthrough lease `identity` object. The client reads the lease, judges same-ness with a pure function (instanceId equality, else a live `/proc` startToken compare for Linux legacy records), re-confirms with a second endpoint probe plus a lease re-read (TOCTOU), then sends SIGTERM and waits for process exit. The residual lease is left to the existing #224 reclaim — no client-side lease deletion. Skipped signals write one deduped diagnostic.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node ≥20 ESM (`.mjs`), `node:test` + `assert/strict`, no new dependencies.
|
|
10
|
-
|
|
11
|
-
**Spec:** `docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md` (issue #239, sha256 `a6e578bf…`; worktree is based on `main` @ `60083f6` — the two upstream commits since the spec baseline touch only UI files).
|
|
12
|
-
|
|
13
|
-
## Global Constraints
|
|
14
|
-
|
|
15
|
-
- Tabs for indentation in `.mjs`/`.ts` sources; English comments and commit messages; conventional commit subjects with the PR number only after the PR exists (`(#N)` appended later if needed).
|
|
16
|
-
- `coordinator-client.mjs` module contract: **never throws** for `ensureCoordinator`/`sendStateCommand` (existing doc at `src/core/coordinator-client.mjs:11-24`). Every new path must preserve it.
|
|
17
|
-
- Do not change `src/core/locks.mjs`, do not bump `COORDINATOR_PROTOCOL_VERSION`, do not add client-side lease deletion (spec non-goals N1/N2/N3/N5).
|
|
18
|
-
- `EnsureResult` shape stays `{ ok, instanceId?, pid?, error? }`; `coordinator_stale_protocol` stays the failure code (`test/coordinator-client.test.mjs:409` deepEqual must keep passing).
|
|
19
|
-
- Lease `identity` stays a passthrough object; `classifyLeaseOwner` behavior must not change.
|
|
20
|
-
- Test commands: `node --test test/coordinator-client.test.mjs`; full local suite `npm test`; CI gate is `npm run verify`.
|
|
21
|
-
- Local Windows baseline (do not regress): 1375 tests / 62 fail / 5 skip; `coordinator-client` and `locks` files are green. Failure-name list: `~/.github-issue-driven/zhuxixi/pi-agent-board/issue-239/research/baseline-failures.txt` (strip trailing ` (1234.5ms)` before set comparison).
|
|
22
|
-
|
|
23
|
-
## Review Focus
|
|
24
|
-
|
|
25
|
-
- **Signal must never reach an unrelated pid**: a lease whose `pid` belongs to a live non-coordinator process must not be SIGTERMed under any branch (J2/J4/J6/J7, and the re-probe paths).
|
|
26
|
-
- **Fresh evidence only**: the judge input `probedInstanceId` must be the pong observed in *this* call, never a cached value; the re-probe must catch "old instance already gone" and "another old instance took over".
|
|
27
|
-
- **never-throws**: diagnostic writes (`appendDiagnosticOnce`), `kill`, `isAlive`, and `readProcessStartToken` are all fallible; each failure has a defined non-throwing outcome (spec D4/D5).
|
|
28
|
-
- **No root resurrection**: a deleted board root must not be recreated by a diagnostic write (spec A3k, #241 class).
|
|
29
|
-
- **Diagnostic flood**: repeated `ensureCoordinator` calls (run_progress beats ~4/s) must not grow `_coordinator/diagnostics.jsonl`.
|
|
30
|
-
- **Windows SIGTERM is TerminateProcess**: the wait-for-exit is the only thing that stops the next spawn from racing a still-exiting process; `existsSync(socketPath)` is meaningless on win32 (named pipe).
|
|
31
|
-
|
|
32
|
-
## File Structure
|
|
33
|
-
|
|
34
|
-
| File | Responsibility |
|
|
35
|
-
|---|---|
|
|
36
|
-
| `src/core/coordinator-client.mjs` | lease record reader, pure judge, replacement orchestration, deduped skip diagnostic, `ensureCoordinator` wiring |
|
|
37
|
-
| `runner/state-coordinator.mjs` | write `instanceId` into the lease `identity` (one-line change at the existing `acquireOwnedViewLock` call) |
|
|
38
|
-
| `test/coordinator-client.test.mjs` | unit tests (T1/T2/T3) + integration fixtures (T4/T5/T6) |
|
|
39
|
-
|
|
40
|
-
No new source files. The U1 verification script lives outside the repo in the issue research directory (not committed).
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
### Task 1: Coordinator records `identity.instanceId`
|
|
45
|
-
|
|
46
|
-
**Files:**
|
|
47
|
-
- Modify: `runner/state-coordinator.mjs:227-231` (the `acquireOwnedViewLock` call in `main()`)
|
|
48
|
-
- Test: `test/coordinator-client.test.mjs` (extend the existing real-spawn happy-path test near `:164-186`)
|
|
49
|
-
|
|
50
|
-
**Interfaces:**
|
|
51
|
-
- Consumes: `instanceId` (already generated at `runner/state-coordinator.mjs:198`)
|
|
52
|
-
- Produces: lease `owner.json` field `identity.instanceId` — the value is identical to the pong's `instanceId` (`runner/state-coordinator.mjs:431`)
|
|
53
|
-
|
|
54
|
-
- [ ] **Step 1: Write the failing test** — in `test/coordinator-client.test.mjs`, extend the `ensureCoordinator spawns the real coordinator…` test (or add a focused test after it):
|
|
55
|
-
|
|
56
|
-
```js
|
|
57
|
-
test("coordinator records its instanceId in the lease identity (issue #239 A4)", async (t) => {
|
|
58
|
-
const root = freshRoot();
|
|
59
|
-
t.after(async () => { await cleanupRoot(root); });
|
|
60
|
-
createView(root, { id: "v1", name: "a4", cwd: root });
|
|
61
|
-
|
|
62
|
-
const ensured = await ensureCoordinator(root, { runnerScript: COORDINATOR_SCRIPT });
|
|
63
|
-
assert.equal(ensured.ok, true);
|
|
64
|
-
if (ensured.pid) track(ensured.pid);
|
|
65
|
-
|
|
66
|
-
const owner = JSON.parse(readFileSync(
|
|
67
|
-
join(P.viewLockPath(root, "_coordinator", "state-coordinator"), "owner.json"), "utf8"));
|
|
68
|
-
assert.equal(typeof owner.identity?.instanceId, "string");
|
|
69
|
-
assert.notEqual(owner.identity.instanceId.length, 0);
|
|
70
|
-
assert.equal(owner.identity.instanceId, ensured.instanceId);
|
|
71
|
-
});
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
- [ ] **Step 2: Run it and see it fail**
|
|
75
|
-
|
|
76
|
-
Run: `node --test test/coordinator-client.test.mjs`
|
|
77
|
-
Expected: FAIL — `owner.identity.instanceId` is `undefined`.
|
|
78
|
-
|
|
79
|
-
- [ ] **Step 3: Implement** — in `runner/state-coordinator.mjs`, add `instanceId` to the identity object:
|
|
80
|
-
|
|
81
|
-
```js
|
|
82
|
-
identity: { pid: process.pid, startToken: captureStartToken(process.pid), instanceId },
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
- [ ] **Step 4: Run the test file**
|
|
86
|
-
|
|
87
|
-
Run: `node --test test/coordinator-client.test.mjs`
|
|
88
|
-
Expected: PASS (the new test and all existing tests).
|
|
89
|
-
|
|
90
|
-
- [ ] **Step 5: Commit**
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
git add runner/state-coordinator.mjs test/coordinator-client.test.mjs
|
|
94
|
-
git commit -m "feat(coordinator): record instanceId in the lease identity (issue #239)"
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
### Task 2: Lease record reader + pure judge
|
|
100
|
-
|
|
101
|
-
**Files:**
|
|
102
|
-
- Modify: `src/core/coordinator-client.mjs` (replace `readCoordinatorLeasePid` at `:119-135`; add `judgeReplacementTarget`)
|
|
103
|
-
- Test: `test/coordinator-client.test.mjs` (new unit tests; import the two functions)
|
|
104
|
-
|
|
105
|
-
**Interfaces:**
|
|
106
|
-
- Produces:
|
|
107
|
-
- `readCoordinatorLeaseRecord(root): { token: string|null, pid: number|null, instanceId: string|null, startToken: string|null } | null`
|
|
108
|
-
- `pid` = `Number(owner.identity.pid ?? owner.pid)`, `null` when not a positive finite number
|
|
109
|
-
- `instanceId` = first non-empty string of `owner.identity.instanceId`, `owner.instanceId`
|
|
110
|
-
- `startToken` = `owner.identity.startToken` when it is a non-empty string, else `null`
|
|
111
|
-
- returns `null` on missing file / bad JSON / non-object
|
|
112
|
-
- `judgeReplacementTarget({ recorded, probedInstanceId, currentStartToken })` → `{ action: "signal" } | { action: "skip", reason }`
|
|
113
|
-
- order: `recorded`/`pid` invalid → `skip("lease_unreadable")`; `recorded.instanceId !== null` → equal = `signal`, unequal = `skip("lease_owner_mismatch")`; else (legacy) `recorded.startToken === null` → `skip("identity_unverified")`; `currentStartToken` not a string (null/undefined/Promise) → `skip("identity_unverified")`; unequal → `skip("pid_reused")`; equal → `signal`
|
|
114
|
-
|
|
115
|
-
- [ ] **Step 1: Write the failing tests** — append to `test/coordinator-client.test.mjs` (update the import line to include both functions):
|
|
116
|
-
|
|
117
|
-
```js
|
|
118
|
-
// --- issue #239: lease record + pure judge (T1/T2) ---
|
|
119
|
-
|
|
120
|
-
function mkRecord(overrides = {}) {
|
|
121
|
-
return { token: "t1", pid: 4242, instanceId: "abc", startToken: null, ...overrides };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
test("judgeReplacementTarget: instanceId equality decides (J1/J2)", () => {
|
|
125
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: mkRecord(), probedInstanceId: "abc" }), { action: "signal" });
|
|
126
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: mkRecord(), probedInstanceId: "other" }),
|
|
127
|
-
{ action: "skip", reason: "lease_owner_mismatch" });
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("judgeReplacementTarget: legacy startToken fallback (J3/J4/J5/J6)", () => {
|
|
131
|
-
const legacy = mkRecord({ instanceId: null, startToken: "777" });
|
|
132
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: legacy, probedInstanceId: "x", currentStartToken: "777" }), { action: "signal" });
|
|
133
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: legacy, probedInstanceId: "x", currentStartToken: "888" }),
|
|
134
|
-
{ action: "skip", reason: "pid_reused" });
|
|
135
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: legacy, probedInstanceId: "x", currentStartToken: null }),
|
|
136
|
-
{ action: "skip", reason: "identity_unverified" });
|
|
137
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: legacy, probedInstanceId: "x", currentStartToken: Promise.resolve("777") }),
|
|
138
|
-
{ action: "skip", reason: "identity_unverified" }, "a missed await must not read as pid_reused");
|
|
139
|
-
const noToken = mkRecord({ instanceId: null, startToken: null });
|
|
140
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: noToken, probedInstanceId: "x" }),
|
|
141
|
-
{ action: "skip", reason: "identity_unverified" });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("judgeReplacementTarget: malformed records are never signalled (J7)", () => {
|
|
145
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: null, probedInstanceId: "x" }),
|
|
146
|
-
{ action: "skip", reason: "lease_unreadable" });
|
|
147
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: mkRecord({ pid: 0 }), probedInstanceId: "abc" }),
|
|
148
|
-
{ action: "skip", reason: "lease_unreadable" });
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("judgeReplacementTarget: empty-string fields count as missing", () => {
|
|
152
|
-
assert.deepEqual(judgeReplacementTarget({ recorded: mkRecord({ instanceId: "" }), probedInstanceId: "abc" }),
|
|
153
|
-
{ action: "skip", reason: "identity_unverified" });
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("readCoordinatorLeaseRecord parses identity / legacy / top-level instanceId (A2)", () => {
|
|
157
|
-
const root = freshRoot();
|
|
158
|
-
const lockPath = P.viewLockPath(root, "_coordinator", "state-coordinator");
|
|
159
|
-
mkdirSync(lockPath, { recursive: true });
|
|
160
|
-
const ownerPath = join(lockPath, "owner.json");
|
|
161
|
-
|
|
162
|
-
assert.equal(readCoordinatorLeaseRecord(root), null, "missing owner.json");
|
|
163
|
-
writeFileSync(ownerPath, "{not json", "utf8");
|
|
164
|
-
assert.equal(readCoordinatorLeaseRecord(root), null, "corrupt json");
|
|
165
|
-
|
|
166
|
-
writeFileSync(ownerPath, JSON.stringify({ token: "a", pid: 7, identity: { pid: 7, startToken: "9", instanceId: "i" } }), "utf8");
|
|
167
|
-
assert.deepEqual(readCoordinatorLeaseRecord(root), { token: "a", pid: 7, instanceId: "i", startToken: "9" });
|
|
168
|
-
|
|
169
|
-
writeFileSync(ownerPath, JSON.stringify({ token: "b", pid: 8, identity: { pid: 8, startToken: null } }), "utf8");
|
|
170
|
-
assert.deepEqual(readCoordinatorLeaseRecord(root), { token: "b", pid: 8, instanceId: null, startToken: null });
|
|
171
|
-
|
|
172
|
-
writeFileSync(ownerPath, JSON.stringify({ token: "c", pid: 9, instanceId: "top" }), "utf8");
|
|
173
|
-
assert.equal(readCoordinatorLeaseRecord(root).instanceId, "top", "top-level compatibility");
|
|
174
|
-
|
|
175
|
-
writeFileSync(ownerPath, JSON.stringify({ token: "d", identity: null }), "utf8");
|
|
176
|
-
assert.equal(readCoordinatorLeaseRecord(root).pid, null);
|
|
177
|
-
rmSync(root, { recursive: true, force: true });
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
- [ ] **Step 2: Run and see them fail**
|
|
182
|
-
|
|
183
|
-
Run: `node --test test/coordinator-client.test.mjs`
|
|
184
|
-
Expected: FAIL — `judgeReplacementTarget`/`readCoordinatorLeaseRecord` are not exported.
|
|
185
|
-
|
|
186
|
-
- [ ] **Step 3: Implement** — in `src/core/coordinator-client.mjs`, replace `readCoordinatorLeasePid` with the exported `readCoordinatorLeaseRecord` (same read, richer shape) and add the exported pure `judgeReplacementTarget` exactly per the Interfaces block. Delete the now-unused `readCoordinatorLeasePid` and update `replaceStaleCoordinator`'s call site to the new reader (temporary: it may still use `.pid` for now; Task 4 rewrites the function).
|
|
187
|
-
|
|
188
|
-
- [ ] **Step 4: Run the test file**
|
|
189
|
-
|
|
190
|
-
Run: `node --test test/coordinator-client.test.mjs`
|
|
191
|
-
Expected: PASS. `npm run typecheck` also passes (JSDoc types are checked).
|
|
192
|
-
|
|
193
|
-
- [ ] **Step 5: Commit**
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
git add src/core/coordinator-client.mjs test/coordinator-client.test.mjs
|
|
197
|
-
git commit -m "feat(coordinator): lease record reader and pure replacement judge (issue #239)"
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
### Task 3: Deduped skip diagnostic
|
|
203
|
-
|
|
204
|
-
**Files:**
|
|
205
|
-
- Modify: `src/core/coordinator-client.mjs` (module-level Set + `reportReplacementSkip`)
|
|
206
|
-
- Test: `test/coordinator-client.test.mjs` (unit test the diagnostic writer through the exported replacement path — see Task 4; here test the dedup contract by calling the exported helper if it is exported, otherwise defer the assertion to Task 4's A8 test)
|
|
207
|
-
|
|
208
|
-
**Interfaces:**
|
|
209
|
-
- Produces: internal `reportReplacementSkip(root, { reason, recordedInstanceId, probedInstanceId })`
|
|
210
|
-
- module-level `Set` key `${root}|${reason}|${recordedInstanceId ?? "null"}|${probedInstanceId}`; a hit returns immediately without touching disk
|
|
211
|
-
- on miss: `if (!existsSync(root)) return;` then `appendDiagnosticOnce(root, "_coordinator", patch, { match: ["reason", "recordedInstanceId", "probedInstanceId"] })`; everything in `try/catch`
|
|
212
|
-
- patch: `{ source: "coordinator-client", level: "warn", code: "coordinator_replacement_skipped", message: \`coordinator replacement skipped: ${reason}\`, details: { reason, recordedInstanceId: recordedInstanceId ?? null, probedInstanceId } }`
|
|
213
|
-
|
|
214
|
-
- [ ] **Step 1:** Implement `reportReplacementSkip` and call it from the replacement function's every skip return (wired fully in Task 4). No new test in this task — its observable contract (`≤1` event for repeated calls, no root resurrection) is asserted by A8/A3k in Task 4, because it is not exported (keeps the public surface small).
|
|
215
|
-
|
|
216
|
-
- [ ] **Step 2: Commit**
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
git add src/core/coordinator-client.mjs
|
|
220
|
-
git commit -m "feat(coordinator): deduped replacement-skip diagnostic (issue #239)"
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
### Task 4: `replaceStaleCoordinator` orchestration
|
|
226
|
-
|
|
227
|
-
**Files:**
|
|
228
|
-
- Modify: `src/core/coordinator-client.mjs` (`replaceStaleCoordinator` body; export it)
|
|
229
|
-
- Test: `test/coordinator-client.test.mjs` (T3 unit tests A3a-A3l + A8 dedup)
|
|
230
|
-
|
|
231
|
-
**Interfaces:**
|
|
232
|
-
- `replaceStaleCoordinator(root, socketPath, expectedInstanceId, opts = {}) → Promise<ReplaceOutcome>`
|
|
233
|
-
- `ReplaceOutcome = { replaced: true } | { replaced: false, reason }`
|
|
234
|
-
- `SkipReason = "lease_unreadable"|"lease_owner_mismatch"|"pid_reused"|"identity_unverified"|"lease_changed"|"instance_unresponsive"|"replacement_raced"|"replace_timeout"`
|
|
235
|
-
- opts (all optional, defaults in spec §4): `probe` (default `probeOnce`, signature `(socketPath, connect, timeoutMs)`), `connect`, `probeTimeoutMs`, `readLease`, `kill` (default `process.kill`), `isAlive`, `readStartToken` (default `readProcessStartToken`, async), `clock`, `sleep`, `replaceTimeoutMs`, `reportSkip`
|
|
236
|
-
- flow (spec D3/D4, exact order): read lease → legacy live-token read only when `instanceId` is null and `startToken` is a string → judge → skip or continue → **re-probe**: pong & same instanceId → continue; pong & different instanceId → `protocolVersion >= COORDINATOR_PROTOCOL_VERSION` ? `{replaced:true}` : `skip("replacement_raced")`; `null` → re-read lease (null/invalid → `skip("lease_unreadable")`; `isAlive(pid)` → `skip("instance_unresponsive")`; else `{replaced:true}`) → **re-read lease**: token/instanceId changed → `skip("lease_changed")` → `kill(pid, "SIGTERM")` in try/catch (`ESRCH` → `{replaced:true}`) → poll `isAlive` every 50ms until `replaceTimeoutMs` (`{replaced:true}` on exit, else `skip("replace_timeout")`)
|
|
237
|
-
- every `skip` return calls `reportSkip` inside its own try/catch
|
|
238
|
-
|
|
239
|
-
- [ ] **Step 1: Write the failing unit tests** — append to `test/coordinator-client.test.mjs`:
|
|
240
|
-
|
|
241
|
-
```js
|
|
242
|
-
// --- issue #239: replacement orchestration (T3) ---
|
|
243
|
-
|
|
244
|
-
/** Build injected dependencies for replaceStaleCoordinator; `calls` records kills. */
|
|
245
|
-
function mkReplacementDeps(overrides = {}) {
|
|
246
|
-
const calls = { kill: [], probe: 0, report: [] };
|
|
247
|
-
const deps = {
|
|
248
|
-
readLease: () => mkRecord(),
|
|
249
|
-
probe: async () => { calls.probe += 1; return { instanceId: "abc", protocolVersion: COORDINATOR_PROTOCOL_VERSION }; },
|
|
250
|
-
kill: (pid, sig) => { calls.kill.push([pid, sig]); },
|
|
251
|
-
isAlive: () => false,
|
|
252
|
-
readStartToken: async () => null,
|
|
253
|
-
clock: () => 0,
|
|
254
|
-
sleep: async () => {},
|
|
255
|
-
replaceTimeoutMs: 3000,
|
|
256
|
-
reportSkip: (details) => { calls.report.push(details); },
|
|
257
|
-
...overrides,
|
|
258
|
-
};
|
|
259
|
-
return { deps, calls };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
test("A3a: matching instanceId signals once and reports replaced", async () => {
|
|
263
|
-
const { deps, calls } = mkReplacementDeps();
|
|
264
|
-
const out = await replaceStaleCoordinator("/root", "/sock", "abc", deps);
|
|
265
|
-
assert.deepEqual(out, { replaced: true });
|
|
266
|
-
assert.deepEqual(calls.kill, [[4242, "SIGTERM"]]);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("A3b: mismatched instanceId never signals", async () => {
|
|
270
|
-
const { deps, calls } = mkReplacementDeps({ readLease: () => mkRecord({ instanceId: "other" }) });
|
|
271
|
-
const out = await replaceStaleCoordinator("/root", "/sock", "abc", deps);
|
|
272
|
-
assert.deepEqual(out, { replaced: false, reason: "lease_owner_mismatch" });
|
|
273
|
-
assert.deepEqual(calls.kill, []);
|
|
274
|
-
assert.equal(calls.report.at(-1).reason, "lease_owner_mismatch");
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
test("A3c/A3d: re-probe null with dead pid replaces; with live pid skips", async () => {
|
|
278
|
-
const dead = mkReplacementDeps({ probe: async () => null, isAlive: () => false });
|
|
279
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", dead.deps), { replaced: true });
|
|
280
|
-
assert.deepEqual(dead.calls.kill, []);
|
|
281
|
-
|
|
282
|
-
const live = mkReplacementDeps({ probe: async () => null, isAlive: () => true });
|
|
283
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", live.deps),
|
|
284
|
-
{ replaced: false, reason: "instance_unresponsive" });
|
|
285
|
-
assert.deepEqual(live.calls.kill, []);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("A3e: another old-protocol instance taking over skips; a healthy one replaces", async () => {
|
|
289
|
-
const stale = mkReplacementDeps({ probe: async () => ({ instanceId: "newer-old", protocolVersion: 1 }) });
|
|
290
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", stale.deps),
|
|
291
|
-
{ replaced: false, reason: "replacement_raced" });
|
|
292
|
-
|
|
293
|
-
const healthy = mkReplacementDeps({ probe: async () => ({ instanceId: "new", protocolVersion: COORDINATOR_PROTOCOL_VERSION }) });
|
|
294
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", healthy.deps), { replaced: true });
|
|
295
|
-
assert.deepEqual(healthy.calls.kill, []);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test("A3f: lease changing between judge and signal skips", async () => {
|
|
299
|
-
let reads = 0;
|
|
300
|
-
const { deps, calls } = mkReplacementDeps({ readLease: () => (++reads === 1 ? mkRecord() : mkRecord({ token: "t2" })) });
|
|
301
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", deps),
|
|
302
|
-
{ replaced: false, reason: "lease_changed" });
|
|
303
|
-
assert.deepEqual(calls.kill, []);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test("A3g: a process that never exits times out after the kill", async () => {
|
|
307
|
-
let now = 0;
|
|
308
|
-
const { deps, calls } = mkReplacementDeps({ isAlive: () => true, clock: () => now, sleep: async () => { now += 100; } });
|
|
309
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", deps),
|
|
310
|
-
{ replaced: false, reason: "replace_timeout" });
|
|
311
|
-
assert.equal(calls.kill.length, 1);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test("A3h: a throwing reportSkip does not throw or change the outcome", async () => {
|
|
315
|
-
const { deps } = mkReplacementDeps({
|
|
316
|
-
readLease: () => mkRecord({ instanceId: "other" }),
|
|
317
|
-
reportSkip: () => { throw new Error("disk full"); },
|
|
318
|
-
});
|
|
319
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", deps),
|
|
320
|
-
{ replaced: false, reason: "lease_owner_mismatch" });
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test("A3j: unreadable lease on the null-probe re-read skips", async () => {
|
|
324
|
-
let reads = 0;
|
|
325
|
-
const { deps, calls } = mkReplacementDeps({
|
|
326
|
-
probe: async () => null,
|
|
327
|
-
readLease: () => (++reads === 1 ? mkRecord() : null),
|
|
328
|
-
});
|
|
329
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", deps),
|
|
330
|
-
{ replaced: false, reason: "lease_unreadable" });
|
|
331
|
-
assert.deepEqual(calls.kill, []);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test("A3l: kill errors are contained (ESRCH replaces, EPERM times out)", async () => {
|
|
335
|
-
const esrch = Object.assign(new Error("gone"), { code: "ESRCH" });
|
|
336
|
-
const a = mkReplacementDeps({ kill: () => { throw esrch; } });
|
|
337
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", a.deps), { replaced: true });
|
|
338
|
-
|
|
339
|
-
const eperm = Object.assign(new Error("denied"), { code: "EPERM" });
|
|
340
|
-
const b = mkReplacementDeps({ kill: () => { throw eperm; }, isAlive: () => true, clock: () => 0, sleep: async () => {} });
|
|
341
|
-
assert.deepEqual(await replaceStaleCoordinator("/root", "/sock", "abc", b.deps),
|
|
342
|
-
{ replaced: false, reason: "replace_timeout" });
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test("A3k: a removed root is not resurrected by the skip diagnostic", async () => {
|
|
346
|
-
const root = join(freshRoot(), "removed-root"); // never created on disk
|
|
347
|
-
const { deps } = mkReplacementDeps({
|
|
348
|
-
readLease: () => mkRecord({ instanceId: "other" }),
|
|
349
|
-
reportSkip: undefined, // default writer -> existsSync guard
|
|
350
|
-
});
|
|
351
|
-
// default reportSkip is not injectable here; instead assert the default writer's guard:
|
|
352
|
-
const out = await replaceStaleCoordinator(root, "/sock", "abc", { ...deps, reportSkip: undefined });
|
|
353
|
-
assert.deepEqual(out, { replaced: false, reason: "lease_owner_mismatch" });
|
|
354
|
-
assert.equal(existsSync(root), false, "a deleted/missing root must not be recreated");
|
|
355
|
-
});
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
(Imports to extend: `readCoordinatorLeaseRecord`, `judgeReplacementTarget`, `replaceStaleCoordinator`; fs: `existsSync`.)
|
|
359
|
-
|
|
360
|
-
- [ ] **Step 2: Run and see them fail**
|
|
361
|
-
|
|
362
|
-
Run: `node --test test/coordinator-client.test.mjs`
|
|
363
|
-
Expected: FAIL — `replaceStaleCoordinator` not exported / old body lacks the branches.
|
|
364
|
-
|
|
365
|
-
- [ ] **Step 3: Implement** — rewrite `replaceStaleCoordinator` per the Interfaces flow. Keep `REPLACE_TIMEOUT_MS = 3_000` and add `REPLACE_POLL_MS = 50`. The old `existsSync(socketPath)` wait is deleted.
|
|
366
|
-
|
|
367
|
-
- [ ] **Step 4: Run and confirm green**
|
|
368
|
-
|
|
369
|
-
Run: `node --test test/coordinator-client.test.mjs` → PASS.
|
|
370
|
-
|
|
371
|
-
- [ ] **Step 5: Commit**
|
|
372
|
-
|
|
373
|
-
```bash
|
|
374
|
-
git add src/core/coordinator-client.mjs test/coordinator-client.test.mjs
|
|
375
|
-
git commit -m "feat(coordinator): verify lease identity before signalling (issue #239)"
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
---
|
|
379
|
-
|
|
380
|
-
### Task 5: `ensureCoordinator` wiring + integration fixtures
|
|
381
|
-
|
|
382
|
-
**Files:**
|
|
383
|
-
- Modify: `src/core/coordinator-client.mjs` (`ensureCoordinator` replacement branch + opts pass-through)
|
|
384
|
-
- Modify: `test/coordinator-client.test.mjs` (`STALE_COORDINATOR_CJS` fixture + new scenario fixtures/tests)
|
|
385
|
-
|
|
386
|
-
**Interfaces:**
|
|
387
|
-
- Consumes: Task 4 `replaceStaleCoordinator`, Task 3 `reportReplacementSkip`
|
|
388
|
-
- Produces: `ensureCoordinator` accepts `probe`, `kill`, `isAlive`, `readStartToken`, `clock`, `sleep`, `replaceTimeoutMs`, `reportSkip` and forwards them plus `connect`/`probeTimeoutMs` into `replaceStaleCoordinator`; result shape unchanged.
|
|
389
|
-
- Fixture contract: `spawnStaleV1Coordinator(root, ownerOverrides, pongInstanceId)` — the CJS writes `identity: { pid, startToken: "1", ...ownerOverrides.identity }`; when `ownerOverrides.instanceId` is set it is merged into `identity`; its pong instanceId is parameterized (default `"stale-v1"`).
|
|
390
|
-
|
|
391
|
-
- [ ] **Step 1: Write the failing integration tests** — update the fixture and add:
|
|
392
|
-
|
|
393
|
-
```js
|
|
394
|
-
test("A5: a matching-instanceId stale coordinator is replaced (end-to-end)", async (t) => {
|
|
395
|
-
const root = freshRoot();
|
|
396
|
-
t.after(async () => { await cleanupRoot(root); });
|
|
397
|
-
const stale = await spawnStaleV1Coordinator(root); // fixture now records identity.instanceId = "stale-v1"
|
|
398
|
-
t.after(() => stale.cleanup());
|
|
399
|
-
createView(root, { id: "v1", name: "a5", cwd: root });
|
|
400
|
-
assert.equal((await rawPing(stale.socketPath))?.instanceId, "stale-v1");
|
|
401
|
-
|
|
402
|
-
const ensured = await ensureCoordinator(root, { runnerScript: COORDINATOR_SCRIPT });
|
|
403
|
-
assert.equal(ensured.ok, true);
|
|
404
|
-
assert.notEqual(ensured.instanceId, "stale-v1");
|
|
405
|
-
if (ensured.pid) track(ensured.pid);
|
|
406
|
-
assert.equal(stale.alive(), false);
|
|
407
|
-
assert.equal((await sendStateCommand(root, markCompleted(), { timeoutMs: 5000 })).status, "applied");
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test("A6: a mismatched lease identity never kills the unrelated pid (core)", async (t) => {
|
|
411
|
-
const root = freshRoot();
|
|
412
|
-
const unrelated = spawn(process.execPath, ["-e", "setInterval(()=>{},1000)"], { stdio: "ignore" });
|
|
413
|
-
t.after(() => { try { unrelated.kill("SIGKILL"); } catch {} });
|
|
414
|
-
const stale = await spawnStaleV1Coordinator(root, { instanceId: "not-the-pong-instance", pid: unrelated.pid });
|
|
415
|
-
t.after(() => stale.cleanup());
|
|
416
|
-
const diagnostics = join(P.diagnosticsPath(root, "_coordinator"));
|
|
417
|
-
|
|
418
|
-
const ensured = await ensureCoordinator(root, { runnerScript: COORDINATOR_SCRIPT });
|
|
419
|
-
assert.deepEqual(ensured, { ok: false, error: "coordinator_stale_protocol" });
|
|
420
|
-
assert.equal(isAlive(unrelated.pid), true, "the unrelated process must survive");
|
|
421
|
-
assert.equal(stale.alive(), true);
|
|
422
|
-
const events = readJsonl(diagnostics).filter((e) => e.code === "coordinator_replacement_skipped");
|
|
423
|
-
assert.equal(events.length, 1);
|
|
424
|
-
assert.equal(events[0].details.reason, "lease_owner_mismatch");
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
test("A7: a legacy record without instanceId/token is not signalled", async (t) => {
|
|
428
|
-
// same fixture with identity: { pid, startToken: null }, no instanceId; pong instanceId "stale-v1"
|
|
429
|
-
// expect: stale alive, stale_protocol, reason identity_unverified
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
test("A8: repeated ensures keep exactly one skip diagnostic", async (t) => {
|
|
433
|
-
// mismatched identity lease + 5 sequential ensureCoordinator calls -> 1 event total
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test("A7b: Linux legacy live-token wiring kills the real lease owner", { skip: process.platform !== "linux" }, async (t) => {
|
|
437
|
-
// fixture whose owner.json records the CHILD's real startToken (captureStartToken(child.pid)),
|
|
438
|
-
// no instanceId; pong instanceId arbitrary -> J3 -> child killed, new coordinator takes over
|
|
439
|
-
});
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
(Exact bodies: follow the existing `spawnStaleV1Coordinator` helper shape at `test/coordinator-client.test.mjs:330-357`; for A6/A7/A8 spawn the fixture with `ownerOverrides` so `pid`/`identity`/`instanceId` are explicit. Import `isAlive`, `readJsonl`, `P.diagnosticsPath`, `captureStartToken` as needed.)
|
|
443
|
-
|
|
444
|
-
- [ ] **Step 2: Run and see them fail**
|
|
445
|
-
|
|
446
|
-
Run: `node --test test/coordinator-client.test.mjs`
|
|
447
|
-
Expected: FAIL — the fixture does not parameterize identity yet; `ensureCoordinator` does not pass `first.instanceId`.
|
|
448
|
-
|
|
449
|
-
- [ ] **Step 3: Implement**
|
|
450
|
-
1. Fixture: add arguments to `spawnStaleV1Coordinator(root, ownerOverrides = {}, pongInstanceId = "stale-v1")`; build the `owner.json` object in JS and embed it into `STALE_COORDINATOR_CJS` via `JSON.stringify` (replace the hardcoded write at `:307`), and parameterize the pong instanceId at `:317`.
|
|
451
|
-
2. `ensureCoordinator`: replace the `replaceStaleCoordinator(root, socketPath)` call with the outcome-based call forwarding `first.instanceId` and all injectable opts; `!outcome.replaced` → `{ ok: false, error: "coordinator_stale_protocol" }`.
|
|
452
|
-
|
|
453
|
-
- [ ] **Step 4: Run the file, then the local suite**
|
|
454
|
-
|
|
455
|
-
```bash
|
|
456
|
-
node --test test/coordinator-client.test.mjs
|
|
457
|
-
npm test *> after-239.log
|
|
458
|
-
# compare ✖ names (strip trailing " (123.4ms)") against baseline-failures.txt: after ⊆ baseline
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
- [ ] **Step 5: Commit**
|
|
462
|
-
|
|
463
|
-
```bash
|
|
464
|
-
git add src/core/coordinator-client.mjs test/coordinator-client.test.mjs
|
|
465
|
-
git commit -m "fix(coordinator): only replace a coordinator whose lease identity matches (issue #239)"
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
---
|
|
469
|
-
|
|
470
|
-
### Task 6: U1 verification script + final gates
|
|
471
|
-
|
|
472
|
-
**Files:**
|
|
473
|
-
- Create (outside repo, issue research dir): `~/.github-issue-driven/zhuxixi/pi-agent-board/issue-239/research/verify-u1-legacy-no-kill.mjs`
|
|
474
|
-
- Test: run it; no repo files change in this task (except possible goal-driven touch-ups from the gates)
|
|
475
|
-
|
|
476
|
-
**Interfaces:**
|
|
477
|
-
- CLI: `node verify-u1-legacy-no-kill.mjs --root <isolated-root> [--recover]`
|
|
478
|
-
- imports the implementation checkout by absolute file URL passed via `--checkout <path>` (default = the worktree root detected from `process.cwd()`); uses `--root` for the board root; creates a `createView` row; writes the legacy lease with an unrelated live node child pid; binds a v1 pong on `P.coordinatorEndpointPathFor(process.platform, root)`; calls `ensureCoordinator(root)`; prints `{ outcome, unrelatedAlive, diagnostics }`; `--recover` skips scenario construction and runs one more `ensureCoordinator(root)`.
|
|
479
|
-
|
|
480
|
-
- [ ] **Step 1: Write the script** (per spec §7; reuse the `STALE_COORDINATOR_CJS` shape from the test file).
|
|
481
|
-
- [ ] **Step 2: Run it twice against the worktree checkout**
|
|
482
|
-
|
|
483
|
-
```bash
|
|
484
|
-
node "<issue>/research/verify-u1-legacy-no-kill.mjs" --root "$env:TEMP\ab-u1-239" --checkout "<worktree>"
|
|
485
|
-
node "<issue>/research/verify-u1-legacy-no-kill.mjs" --root "$env:TEMP\ab-u1-239" --checkout "<worktree>"
|
|
486
|
-
```
|
|
487
|
-
Expected: `outcome.error === "coordinator_stale_protocol"`, `unrelatedAlive === true`, one `coordinator_replacement_skipped` with `reason: "identity_unverified"`, second run adds no diagnostic.
|
|
488
|
-
- [ ] **Step 3: Recovery demo** — delete the isolated lease dir, run with `--recover` → `outcome.ok === true`; then stop that coordinator / delete the isolated root.
|
|
489
|
-
- [ ] **Step 4: Final gates** — `npm run typecheck`; `node --test test/coordinator-client.test.mjs test/locks.test.mjs test/state-coordinator.integration.test.mjs`; `npm test` vs baseline (A10); record all results in the PR body.
|
|
490
|
-
- [ ] **Step 5: Commit any final fix; do not commit the script.**
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Self-Review
|
|
495
|
-
|
|
496
|
-
**1. Spec coverage** — A1/A2 (Task 2), A3a-A3l/A8 (Task 4; A8 re-asserted at ensure level in Task 5), A4 (Task 1), A5/A6/A7/A7b/A8 (Task 5), A9/A10/A11 (Task 5 Step 4 + Task 6 Step 4), U1 (Task 6). D1 (Task 1), D2 (Task 2), D3/D4/D5 (Task 4), D6 (Task 2 judge legacy + Task 4 live-token read + A7b). Every spec decision has an owning task.
|
|
497
|
-
|
|
498
|
-
**2. Step scan** — each step names the exact test, command, function signature, or file; no TBD/skip placeholders except the four integration tests whose bodies are pinned by the fixture contract paragraph in Task 5 (they reuse the existing helper verbatim).
|
|
499
|
-
|
|
500
|
-
**3. Type consistency** — `LeaseRecord`, `ReplaceOutcome`, `SkipReason`, `judgeReplacementTarget` args, and `replaceStaleCoordinator` opts names are identical across Tasks 2/4/5; `ensureCoordinator` forwards the same names.
|
|
501
|
-
|
|
502
|
-
**4. Review Focus** — each of the six listed failure modes has a test: unrelated-pid survial (A6/A7/A3b), fresh evidence (A3c/A3d/A3e), never-throws (A3h/A3l), root resurrection (A3k), diagnostic flood (A8), Windows SIGTERM semantics (A3g timeout + wait-for-exit; U1 on win32).
|
|
503
|
-
|
|
504
|
-
**5. Proportion** — plan ≈ 2.5× the spec's length in lines but mostly test code the spec's matrix demanded; no production body is transcribed except the flow order that the spec fixes.
|