@huseynsnmz/pi-board 0.15.3 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -4
- package/CONTRIBUTING.md +7 -0
- package/README.md +53 -419
- package/SECURITY.md +19 -0
- package/docs/how-it-works.md +31 -0
- package/docs/reference.md +276 -0
- package/package.json +5 -11
- package/src/index.ts +1 -1
- package/src/runtime/service.mjs +12 -0
- package/src/ui/dashboard.ts +14 -8
- 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,256 +0,0 @@
|
|
|
1
|
-
# Issue #257 — Legacy pty-runner endpoint cleanup: 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 legacy PTY runner never deletes an endpoint it no longer owns, while still cleaning up its own on a normal shutdown.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Inside `legacyMain` only: record `{dev,ino}` at listen, stop calling `server.close()` (libuv unlinks the bind path on handle close — `nodejs/node#19729`), and gate the explicit unlink on the existing pure `ownsEndpoint`. No new pure functions, no protocol change.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node ESM (`.mjs`), `node:test` + `assert/strict`, existing `pty-runner.integration.test.mjs` fixtures.
|
|
10
|
-
|
|
11
|
-
**Spec:** `docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md` (converged after 2 cross-review rounds; findings trail in `~/.github-issue-driven/zhuxixi/pi-agent-board/issue-257/research/`)
|
|
12
|
-
|
|
13
|
-
## Global Constraints
|
|
14
|
-
|
|
15
|
-
- POSIX-only behaviour; all new assertions guarded by `process.platform === "win32"` → `t.skip("POSIX AF_UNIX only")`.
|
|
16
|
-
- New test titles MUST be exactly `#257 A<n>: <summary>`; A1's selector anchors on `#257 A1:` because `#257 A1` is a prefix of `#257 A1b`.
|
|
17
|
-
- Do not touch `ownedMain` (out of scope) or the legacy boot-time unlink at `:140-142`.
|
|
18
|
-
- Tabs for indentation; English comments; conventional commit subjects with `(issue #257)`.
|
|
19
|
-
- Local Windows has no WSL/Docker: A1/A1b/A2 show as `skip` locally and execute on Linux CI only. `node --test --test-name-pattern="zzz"` exits 0 — always read the summary line, never the exit code alone.
|
|
20
|
-
|
|
21
|
-
## Review Focus
|
|
22
|
-
|
|
23
|
-
- A legacy shutdown after the path was replaced must leave the replacement intact (the bug).
|
|
24
|
-
- A legacy normal shutdown must still remove its own socket (no stale-socket regression).
|
|
25
|
-
- The identity must be captured before `state: "alive"` is published, or `hostReady` is not a barrier.
|
|
26
|
-
- Removing `server.close()` must not change shutdown timing/child handling.
|
|
27
|
-
- win32: no identity is captured; the cleanup degrades to a no-op (named pipes never unlink).
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
### Task 1: legacyMain identity + no-close + gated unlink, with the three integration cases
|
|
32
|
-
|
|
33
|
-
**Files:**
|
|
34
|
-
- Modify: `runner/pty-runner.mjs` (`legacyMain`: identity holder, listen callback at `:409`, `shutdown` at `:503-531`)
|
|
35
|
-
- Test: `test/pty-runner.integration.test.mjs` (append three tests; file already imports `statSync`, `createServer`, `spawn`, `P`, `createView`, `readHost`, `atomicWriteJson`, and has `freshRoot`/`waitFor`/`hostReady`)
|
|
36
|
-
|
|
37
|
-
**Interfaces:**
|
|
38
|
-
- Consumes: `ownsEndpoint(bound, current)` (already imported at `runner/pty-runner.mjs:20`), `P.controlSocketPath(root, viewId)`, `P.hostConfigPath(root, viewId)`.
|
|
39
|
-
- Produces: nothing exported; `boundSocketIdentity` becomes a `legacyMain`-local variable; diagnostic `coordinator_endpoint_lost` is NOT used here (that is the coordinator path).
|
|
40
|
-
|
|
41
|
-
- [ ] **Step 1: Write the three failing tests**
|
|
42
|
-
|
|
43
|
-
Append to `test/pty-runner.integration.test.mjs` (use the exact helper names already in the file):
|
|
44
|
-
|
|
45
|
-
```js
|
|
46
|
-
/** Shared fixture: boot a legacy runner (host-config WITHOUT instanceId). */
|
|
47
|
-
async function bootLegacyRunner(t, viewId = "v1") {
|
|
48
|
-
const root = freshRoot();
|
|
49
|
-
const meta = createView(root, { id: viewId, name: "legacy-endpoint", cwd: process.cwd() });
|
|
50
|
-
const configPath = P.hostConfigPath(root, viewId);
|
|
51
|
-
atomicWriteJson(configPath, {
|
|
52
|
-
root,
|
|
53
|
-
viewId,
|
|
54
|
-
sessionFile: meta.sessionFile,
|
|
55
|
-
cwd: process.cwd(),
|
|
56
|
-
initialPrompt: null,
|
|
57
|
-
piCommand: process.execPath,
|
|
58
|
-
piArgsPrefix: [resolve("test-support/fake-pty-pi.mjs")],
|
|
59
|
-
model: null,
|
|
60
|
-
tools: null,
|
|
61
|
-
env: { AGENT_BOARD_ALLOW_PIPE_FALLBACK: "1" },
|
|
62
|
-
cols: 80,
|
|
63
|
-
rows: 24,
|
|
64
|
-
});
|
|
65
|
-
const runner = spawn(process.execPath, [resolve("runner/pty-runner.mjs"), configPath], {
|
|
66
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
67
|
-
});
|
|
68
|
-
runner.stdout.resume();
|
|
69
|
-
runner.stderr.resume();
|
|
70
|
-
await waitFor(() => hostReady(root, viewId));
|
|
71
|
-
return { root, runner, socketPath: P.controlSocketPath(root, viewId) };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function waitExit(child, timeoutMs = 5000) {
|
|
75
|
-
return new Promise((done) => {
|
|
76
|
-
if (child.exitCode !== null) return done(child.exitCode);
|
|
77
|
-
const timer = setTimeout(() => done(null), timeoutMs);
|
|
78
|
-
child.once("exit", (code) => { clearTimeout(timer); done(code); });
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
test("#257 A1: a legacy runner cleans up its own endpoint on SIGTERM", async (t) => {
|
|
83
|
-
if (process.platform === "win32") return t.skip("POSIX AF_UNIX only");
|
|
84
|
-
const { root, runner, socketPath } = await bootLegacyRunner(t);
|
|
85
|
-
try {
|
|
86
|
-
assert.ok(existsSync(socketPath), "precondition: endpoint exists before SIGTERM");
|
|
87
|
-
runner.kill("SIGTERM");
|
|
88
|
-
const code = await waitExit(runner);
|
|
89
|
-
assert.equal(code, 0, "clean SIGTERM exit");
|
|
90
|
-
assert.equal(existsSync(socketPath), false, "normal shutdown removes its own endpoint");
|
|
91
|
-
} finally {
|
|
92
|
-
if (runner.exitCode === null) runner.kill("SIGKILL");
|
|
93
|
-
rmSync(root, { recursive: true, force: true });
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("#257 A1b: a legacy runner exits cleanly when the endpoint is already gone", async (t) => {
|
|
98
|
-
if (process.platform === "win32") return t.skip("POSIX AF_UNIX only");
|
|
99
|
-
const { root, runner, socketPath } = await bootLegacyRunner(t);
|
|
100
|
-
try {
|
|
101
|
-
assert.ok(existsSync(socketPath), "precondition: endpoint exists");
|
|
102
|
-
unlinkSync(socketPath);
|
|
103
|
-
runner.kill("SIGTERM");
|
|
104
|
-
const code = await waitExit(runner);
|
|
105
|
-
assert.equal(code, 0, "ENOENT at cleanup must not crash the shutdown");
|
|
106
|
-
} finally {
|
|
107
|
-
if (runner.exitCode === null) runner.kill("SIGKILL");
|
|
108
|
-
rmSync(root, { recursive: true, force: true });
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("#257 A2: a legacy runner does not unlink a replaced endpoint", async (t) => {
|
|
113
|
-
if (process.platform === "win32") return t.skip("POSIX AF_UNIX only");
|
|
114
|
-
const { root, runner, socketPath } = await bootLegacyRunner(t);
|
|
115
|
-
const standinPath = join(root, `.standin-${process.pid}.sock`);
|
|
116
|
-
let standin = null;
|
|
117
|
-
try {
|
|
118
|
-
assert.ok(existsSync(socketPath), "precondition: endpoint exists");
|
|
119
|
-
const before = statSync(socketPath).ino;
|
|
120
|
-
standin = createServer(() => {});
|
|
121
|
-
await new Promise((ok, fail) => {
|
|
122
|
-
standin.once("error", fail);
|
|
123
|
-
standin.listen(standinPath, ok);
|
|
124
|
-
});
|
|
125
|
-
renameSync(standinPath, socketPath); // atomic, same filesystem
|
|
126
|
-
assert.notEqual(statSync(socketPath).ino, before, "stand-in really replaced the inode");
|
|
127
|
-
runner.kill("SIGTERM");
|
|
128
|
-
const code = await waitExit(runner);
|
|
129
|
-
assert.equal(code, 0, "clean SIGTERM exit");
|
|
130
|
-
assert.ok(existsSync(socketPath), "a legacy runner must not unlink an endpoint it no longer owns");
|
|
131
|
-
} finally {
|
|
132
|
-
if (runner.exitCode === null) runner.kill("SIGKILL");
|
|
133
|
-
if (standin) await new Promise((done) => standin.close(done));
|
|
134
|
-
rmSync(root, { recursive: true, force: true });
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
New imports needed at the top of the file: `unlinkSync`, `renameSync` (add to the `node:fs` import at `:2`).
|
|
140
|
-
|
|
141
|
-
- [ ] **Step 2: Run them (local: expect skip; Linux CI: expect A2 red)**
|
|
142
|
-
|
|
143
|
-
Run: `node --test --test-name-pattern="#257 A" test/pty-runner.integration.test.mjs`
|
|
144
|
-
Local Windows: `tests 3 / skipped 3` (documented channel limit). Linux: A1/A1b pass, **A2 fails**
|
|
145
|
-
(`a legacy runner must not unlink an endpoint it no longer owns`) because the old shutdown's
|
|
146
|
-
`server.close()` and unconditional unlink both delete the stand-in. Record the observed
|
|
147
|
-
summary in the ledger either way.
|
|
148
|
-
|
|
149
|
-
- [ ] **Step 3: Implement the three changes in `legacyMain`**
|
|
150
|
-
|
|
151
|
-
In `runner/pty-runner.mjs`, inside `legacyMain` (after `let shutdownExitCode = null;`):
|
|
152
|
-
|
|
153
|
-
```js
|
|
154
|
-
/** {dev,ino} recorded at bind time; cleanup unlinks only this exact inode. */
|
|
155
|
-
let boundSocketIdentity = null;
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Replace the `server.listen(...)` call at `:409` with:
|
|
159
|
-
|
|
160
|
-
```js
|
|
161
|
-
server.listen(socketPath, () => {
|
|
162
|
-
// Capture the identity BEFORE publishing readiness: "host ready" must imply
|
|
163
|
-
// "identity recorded", or a shutdown could run with a stale/null identity.
|
|
164
|
-
if (process.platform !== "win32") {
|
|
165
|
-
try {
|
|
166
|
-
const st = statSync(socketPath);
|
|
167
|
-
boundSocketIdentity = { dev: st.dev, ino: st.ino };
|
|
168
|
-
} catch { /* without the identity, cleanup degrades to a no-op */ }
|
|
169
|
-
}
|
|
170
|
-
update({ socketPath, state: "alive" });
|
|
171
|
-
});
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
In `shutdown()`, replace:
|
|
175
|
-
|
|
176
|
-
```js
|
|
177
|
-
try { server?.close(); } catch {}
|
|
178
|
-
try { if (existsSync(socketPath)) unlinkSync(socketPath); } catch {}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
with:
|
|
182
|
-
|
|
183
|
-
```js
|
|
184
|
-
// Do NOT call server.close() (issue #257, nodejs/node#19729): libuv unlinks the
|
|
185
|
-
// unix-socket bind path when the listening handle closes, so a shutdown after a
|
|
186
|
-
// successor bound would delete the successor's endpoint. There is no close here,
|
|
187
|
-
// so the implicit unlink never runs. Cleanup is explicit and identity-gated below
|
|
188
|
-
// (the legacy path has no lease, so dev+ino is its only ownership proof).
|
|
189
|
-
if (process.platform !== "win32" && boundSocketIdentity) {
|
|
190
|
-
try {
|
|
191
|
-
const st = statSync(socketPath);
|
|
192
|
-
if (ownsEndpoint(boundSocketIdentity, { dev: st.dev, ino: st.ino })) {
|
|
193
|
-
try { unlinkSync(socketPath); } catch { /* best effort */ }
|
|
194
|
-
}
|
|
195
|
-
} catch { /* path gone — nothing to clean */ }
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
- [ ] **Step 4: Run the cases again**
|
|
200
|
-
|
|
201
|
-
Run: `node --test --test-name-pattern="#257 A" test/pty-runner.integration.test.mjs`
|
|
202
|
-
Local Windows: `tests 3 / skipped 3` and exit 0 — read the summary, confirm 3 skipped (not 0 tests).
|
|
203
|
-
Linux CI: `tests 3 / pass 3`.
|
|
204
|
-
|
|
205
|
-
- [ ] **Step 5: Run the existing pty-runner suite for regressions**
|
|
206
|
-
|
|
207
|
-
Run: `node --test test/pty-runner.integration.test.mjs`
|
|
208
|
-
Local expectation: everything that ran before still passes (the legacy/owned cases that are
|
|
209
|
-
platform-independent). Linux CI: full pass. Any new failure must be understood before commit.
|
|
210
|
-
|
|
211
|
-
- [ ] **Step 6: Commit**
|
|
212
|
-
|
|
213
|
-
```bash
|
|
214
|
-
git add runner/pty-runner.mjs test/pty-runner.integration.test.mjs
|
|
215
|
-
git commit -m "fix(runner): stop legacy pty-runner from unlinking a replaced endpoint (issue #257)"
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
### Task 2: Full verification and local code review
|
|
221
|
-
|
|
222
|
-
**Files:** none (verification + review); modify only if a gate fails.
|
|
223
|
-
|
|
224
|
-
**Interfaces:** consumes Task 1.
|
|
225
|
-
|
|
226
|
-
- [ ] **Step 1: Typecheck**
|
|
227
|
-
|
|
228
|
-
Run: `npm run typecheck` — exit 0.
|
|
229
|
-
|
|
230
|
-
- [ ] **Step 2: Targeted suites**
|
|
231
|
-
|
|
232
|
-
Run: `node --test test/pty-runner.integration.test.mjs test/host-coordination.test.mjs test/locks.test.mjs`
|
|
233
|
-
Local Windows: pass (POSIX cases skip). Linux CI: full pass.
|
|
234
|
-
|
|
235
|
-
- [ ] **Step 3: Pre-PR gate**
|
|
236
|
-
|
|
237
|
-
Run: `npm run test:coverage` (Linux CI authoritative; local Windows baseline is not the gate for
|
|
238
|
-
POSIX cases), then `npm run test:perf && npm run pack:dry`, then `npm run verify`. Record actual
|
|
239
|
-
results; a local failure caused by the known Windows platform gap must be named explicitly.
|
|
240
|
-
|
|
241
|
-
- [ ] **Step 4: Fresh-context local code review**
|
|
242
|
-
|
|
243
|
-
Dispatch a reviewer over `git diff main...HEAD` with the spec + this plan + the Review Focus
|
|
244
|
-
list; disposition findings (Critical/Important → one fix pass with a failing test first; Minor →
|
|
245
|
-
ledger). Commit only if files change.
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
## Task ↔ acceptance traceability
|
|
250
|
-
|
|
251
|
-
| Spec acceptance ID | Task |
|
|
252
|
-
|---|---|
|
|
253
|
-
| A1, A1b, A2 | Task 1 (Steps 1-6) |
|
|
254
|
-
| A3 | Task 1 Step 5 + Task 2 Step 2 |
|
|
255
|
-
| A4 | Task 2 Step 1 |
|
|
256
|
-
| A5, A6 | Task 2 Step 3 (Linux CI authoritative) |
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
# cwd Picker Full-History Search — 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 launch-dialog cwd picker search the full ranked history instead of the top 8 entries (issue #262).
|
|
6
|
-
|
|
7
|
-
**Architecture:** Remove the pre-filter truncation at the dashboard wiring layer and make `rankedCwdCandidates` uncapped by default; the picker's pure state machine and the 6-row render window stay unchanged. A TS probe drives the real `handleInput()` path so the truncation regression is pinned at the layer that owned the bug.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node ESM `.mjs` core + TypeScript TUI (`src/ui/dashboard.ts`), `node:test` + `assert/strict`, Node `--experimental-transform-types` probes.
|
|
10
|
-
|
|
11
|
-
## Global Constraints
|
|
12
|
-
|
|
13
|
-
- `rankedCwdCandidates(root, limit = Number.POSITIVE_INFINITY)`: default returns every ranked entry; an explicit numeric `limit` still caps and still appends the home fallback afterwards (existing tests pass `limit = 2`, keep them green).
|
|
14
|
-
- `src/ui/dashboard.ts` `openLaunchPicker` loads the pool with `existingCwdCandidates(rankedCwdCandidates(this.deps.root))` — no `, 8` and no other hard-coded cap.
|
|
15
|
-
- Do not change `filterCwdCandidates`, `nextCwdPickerState`, `listDirectorySuggestions`, `browseTabCompletion`, `existingCwdCandidates`, the 6-row render window, or the ranking rule (count desc → lastUsed desc).
|
|
16
|
-
- Non-goals: symlink directory enumeration in browse mode, fuzzy matching, manual favorites, `cwd-stats.json` format changes.
|
|
17
|
-
- Tabs for indentation; English comments and commit subjects; conventional commit types.
|
|
18
|
-
- Tests use `node:test` + `assert/strict`; acceptance commands are `node --test <file>` and `npm run verify` (typecheck + perf gate + tests + coverage + pack:dry).
|
|
19
|
-
- Work from: `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-262-cwd-favorites-full-pool`
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
### Task 1: Uncapped ranked candidates (core)
|
|
24
|
-
|
|
25
|
-
**Files:**
|
|
26
|
-
- Modify: `src/core/cwd-stats.mjs:92-108`
|
|
27
|
-
- Test: `test/cwd-stats.test.mjs` (append after the existing `rankedCwdCandidates appends home with its real count when sliced out by limit` test)
|
|
28
|
-
|
|
29
|
-
**Interfaces:**
|
|
30
|
-
- Consumes: `readCwdStats(root)` (already returns all entries), `os.homedir()`.
|
|
31
|
-
- Produces: `rankedCwdCandidates(root, limit = Number.POSITIVE_INFINITY) -> Array<{path: string, count: number}>` — uncapped by default.
|
|
32
|
-
|
|
33
|
-
- [ ] **Step 1: Write the failing test**
|
|
34
|
-
|
|
35
|
-
Append to `test/cwd-stats.test.mjs`:
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
test("rankedCwdCandidates defaults to the full history with no 8-entry cap (issue #262)", () => {
|
|
39
|
-
const root = freshRoot();
|
|
40
|
-
try {
|
|
41
|
-
const dirs = Array.from({ length: 10 }, (_, i) => `/dir-${String(i + 1).padStart(2, "0")}`);
|
|
42
|
-
writeViews(root, dirs.map((dir, i) => [`v${i}`, dir, 1000 - i]));
|
|
43
|
-
seedCwdStatsFromViews(root);
|
|
44
|
-
const ranked = rankedCwdCandidates(root);
|
|
45
|
-
assert.deepEqual(
|
|
46
|
-
ranked.slice(0, 10),
|
|
47
|
-
dirs.map((dir) => ({ path: dir, count: 1 })),
|
|
48
|
-
);
|
|
49
|
-
assert.equal(ranked.length, 11);
|
|
50
|
-
assert.deepEqual(ranked[10], { path: os.homedir(), count: 0 });
|
|
51
|
-
} finally {
|
|
52
|
-
rmSync(root, { recursive: true, force: true });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
- [ ] **Step 2: Run the test to verify it fails**
|
|
58
|
-
|
|
59
|
-
Run: `node --test test/cwd-stats.test.mjs`
|
|
60
|
-
Expected: the new test FAILS — `ranked.slice(0, 10)` has only 8 recorded dirs, then the home fallback at index 8 (`ranked.length` is 9, not 11).
|
|
61
|
-
|
|
62
|
-
- [ ] **Step 3: Change the default limit**
|
|
63
|
-
|
|
64
|
-
In `src/core/cwd-stats.mjs`, replace the function header and JSDoc:
|
|
65
|
-
|
|
66
|
-
```js
|
|
67
|
-
/**
|
|
68
|
-
* Ranked candidates: count desc, then lastUsed desc; home appended at the
|
|
69
|
-
* end when absent so the user's home dir is always one keystroke away.
|
|
70
|
-
* `limit` is an optional cap for callers that explicitly want one; the
|
|
71
|
-
* default returns the whole history so the launch picker can search it.
|
|
72
|
-
* @param {string} root @param {number=} limit
|
|
73
|
-
* @returns {Array<{path: string, count: number}>}
|
|
74
|
-
*/
|
|
75
|
-
export function rankedCwdCandidates(root, limit = Number.POSITIVE_INFINITY) {
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Leave the body unchanged: `rows.slice(0, Math.max(1, limit))` already treats `Infinity` as "no cap".
|
|
79
|
-
|
|
80
|
-
- [ ] **Step 4: Run the test to verify it passes**
|
|
81
|
-
|
|
82
|
-
Run: `node --test test/cwd-stats.test.mjs`
|
|
83
|
-
Expected: PASS — all `cwd-stats` tests green, including the existing `limit = 2` cases.
|
|
84
|
-
|
|
85
|
-
- [ ] **Step 5: Commit**
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
git add src/core/cwd-stats.mjs test/cwd-stats.test.mjs
|
|
89
|
-
git commit -m "fix(cwd): stop capping ranked cwd candidates at 8 (issue #262)"
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
### Task 2: Dashboard loads the full pool + picker regression probe
|
|
95
|
-
|
|
96
|
-
**Files:**
|
|
97
|
-
- Modify: `src/ui/dashboard.ts:755`
|
|
98
|
-
- Create: `test-support/cwd-favorites-pool-probe.ts`
|
|
99
|
-
- Create: `test/dashboard-cwd-favorites-pool.test.mjs`
|
|
100
|
-
- Test: `test/launch-options.test.mjs` (append)
|
|
101
|
-
|
|
102
|
-
**Interfaces:**
|
|
103
|
-
- Consumes: `rankedCwdCandidates(root)` (uncapped, Task 1), `existingCwdCandidates`, `nextCwdPickerState`, `DashboardComponent.openLaunchPicker`.
|
|
104
|
-
- Produces: no new production API; the dashboard now stores the full existing ranked history in `launch.cwdRanked`.
|
|
105
|
-
|
|
106
|
-
- [ ] **Step 1: Write the failing integration probe**
|
|
107
|
-
|
|
108
|
-
Create `test-support/cwd-favorites-pool-probe.ts`:
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
// cwd favorites-pool probe (issue #262): seed more favorites than the legacy
|
|
112
|
-
// 8-entry cap, then type a fragment of the lowest-ranked entry through the
|
|
113
|
-
// public handleInput() path and report the picker state as JSON.
|
|
114
|
-
// Run via `node --experimental-transform-types` (dashboard.ts uses TS
|
|
115
|
-
// parameter properties). Not typechecked (tsconfig excludes test-support).
|
|
116
|
-
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
117
|
-
import { tmpdir } from "node:os";
|
|
118
|
-
import { join } from "node:path";
|
|
119
|
-
import { createService } from "../src/runtime/service.mjs";
|
|
120
|
-
import { DashboardComponent } from "../src/ui/dashboard.ts";
|
|
121
|
-
|
|
122
|
-
const root = mkdtempSync(join(tmpdir(), "agentview-cwd-pool-"));
|
|
123
|
-
const entries: Record<string, { count: number; lastUsed: number }> = {};
|
|
124
|
-
for (let i = 1; i <= 9; i++) {
|
|
125
|
-
const dir = join(root, `rank-${String(i).padStart(2, "0")}`);
|
|
126
|
-
mkdirSync(dir, { recursive: true });
|
|
127
|
-
entries[dir] = { count: 100 - i, lastUsed: 1000 - i };
|
|
128
|
-
}
|
|
129
|
-
const target = join(root, "work", "proj", "stability", "alert-automation");
|
|
130
|
-
mkdirSync(target, { recursive: true });
|
|
131
|
-
entries[target] = { count: 1, lastUsed: 1 };
|
|
132
|
-
writeFileSync(join(root, "cwd-stats.json"), JSON.stringify({ version: 1, entries }));
|
|
133
|
-
|
|
134
|
-
const service = createService({
|
|
135
|
-
root,
|
|
136
|
-
runnerScript: "/no/runner.mjs",
|
|
137
|
-
piCommand: "pi",
|
|
138
|
-
piArgsPrefix: [],
|
|
139
|
-
defaultCwd: root,
|
|
140
|
-
launch: () => ({ pid: null, configPath: "/no/config.json" }),
|
|
141
|
-
launchHost: () => ({ pid: null, configPath: "/no/host-config.json" }),
|
|
142
|
-
launchTitle: () => ({ pid: null, configPath: "/no/title-config.json" }),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const tui = {
|
|
146
|
-
terminal: { rows: 24, cols: 80, columns: 80, write: () => {} },
|
|
147
|
-
requestRender: () => {},
|
|
148
|
-
};
|
|
149
|
-
const theme = { fg: (_c: string, t: string) => t, bold: (t: string) => t, bg: (_c: string, t: string) => t };
|
|
150
|
-
|
|
151
|
-
const dash = new DashboardComponent(tui, theme, {}, () => {}, {
|
|
152
|
-
service,
|
|
153
|
-
root,
|
|
154
|
-
defaultCwd: root,
|
|
155
|
-
availableModels: [],
|
|
156
|
-
currentModel: null,
|
|
157
|
-
currentThinkingLevel: "off",
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// TS-private is runtime-accessible; input must be non-empty or
|
|
161
|
-
// openLaunchDialog() bails back to list mode and launch stays null.
|
|
162
|
-
dash.input = "go";
|
|
163
|
-
dash.openLaunchDialog();
|
|
164
|
-
dash.openLaunchPicker("cwd", "");
|
|
165
|
-
for (const ch of "aler") dash.handleInput(ch);
|
|
166
|
-
|
|
167
|
-
const out = { target, mode: dash.launch?.cwdPickerMode, suggestions: dash.launch?.cwdSuggestions };
|
|
168
|
-
dash.dispose();
|
|
169
|
-
console.log(JSON.stringify(out));
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Create `test/dashboard-cwd-favorites-pool.test.mjs`:
|
|
173
|
-
|
|
174
|
-
```js
|
|
175
|
-
import assert from "node:assert/strict";
|
|
176
|
-
import { execFileSync } from "node:child_process";
|
|
177
|
-
import { fileURLToPath } from "node:url";
|
|
178
|
-
import { join } from "node:path";
|
|
179
|
-
import test from "node:test";
|
|
180
|
-
|
|
181
|
-
const ROOT_DIR = fileURLToPath(new URL("../", import.meta.url));
|
|
182
|
-
const PROBE = join(ROOT_DIR, "test-support", "cwd-favorites-pool-probe.ts");
|
|
183
|
-
|
|
184
|
-
test("cwd picker: favorites search covers entries ranked below the old 8-entry cap (issue #262)", () => {
|
|
185
|
-
// dashboard.ts uses TS parameter properties, which strip-only mode rejects;
|
|
186
|
-
// --experimental-transform-types handles them (Node 22.7+ / 24).
|
|
187
|
-
const out = execFileSync(process.execPath, ["--experimental-transform-types", PROBE], {
|
|
188
|
-
encoding: "utf8",
|
|
189
|
-
timeout: 30_000,
|
|
190
|
-
});
|
|
191
|
-
const r = JSON.parse(out);
|
|
192
|
-
assert.equal(r.mode, "favorites");
|
|
193
|
-
assert.deepEqual(r.suggestions, [r.target]);
|
|
194
|
-
});
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
- [ ] **Step 2: Run the probe test to verify it fails**
|
|
198
|
-
|
|
199
|
-
Run: `node --test test/dashboard-cwd-favorites-pool.test.mjs`
|
|
200
|
-
Expected: FAIL — `mode` is `"browse"` and `suggestions` is `[]`, because `dashboard.ts:755` still passes `, 8` and the rank-10 target falls outside the pool.
|
|
201
|
-
|
|
202
|
-
- [ ] **Step 3: Load the full pool in the dashboard**
|
|
203
|
-
|
|
204
|
-
In `src/ui/dashboard.ts`, inside `openLaunchPicker`, replace:
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
launch.cwdRanked = existingCwdCandidates(rankedCwdCandidates(this.deps.root, 8));
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
with:
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
launch.cwdRanked = existingCwdCandidates(rankedCwdCandidates(this.deps.root));
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
- [ ] **Step 4: Run the probe test to verify it passes**
|
|
217
|
-
|
|
218
|
-
Run: `node --test test/dashboard-cwd-favorites-pool.test.mjs`
|
|
219
|
-
Expected: PASS — `mode === "favorites"` and `suggestions === [target]`.
|
|
220
|
-
|
|
221
|
-
- [ ] **Step 5: Pin the state-machine semantics for long lists**
|
|
222
|
-
|
|
223
|
-
Append to `test/launch-options.test.mjs`:
|
|
224
|
-
|
|
225
|
-
```js
|
|
226
|
-
test("nextCwdPickerState searches matches below the 8th ranked entry (issue #262)", () => {
|
|
227
|
-
const bigRanked = Array.from({ length: 12 }, (_, i) => ({
|
|
228
|
-
path: `/ranked/dir-${String(i + 1).padStart(2, "0")}`,
|
|
229
|
-
count: 100 - i,
|
|
230
|
-
}));
|
|
231
|
-
const state = nextCwdPickerState("dir-10", bigRanked, "/tmp");
|
|
232
|
-
assert.equal(state.mode, "favorites");
|
|
233
|
-
assert.deepEqual(state.suggestions, [bigRanked[9].path]);
|
|
234
|
-
});
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
- [ ] **Step 6: Run the focused suites**
|
|
238
|
-
|
|
239
|
-
Run: `node --test test/cwd-stats.test.mjs test/launch-options.test.mjs test/dashboard-cwd-favorites-pool.test.mjs test/dashboard-cwd-tab.test.mjs`
|
|
240
|
-
Expected: PASS — new coverage plus the existing browse/favorites Tab regression (A4).
|
|
241
|
-
|
|
242
|
-
- [ ] **Step 7: Run the full gate**
|
|
243
|
-
|
|
244
|
-
Run: `npm run verify`
|
|
245
|
-
Expected: PASS — typecheck, perf gate, full test suite, coverage thresholds, pack:dry.
|
|
246
|
-
|
|
247
|
-
- [ ] **Step 8: Commit**
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
git add src/ui/dashboard.ts test-support/cwd-favorites-pool-probe.ts test/dashboard-cwd-favorites-pool.test.mjs test/launch-options.test.mjs
|
|
251
|
-
git commit -m "fix(cwd): search the full ranked history in the launch picker (issue #262)"
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## Acceptance Traceability
|
|
257
|
-
|
|
258
|
-
| Spec ID | Plan task | Verification in this plan |
|
|
259
|
-
| --- | --- | --- |
|
|
260
|
-
| A1 | Task 1 | Step 1–4 (`node --test test/cwd-stats.test.mjs`) |
|
|
261
|
-
| A2 | Task 2 | Step 1–4 (`node --test test/dashboard-cwd-favorites-pool.test.mjs`) |
|
|
262
|
-
| A3 | Task 2 | Step 5 (`node --test test/launch-options.test.mjs`) |
|
|
263
|
-
| A4 | Task 2 | Step 6 (focused suites incl. `test/dashboard-cwd-tab.test.mjs`) |
|
|
264
|
-
| A5 | Task 2 | Step 7 (`npm run verify`) |
|
|
265
|
-
| A6 | Task 1 / Task 2 | Existing corrupt-stats, stale-filter and empty-pool tests re-run in Steps 4 and 6 |
|
|
266
|
-
| U1 | post-merge | Manual TUI run per spec §8 U1 (requires updated install copy; recorded as `pending` until executed) |
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# Spec: screen.log Startup GC(startup 回收 ended view 的 screen.log)
|
|
2
|
-
|
|
3
|
-
- Issue: zhuxixi/pi-agent-board#1
|
|
4
|
-
- Date: 2026-08-15
|
|
5
|
-
- Status: draft(待 review)
|
|
6
|
-
|
|
7
|
-
## Background(来自调研,证据已评论到 issue)
|
|
8
|
-
|
|
9
|
-
上游 PR #41(commit `ee3780a`,2026-08-03 合入)已给**写路径**加上界:
|
|
10
|
-
`appendBoundedScreenLog()` 超过 `SCREEN_LOG_MAX_BYTES`(5 MB)时原子 compact 成
|
|
11
|
-
100 KB tail;runner 启动时 `reconcileScreenLog()` 压存量。本机验证(2026-08-15):
|
|
12
|
-
活跃 view 的 screen.log 最大 3.9 MB,cap 工作正常。
|
|
13
|
-
|
|
14
|
-
**残留缺口(本 spec 的 scope)**:
|
|
15
|
-
|
|
16
|
-
1. ended view 的 screen.log 无人回收——`reconcileScreenLog` 只在该 view 的
|
|
17
|
-
pty-runner 重启时触发;session 结束后 runner 不再运行,文件永久留存。
|
|
18
|
-
本机 views 目录仍有 1.6 GB 历史存量。
|
|
19
|
-
2. 全仓无 retention/prune/GC 机制(ended view 目录永久保留)。
|
|
20
|
-
3. `SCREEN_LOG_MAX_BYTES` / `SCREEN_LOG_REPLAY_BYTES` 硬编码,不可配。
|
|
21
|
-
|
|
22
|
-
## Goals
|
|
23
|
-
|
|
24
|
-
- Dashboard 启动时异步回收:ended 超龄 view 的 `screen.log` 被删除(unlink)。
|
|
25
|
-
- 两个配置项写入 `launch-prefs.json` 即生效:
|
|
26
|
-
- `screenLogRetentionDays`(默认 7;**0 = 关闭 GC**)
|
|
27
|
-
- `screenLogMaxSize`(默认 5 MB;透传给 pty-runner 写路径的 cap)
|
|
28
|
-
- 全程容错:GC 任何失败不得影响 dashboard 或 runner。
|
|
29
|
-
|
|
30
|
-
## Non-goals
|
|
31
|
-
|
|
32
|
-
- 不做 log 轮转(screen.log → screen.log.1)。写路径已有 cap + tail compact,轮转与其重叠(YAGNI)。
|
|
33
|
-
- 不做压缩(.gz)。
|
|
34
|
-
- 不删整个 view 目录;保留 `meta.json` / `state.json` / `evidence.json` 等 KB 级文件,dashboard 历史行不丢。
|
|
35
|
-
- 不动 job-runner 路径(它不写 screen.log)。
|
|
36
|
-
- 不引入防抖/状态持久化(扫描成本可忽略,每次启动都跑)。
|
|
37
|
-
|
|
38
|
-
## Design
|
|
39
|
-
|
|
40
|
-
### 新模块:`src/core/screen-log-gc.mjs`
|
|
41
|
-
|
|
42
|
-
单一职责,导出 `pruneScreenLogs(root, opts)`:
|
|
43
|
-
|
|
44
|
-
- 扫描 `<root>/views/view_*/`。
|
|
45
|
-
- 对每个 view,判定清理条件(全部满足才删):
|
|
46
|
-
1. `screen.log` 存在且非空;
|
|
47
|
-
2. view 已结束——`host.json` 的 `endedAt` 非 null(首选依据),
|
|
48
|
-
或 `host.state !== "alive"`;host.json 缺失/损坏时 fallback 用
|
|
49
|
-
screen.log 的 mtime 判定年龄;
|
|
50
|
-
3. 结束时间(或 mtime)早于 `now - retentionDays`。
|
|
51
|
-
- 满足条件 → `unlinkSync(screenLog)`。失败静默(单文件失败不中断整体扫描)。
|
|
52
|
-
- **活跃 view 一律跳过**:runner 持有 `screenLogBytes` 内存计数,外部动活跃
|
|
53
|
-
文件引入 race;活跃 view 由 `appendBoundedScreenLog` 的 cap 自管。
|
|
54
|
-
- fs 操作沿用 `screen-log.mjs` 的可注入 fs 模式(defaultScreenLogFs),便于单测。
|
|
55
|
-
- 返回统计 `{ scanned, removed, skippedActive, skippedFresh, bytesReclaimed, errors }`,
|
|
56
|
-
供可选的 diagnostics 记录。
|
|
57
|
-
|
|
58
|
-
### 触发点:`src/runtime/service.mjs`
|
|
59
|
-
|
|
60
|
-
- service 初始化时 fire-and-forget 调 `pruneScreenLogs()`(不 await、不阻塞首帧;
|
|
61
|
-
几百个 view 的 stat 扫描 <10ms,实际删除走 unlink,足够快)。
|
|
62
|
-
- 整个调用包 try/catch,异常静默;可选把统计 append 到 diagnostics.jsonl。
|
|
63
|
-
|
|
64
|
-
### 配置读取:`launch-prefs.json`
|
|
65
|
-
|
|
66
|
-
新增两个字段(缺省用默认值,向后兼容旧 prefs 文件):
|
|
67
|
-
|
|
68
|
-
| 字段 | 类型 | 默认 | 语义 |
|
|
69
|
-
|---|---|---|---|
|
|
70
|
-
| `screenLogRetentionDays` | number | 7 | ended 超过该天数的 view 清理 screen.log;0 = 关闭 GC |
|
|
71
|
-
| `screenLogMaxSize` | number | 5_000_000 | pty-runner 写路径 cap(字节),透传进 HostConfig → `appendBoundedScreenLog(opts.maxBytes)` |
|
|
72
|
-
|
|
73
|
-
- 非法值(负数、NaN、非数)回退默认值。
|
|
74
|
-
- `screenLogMaxSize` 传递链路:service 读 prefs → 写入 pty-runner 的 HostConfig
|
|
75
|
-
JSON → runner `appendBoundedScreenLog(screenLog, data, bytes, { maxBytes })`。
|
|
76
|
-
`appendBoundedScreenLog` 已支持 `opts.maxBytes`,runner 侧只需读取并透传。
|
|
77
|
-
|
|
78
|
-
### Error handling
|
|
79
|
-
|
|
80
|
-
- GC 整体 fire-and-forget + try/catch;单 view 失败跳过继续。
|
|
81
|
-
- host.json 读取失败 → mtime fallback;两者都失败 → 跳过该 view。
|
|
82
|
-
- prefs 读取失败 → 全默认值(不影响现有 launch-prefs 逻辑)。
|
|
83
|
-
|
|
84
|
-
### Testing
|
|
85
|
-
|
|
86
|
-
新增 `test/screen-log-gc.test.mjs`(沿用现有 test 风格与可注入 fs):
|
|
87
|
-
|
|
88
|
-
1. ended 超龄 view → screen.log 被删,meta/state 等文件保留。
|
|
89
|
-
2. 活跃 view(host.json `state: "alive"` 且 endedAt null)→ 不删。
|
|
90
|
-
3. ended 但未超龄 → 不删。
|
|
91
|
-
4. `screenLogRetentionDays: 0` → 全部不删。
|
|
92
|
-
5. host.json 缺失 → mtime fallback 正确判定。
|
|
93
|
-
6. `screenLogMaxSize` 经 HostConfig 透传到 runner cap(runner 侧单测或链路断言)。
|
|
94
|
-
7. 单文件 unlink 失败不中断其他 view 的清理。
|
|
95
|
-
|
|
96
|
-
## Data flow
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
/dashboard 打开
|
|
100
|
-
└─ service init
|
|
101
|
-
└─ (async, fire-and-forget) pruneScreenLogs(root, prefs)
|
|
102
|
-
├─ scan views/view_*/
|
|
103
|
-
├─ per view: ended? aged? → unlink screen.log
|
|
104
|
-
└─ return stats → (optional) diagnostics.jsonl
|
|
105
|
-
```
|