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