@huseynsnmz/pi-board 0.15.3 → 0.17.0

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