@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,605 +0,0 @@
1
- # probe 不得计入 attachedClients(issue #130)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:** 让 probe 连接在握手后立刻退出 `clients`(`attachedClients` 的唯一来源),并把这个记账策略收拢成一个纯函数,使两条 runner 路径不再漂移。
6
-
7
- **Architecture:** 新增纯函数 `helloBookkeeping(kind)` 到 `src/core/host-protocol.mjs`,把「哪些 socket 计入 `attachedClients`」「要不要写 host.json」变成一张显式决策表;`runner/pty-runner.mjs` 的 legacy / owned 两条 hello 分支改为只消费该描述符、只做副作用。probe 在 hello 时移出 `clients` 与 `terminalSubscriptions`,并且**不**写 host.json(解析期每 150ms 一轮 probe,写一次就是写放大);内存计数在 hello 后立刻正确,下一次心跳(≤1s)落盘。
8
-
9
- **Tech Stack:** Node.js ESM(`.mjs`)、`node:test`、`node:assert/strict`。无编译步骤;`npm run typecheck` 是 `tsc --noEmit` 对 `.ts`/JSDoc 的检查。
10
-
11
- ## Global Constraints
12
-
13
- - **工作根**:`WT=/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-130-probe-attached-clients`。所有读写用 `$WT/...` 绝对路径;git 用 `git -C "$WT"`。**不要在会话 cwd(主 checkout)里改任何文件**。
14
- - **不变量**:`attachedClients` 的来源永远是 `clients.size`(legacy `:256`、owned `:611`),不得新增第二个计数来源。
15
- - **判定逻辑收口**:`if (kind === "probe")` 这类判定只允许出现在 `helloBookkeeping` 内;两条 hello 分支不得再出现身份字符串比较(只消费描述符字段)。
16
- - **非目标(不得顺手改)**:owned close/error 的 probe 写抑制(`probeSockets`/`markProbe`)、legacy close 的无条件 `update()`、`attachedEver` 语义、`clients` 的广播角色、warm-host 回收阈值。
17
- - **注释即契约**:本 issue 的根因之一是注释声称 probe 不计数而实现计数。任何被改动的分支,其注释必须与实现一致。
18
- - **提交**:英文 conventional commits;`git add <file>` 逐文件 stage,**禁止 `git add -A`**。
19
- - **测试基线**:`main@3dad7f9` 的 `npm test` = **1047 tests / 1044 pass / 0 fail / 3 skipped**(30s,exit 0)。最终验收必须对比「无新增失败」。
20
- - **平台**:本改动不引入 OS 分支;测试走既有跨平台基建(`P.hostEndpointPathFor` / `P.controlSocketPath`)。
21
-
22
- ## 验收 ID 追溯总表
23
-
24
- | 验收 ID(spec §3) | 落点任务 |
25
- |---|---|
26
- | A1 hello 记账策略纯函数(unit) | Task 1 |
27
- | A3 legacy:probe 在连时不计入(integration) | Task 2 |
28
- | A4 判别性:probe + 真实 client 并存计数恰为 1 | Task 2、Task 3 |
29
- | A5 错值在一个心跳内自愈 | Task 2、Task 3 |
30
- | A6 probe 应答零回归(真实 `probeHost` 判 ready) | Task 2、Task 3 |
31
- | A7 既有 probe/reporter 语义零回归 | Task 2、Task 3、Task 4 |
32
- | A2 owned:probe 在连时不计入(integration) | Task 3 |
33
- | A8 静态 + 全量对比基线 | Task 4 |
34
-
35
- 端到端组合证据(spec §3):A2/A3 + `test/warm-host-sweeper.test.mjs` A2 + `test/warm-host-sweep.integration.test.mjs` A6 ⇒ probe 不再延迟 warm-host 回收。
36
-
37
- ---
38
-
39
- ### Task 1: `helloBookkeeping` 纯函数 + 决策表单测(A1)
40
-
41
- **Files:**
42
- - Modify: `src/core/host-protocol.mjs`(新增导出函数 + 模块头注释对齐)
43
- - Test: `test/host-protocol.test.mjs`(新增 4 个用例)
44
-
45
- **Interfaces:**
46
- - Consumes: 无(本任务是最底层,只有既有 `classifyClientHello`)
47
- - Produces: `helloBookkeeping(kind: "probe" | "editor-reporter" | "client") → { keepInClients: boolean, registerReporter: boolean, flipAttachedEver: boolean, persist: boolean, suppressCloseWrite: boolean }`。Task 2 / Task 3 按这些字段名消费,**不得改名**。
48
-
49
- - [ ] **Step 1: 写失败测试**
50
-
51
- 在 `$WT/test/host-protocol.test.mjs` 末尾追加(该文件已 `import { CLIENT_ID_EDITOR_REPORTER, CLIENT_ID_PROBE, classifyClientHello }`,需把 `helloBookkeeping` 加入同一 import):
52
-
53
- ```js
54
- test("helloBookkeeping keeps a probe out of clients and out of host.json (issue #130)", () => {
55
- assert.deepEqual(helloBookkeeping("probe"), {
56
- keepInClients: false,
57
- registerReporter: false,
58
- flipAttachedEver: false,
59
- persist: false,
60
- suppressCloseWrite: true,
61
- });
62
- });
63
-
64
- test("helloBookkeeping keeps the resident reporter bookkeeping-only (issue #103)", () => {
65
- assert.deepEqual(helloBookkeeping("editor-reporter"), {
66
- keepInClients: false,
67
- registerReporter: true,
68
- flipAttachedEver: false,
69
- persist: true,
70
- suppressCloseWrite: false,
71
- });
72
- });
73
-
74
- test("helloBookkeeping counts a real client as a full attachment", () => {
75
- assert.deepEqual(helloBookkeeping("client"), {
76
- keepInClients: true,
77
- registerReporter: false,
78
- flipAttachedEver: true,
79
- persist: true,
80
- suppressCloseWrite: false,
81
- });
82
- });
83
-
84
- test("helloBookkeeping is total over classifyClientHello and defaults to the conservative client row", () => {
85
- // Every kind the handshake can produce must have a row — and an unknown kind
86
- // must fall back to counting the socket, never to silently dropping it.
87
- const produced = [
88
- classifyClientHello({ type: "hello", clientId: CLIENT_ID_PROBE }),
89
- classifyClientHello({ type: "hello", clientId: CLIENT_ID_EDITOR_REPORTER }),
90
- classifyClientHello({ type: "hello" }),
91
- ];
92
- for (const kind of produced) {
93
- const book = helloBookkeeping(kind);
94
- assert.equal(typeof book.keepInClients, "boolean", `missing policy row for ${kind}`);
95
- }
96
- assert.equal(helloBookkeeping("probe").keepInClients, false, "the probe row is the one that must not count");
97
- assert.deepEqual(helloBookkeeping(undefined), helloBookkeeping("client"), "unknown kinds count as clients (conservative)");
98
- });
99
- ```
100
-
101
- - [ ] **Step 2: 跑测试确认失败**
102
-
103
- Run: `cd "$WT" && node --test test/host-protocol.test.mjs`
104
- Expected: FAIL — `SyntaxError: The requested module '../src/core/host-protocol.mjs' does not provide an export named 'helloBookkeeping'`
105
-
106
- - [ ] **Step 3: 最小实现**
107
-
108
- 在 `$WT/src/core/host-protocol.mjs` 的 `classifyClientHello` 之后追加:
109
-
110
- ```js
111
- /**
112
- * Socket bookkeeping implied by a hello classification (issue #103 §C / #130).
113
- *
114
- * `probe` and `editor-reporter` are bookkeeping-only: neither may keep
115
- * `attachedClients` — whose sole source is the runner's `clients` set —
116
- * non-zero, or warm-host reclaim never fires and hosts leak (issue #75). Both
117
- * therefore leave `clients` at hello time, while their socket stays writable so
118
- * probe replies and editor_state keep flowing.
119
- *
120
- * `persist: false` for probes is deliberate: the attach resolver probes every
121
- * HOST_PROBE_RETRY_MS (see host-probe.mjs), so one host.json write per probe
122
- * would amplify fenced writes — the exact cost the runner's probe-socket write
123
- * suppression exists to avoid. Dropping the socket from `clients` already makes
124
- * the in-memory count right, and the next heartbeat (<=1s) persists it.
125
- *
126
- * `suppressCloseWrite: true` is the other half of that suppression: close is the
127
- * wrong moment to flush a record a probe never changed.
128
- *
129
- * An unrecognized kind falls back to the real-client row on purpose — counting
130
- * an unknown connection is the conservative direction (it can only delay
131
- * reclaim, never risk killing a host with a live client).
132
- *
133
- * @param {"probe" | "editor-reporter" | "client"} kind
134
- * @returns {{ keepInClients: boolean, registerReporter: boolean,
135
- * flipAttachedEver: boolean, persist: boolean, suppressCloseWrite: boolean }}
136
- */
137
- export function helloBookkeeping(kind) {
138
- switch (kind) {
139
- case "probe":
140
- return { keepInClients: false, registerReporter: false, flipAttachedEver: false, persist: false, suppressCloseWrite: true };
141
- case "editor-reporter":
142
- return { keepInClients: false, registerReporter: true, flipAttachedEver: false, persist: true, suppressCloseWrite: false };
143
- default:
144
- return { keepInClients: true, registerReporter: false, flipAttachedEver: true, persist: true, suppressCloseWrite: false };
145
- }
146
- }
147
- ```
148
-
149
- 同时把该文件模块头注释的第 3-6 行从「keep bookkeeping-only connections out of `attachedClients` / `attachedEver`」扩写为指向 `helloBookkeeping` 的显式契约(避免再次出现「注释声称、实现不做」):
150
-
151
- ```js
152
- * Client ids travel in the `hello` handshake. `helloBookkeeping` turns the
153
- * classification into the explicit socket policy: probes and the resident
154
- * editor-state reporter stay out of `attachedClients` / `attachedEver`, and
155
- * probes additionally never trigger a host.json write — counting or writing
156
- * either would pin every host against warm-host reclaim (issue #75 / #103 §C /
157
- * #130).
158
- ```
159
-
160
- - [ ] **Step 4: 跑测试确认通过**
161
-
162
- Run: `cd "$WT" && node --test test/host-protocol.test.mjs`
163
- Expected: PASS — 7 tests(既有 3 + 新增 4),0 fail
164
-
165
- - [ ] **Step 5: 提交**
166
-
167
- ```bash
168
- git -C "$WT" add src/core/host-protocol.mjs test/host-protocol.test.mjs
169
- git -C "$WT" commit -m "feat(core): explicit hello socket policy, probes leave attachedClients (issue #130)"
170
- ```
171
-
172
- ---
173
-
174
- ### Task 2: legacy 路径接线 + integration 覆盖(A3 / A4 / A5 / A6 / A7)
175
-
176
- **Files:**
177
- - Modify: `runner/pty-runner.mjs:422-437`(legacy `handleClientLine` 的 `case "hello"`)+ `:40` import
178
- - Test: `test/pty-runner.integration.test.mjs`(在 `:1389` 之后新增一个用例)
179
-
180
- **Interfaces:**
181
- - Consumes: `helloBookkeeping(kind)`(Task 1 的精确签名与字段名)
182
- - Produces: legacy hello 分支的记账行为——probe 在 hello 后不在 `clients` 内、不写 host.json;reporter 行为不变
183
-
184
- - [ ] **Step 1: 写失败测试**
185
-
186
- 在 `$WT/test/pty-runner.integration.test.mjs` 第 1389 行(`test("legacy runner keeps the editor reporter and probes out of attachedClients/attachedEver (issue #103)")` 的收尾 `});`)之后、`// ---- child-exit error attribution (issue #90) ----` 之前插入。该用例复用文件内既有 helper(`freshRoot` / `waitFor` / `hostReady` / `readHost` / `send` / `waitForExit` / `stopRunner` / `reapChild`)。
187
-
188
- 先在文件头部 import 区加入真实探针:
189
-
190
- ```js
191
- import { probeHost } from "../src/core/host-probe.mjs";
192
- ```
193
-
194
- 再加用例:
195
-
196
- ```js
197
- test("legacy runner: a connected probe never inflates attachedClients (issue #130)", async () => {
198
- const root = freshRoot();
199
- let runner;
200
- let childPid;
201
- try {
202
- // Legacy host: host-config WITHOUT `instanceId`, so `main()` dispatches
203
- // `legacyMain()` and binds the stable control.sock endpoint.
204
- const meta = createView(root, { id: "v1", name: "legacy-probe-count", cwd: process.cwd() });
205
- const configPath = P.hostConfigPath(root, "v1");
206
- atomicWriteJson(configPath, {
207
- root,
208
- viewId: "v1",
209
- sessionFile: meta.sessionFile,
210
- cwd: process.cwd(),
211
- initialPrompt: null,
212
- piCommand: process.execPath,
213
- piArgsPrefix: [resolve("test-support/fake-pty-pi.mjs")],
214
- model: null,
215
- tools: null,
216
- env: { AGENT_BOARD_ALLOW_PIPE_FALLBACK: "1" },
217
- cols: 80,
218
- rows: 24,
219
- });
220
- runner = spawn(process.execPath, [resolve("runner/pty-runner.mjs"), configPath], { stdio: ["ignore", "pipe", "pipe"] });
221
- await waitFor(() => hostReady(root, "v1"));
222
- childPid = readHost(root, "v1")?.childPid ?? null;
223
- const socketPath = P.controlSocketPath(root, "v1");
224
- const revBefore = readHost(root, "v1").revision;
225
-
226
- // A probe that STAYS connected: the resolver's real pattern is
227
- // connect → probe hello → destroy, but the count must already be clean
228
- // after the hello — before the close is observed.
229
- const probe = createConnection(socketPath);
230
- probe.on("error", () => {});
231
- await once(probe, "connect");
232
- const probeMessages = [];
233
- let probeBuf = "";
234
- probe.on("data", (chunk) => {
235
- probeBuf += chunk.toString();
236
- const lines = probeBuf.split("\n");
237
- probeBuf = lines.pop() ?? "";
238
- for (const line of lines) if (line.trim()) probeMessages.push(JSON.parse(line));
239
- });
240
- probe.write(JSON.stringify({ type: "hello", clientId: "probe", wantOutput: false }) + "\n");
241
- // Two hellos = the unsolicited one sent on connect + the reply to ours,
242
- // which is sent AFTER classification ran — so the count is settled here.
243
- await waitFor(() => probeMessages.filter((m) => m.type === "hello").length >= 2, 5000);
244
-
245
- // A5/A3: cross at least one heartbeat write while the probe stays
246
- // connected. That is exactly the write that used to persist 1.
247
- const afterHeartbeat = await waitFor(() => {
248
- const h = readHost(root, "v1");
249
- return h && h.revision > revBefore ? h : false;
250
- }, 5000);
251
- assert.equal(afterHeartbeat.attachedClients, 0, "a connected probe must not be counted as attached");
252
- assert.notEqual(afterHeartbeat.attachedEver, true, "a probe must never mark the host attached");
253
-
254
- // A4: a real client alongside the probe must count exactly 1 — a probe
255
- // still sitting in `clients` would make this 2.
256
- const client = createConnection(socketPath);
257
- client.on("error", () => {});
258
- await once(client, "connect");
259
- client.write(JSON.stringify({ type: "hello", clientId: "ui-test" }) + "\n");
260
- const counted = await waitFor(() => {
261
- const h = readHost(root, "v1");
262
- return h && h.attachedClients === 1 ? h : false;
263
- }, 3000);
264
- assert.equal(counted.attachedClients, 1, "exactly one attached client — a counted probe would make this 2");
265
- assert.equal(counted.attachedEver, true, "a real client still flips attachedEver");
266
- client.destroy();
267
- const released = await waitFor(() => {
268
- const h = readHost(root, "v1");
269
- return h && h.attachedClients === 0 ? h : false;
270
- }, 3000);
271
- assert.equal(released.attachedClients, 0, "probe-only host must read as detached for warm-host reclaim");
272
-
273
- // A5 again, driven by the probe's own close: the persisted record must
274
- // still read 0 after the probe goes away and a heartbeat lands.
275
- const revAfterClient = readHost(root, "v1").revision;
276
- probe.destroy();
277
- const settled = await waitFor(() => {
278
- const h = readHost(root, "v1");
279
- return h && h.revision > revAfterClient ? h : false;
280
- }, 5000);
281
- assert.equal(settled.attachedClients, 0, "the record must stay at 0 once the probe is gone");
282
-
283
- // A6: the real resolver probe still classifies this legacy host as ready
284
- // (no expectedInstanceId → the legacy ready fallback).
285
- const probeResult = await probeHost(socketPath, { expectedViewId: "v1", expectedInstanceId: null });
286
- assert.equal(probeResult.classification, "ready", "the real resolver probe must still classify the host ready");
287
-
288
- // Cleanup stays on the tested path: natural child exit.
289
- const exitClient = createConnection(socketPath);
290
- exitClient.on("error", () => {});
291
- await once(exitClient, "connect");
292
- send(exitClient, { type: "input", data: "exit\r" });
293
- await waitForExit(runner, 5000);
294
- exitClient.destroy();
295
- } finally {
296
- await stopRunner(runner);
297
- reapChild(root, "v1");
298
- await new Promise((r) => setTimeout(r, 50));
299
- rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
300
- }
301
- });
302
- ```
303
-
304
- - [ ] **Step 2: 跑测试确认失败**
305
-
306
- Run: `cd "$WT" && node --test --test-name-pattern="a connected probe never inflates attachedClients" test/pty-runner.integration.test.mjs`
307
- Expected: FAIL — `AssertionError: a connected probe must not be counted as attached: 1 !== 0`(修复前的真实失败模式,与 spec §1.3 的实测一致)
308
-
309
- - [ ] **Step 3: 改 runner 的 legacy hello 分支**
310
-
311
- 把 `$WT/runner/pty-runner.mjs:40` 的 import 改为:
312
-
313
- ```js
314
- import { classifyClientHello, helloBookkeeping } from "../src/core/host-protocol.mjs";
315
- ```
316
-
317
- 把 `:422-437` 整段替换为:
318
-
319
- ```js
320
- case "hello": {
321
- // Bookkeeping-only clients must never pin the host against warm-host
322
- // reclaim (issue #103 §C / #130): probes are read-only and transient,
323
- // the editor reporter is resident. Both leave `clients` — the sole
324
- // source of `attachedClients` — while their socket stays writable so
325
- // probe replies and editor_state keep flowing. The policy lives in
326
- // `helloBookkeeping`, so this path and the owned one cannot drift.
327
- const book = helloBookkeeping(classifyClientHello(msg));
328
- if (!book.keepInClients) {
329
- clients.delete(socket);
330
- terminalSubscriptions.delete(socket);
331
- }
332
- if (book.registerReporter) editorReporters.add(socket);
333
- if (book.flipAttachedEver) update({ attachedEver: true });
334
- else if (book.persist) update();
335
- send(socket, { type: "hello", status: host, editorEmpty, generation: GENERATION });
336
- break;
337
- }
338
- ```
339
-
340
- > `book.suppressCloseWrite` 在 legacy 分支**不出现**:legacy 的 socket 从未被赋 `markProbe`(只有 owned 的 listen 块赋值),调用它会是恒空操作。描述符仍然导出该字段,A1 单测照旧钉住「probe 不得靠 close 写 host.json」这条路径无关的不变量。
341
-
342
- > 注意:legacy reporter 分支原来无条件 `update()`,现在由 `book.persist === true` 驱动,行为等价;probe 分支原来什么都不做,现在**只**移出集合、不 `update()` —— 这正是本任务的行为修复。
343
-
344
- - [ ] **Step 4: 跑测试确认通过**
345
-
346
- Run: `cd "$WT" && node --test --test-name-pattern="a connected probe never inflates attachedClients" test/pty-runner.integration.test.mjs`
347
- Expected: PASS
348
-
349
- Run(A7 同文件回归): `cd "$WT" && node --test test/pty-runner.integration.test.mjs`
350
- Expected: PASS,0 fail(含既有 `:1159` / `:1206` / `:1289` 三条 probe/reporter 用例)
351
-
352
- - [ ] **Step 5: 提交**
353
-
354
- ```bash
355
- git -C "$WT" add runner/pty-runner.mjs test/pty-runner.integration.test.mjs
356
- git -C "$WT" commit -m "fix(runner): legacy probes leave attachedClients at hello (issue #130)"
357
- ```
358
-
359
- ---
360
-
361
- ### Task 3: owned 路径接线 + integration 覆盖(A2 / A4 / A5 / A6 / A7)
362
-
363
- **Files:**
364
- - Modify: `runner/pty-runner.mjs:1091-1108`(owned `handleClientLine` 的 `case "hello"`)+ `:864-867` 注释
365
- - Test: `test/pty-runner.integration.test.mjs`(在 Task 2 新增用例之后再加一个,即 `:1204` 之后、原 `:1206` 之前的位置)
366
-
367
- **Interfaces:**
368
- - Consumes: `helloBookkeeping(kind)`(Task 1);owned 侧的 `ownedUpdate` / `socket.markProbe`(既有,不改语义)
369
- - Produces: owned hello 分支的记账行为——probe 在 hello 后不在 `clients` 内、不写 host.json,且 close/error 仍不写
370
-
371
- - [ ] **Step 1: 写失败测试**
372
-
373
- 把下面这个用例插入到 `$WT/test/pty-runner.integration.test.mjs` 中**紧接**原 `test("probe connections leave host.json untouched; real clients flip attachedEver (CR r1 f3)")` 的收尾 `});` 之后(该收尾原为 `:1204`),即插在 `test("editor reporter connections leave attachedClients/attachedEver untouched (issue #103)")` 之前:
374
-
375
- ```js
376
- test("owned runner: a connected probe never inflates attachedClients (issue #130)", async () => {
377
- const root = freshRoot();
378
- let runner;
379
- let childPid;
380
- try {
381
- const { runner: r, socketPath } = await launchOwnedRunner(root, "v1", "i130");
382
- runner = r;
383
- const host = await waitFor(() => {
384
- const h = readHost(root, "v1");
385
- return h?.state === "alive" && h?.readyAt != null && h?.childPid ? h : false;
386
- });
387
- childPid = host.childPid;
388
- const revBefore = readHost(root, "v1").revision;
389
-
390
- // A probe that STAYS connected. The owned path suppresses the probe's
391
- // close-time write on purpose (the resolver probes every 150ms), so the
392
- // count has to be right at hello time — waiting for close is not enough.
393
- const probe = createConnection(socketPath);
394
- probe.on("error", () => {});
395
- await once(probe, "connect");
396
- const probeMessages = [];
397
- let probeBuf = "";
398
- probe.on("data", (chunk) => {
399
- probeBuf += chunk.toString();
400
- const lines = probeBuf.split("\n");
401
- probeBuf = lines.pop() ?? "";
402
- for (const line of lines) if (line.trim()) probeMessages.push(JSON.parse(line));
403
- });
404
- probe.write(JSON.stringify({ type: "hello", clientId: "probe", wantOutput: false }) + "\n");
405
- // Two hellos = the unsolicited one sent on connect + the reply to ours,
406
- // which is sent AFTER classification ran — so the count is settled here.
407
- await waitFor(() => probeMessages.filter((m) => m.type === "hello").length >= 2, 5000);
408
-
409
- // A2: cross at least one heartbeat write while the probe stays connected.
410
- // That is exactly the write that used to persist attachedClients=1 and
411
- // then keep it wrong for a full heartbeat period (measured: ~975ms).
412
- const afterHeartbeat = await waitFor(() => {
413
- const h = readHost(root, "v1");
414
- return h && h.revision > revBefore ? h : false;
415
- }, 5000);
416
- assert.equal(afterHeartbeat.attachedClients, 0, "a connected probe must not be counted as attached");
417
- assert.notEqual(afterHeartbeat.attachedEver, true, "a probe must never mark the host attached");
418
-
419
- // A4: a real client alongside the probe must count exactly 1 — a probe
420
- // still sitting in `clients` would make this 2.
421
- const client = createConnection(socketPath);
422
- client.on("error", () => {});
423
- await once(client, "connect");
424
- client.write(JSON.stringify({ type: "hello", clientId: "ui-test" }) + "\n");
425
- const counted = await waitFor(() => {
426
- const h = readHost(root, "v1");
427
- return h && h.attachedClients === 1 ? h : false;
428
- }, 3000);
429
- assert.equal(counted.attachedClients, 1, "exactly one attached client — a counted probe would make this 2");
430
- assert.equal(counted.attachedEver, true, "a real client still flips attachedEver");
431
- client.destroy();
432
- const released = await waitFor(() => {
433
- const h = readHost(root, "v1");
434
- return h && h.attachedClients === 0 ? h : false;
435
- }, 3000);
436
- assert.equal(released.attachedClients, 0, "probe-only host must read as detached for warm-host reclaim");
437
-
438
- // A5: the probe's own close is write-suppressed on this path, so the
439
- // record must already be 0 and stay 0 across the next heartbeat.
440
- const revAfterClient = readHost(root, "v1").revision;
441
- probe.destroy();
442
- const settled = await waitFor(() => {
443
- const h = readHost(root, "v1");
444
- return h && h.revision > revAfterClient ? h : false;
445
- }, 5000);
446
- assert.equal(settled.attachedClients, 0, "the record must stay at 0 once the probe is gone");
447
-
448
- // A6: the real resolver probe still classifies this host as ready, and
449
- // leaves no residue behind.
450
- const probeResult = await probeHost(socketPath, { expectedViewId: "v1", expectedInstanceId: "i130" });
451
- assert.equal(probeResult.classification, "ready", "the real resolver probe must still classify the host ready");
452
- assert.equal(readHost(root, "v1").attachedEver, true, "probeHost must not disturb the record's attachment history");
453
-
454
- // Cleanup stays on the tested path: natural child exit.
455
- const exitClient = createConnection(socketPath);
456
- exitClient.on("error", () => {});
457
- await once(exitClient, "connect");
458
- send(exitClient, { type: "input", data: "exit\r" });
459
- await waitForExit(runner, 5000);
460
- exitClient.destroy();
461
- } finally {
462
- try { runner?.kill("SIGKILL"); } catch {}
463
- if (childPid) { try { process.kill(childPid, "SIGKILL"); } catch {} }
464
- await new Promise((r) => setTimeout(r, 50));
465
- rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
466
- }
467
- });
468
- ```
469
-
470
- - [ ] **Step 2: 跑测试确认失败**
471
-
472
- Run: `cd "$WT" && node --test --test-name-pattern="owned runner: a connected probe never inflates" test/pty-runner.integration.test.mjs`
473
- Expected: FAIL — `AssertionError: a connected probe must not be counted as attached: 1 !== 0`
474
-
475
- - [ ] **Step 3: 改 runner 的 owned hello 分支**
476
-
477
- 把 `$WT/runner/pty-runner.mjs:1091-1108` 整段替换为:
478
-
479
- ```js
480
- case "hello": {
481
- // Same bookkeeping contract as the legacy path (issue #103 §C /
482
- // #130): probes and the resident editor reporter are
483
- // bookkeeping-only — neither may flip attachedEver nor keep
484
- // attachedClients non-zero, or warm-host reclaim never fires and
485
- // hosts leak. The policy lives in `helloBookkeeping`.
486
- const book = helloBookkeeping(classifyClientHello(msg));
487
- if (!book.keepInClients) {
488
- clients.delete(socket);
489
- terminalSubscriptions.delete(socket);
490
- }
491
- if (book.registerReporter) editorReporters.add(socket);
492
- // The probe's close-time host.json refresh stays suppressed: the
493
- // resolver probes every HOST_PROBE_RETRY_MS, so flushing there
494
- // would amplify fenced writes (see the probeSockets comment at the
495
- // listen block). Being out of `clients` is what keeps the count
496
- // right — this only avoids a pointless write.
497
- if (book.suppressCloseWrite) socket.markProbe?.();
498
- if (book.flipAttachedEver) ownedUpdate((cur) => ({ ...cur, attachedEver: true }));
499
- else if (book.persist) ownedUpdate((cur) => ({ ...cur }));
500
- send(socket, { type: "hello", status: host, editorEmpty, generation: GENERATION });
501
- break;
502
- }
503
- ```
504
-
505
- 同时把 `:864-867` 的注释从
506
-
507
- ```js
508
- // Probe connections (clientId:"probe" hello) must not write host.json: the
509
- // attach resolver's 150ms probe loop would amplify fenced writes and flip
510
- // attachedEver with no client ever attached (CR round-1 finding 3).
511
- ```
512
-
513
- 改为
514
-
515
- ```js
516
- // Probe connections (clientId:"probe" hello) must not write host.json: the
517
- // attach resolver's 150ms probe loop would amplify fenced writes and flip
518
- // attachedEver with no client ever attached (CR round-1 finding 3). The
519
- // "not counted" half is handled at hello time instead (#130): the socket
520
- // leaves `clients` there, so this WeakSet only suppresses the write.
521
- ```
522
-
523
- - [ ] **Step 4: 跑测试确认通过**
524
-
525
- Run: `cd "$WT" && node --test --test-name-pattern="owned runner: a connected probe never inflates" test/pty-runner.integration.test.mjs`
526
- Expected: PASS
527
-
528
- Run(A7 全文件回归,含任务 2 的 legacy 用例): `cd "$WT" && node --test test/pty-runner.integration.test.mjs`
529
- Expected: PASS,0 fail
530
-
531
- - [ ] **Step 5: 提交**
532
-
533
- ```bash
534
- git -C "$WT" add runner/pty-runner.mjs test/pty-runner.integration.test.mjs
535
- git -C "$WT" commit -m "fix(runner): owned probes leave attachedClients at hello (issue #130)"
536
- ```
537
-
538
- ---
539
-
540
- ### Task 4: 全量验证 + 基线对比 + 组合证据(A7 / A8)
541
-
542
- **Files:**
543
- - 不改代码(只跑验证;若发现回归则回到对应任务修)
544
-
545
- **Interfaces:**
546
- - Consumes: Task 1–3 的全部产出
547
- - Produces: 用于 PR 描述的验收证据(每条 A 的实际命令与结果)
548
-
549
- - [ ] **Step 1: 静态检查**
550
-
551
- Run: `cd "$WT" && npm run typecheck`
552
- Expected: 0 error
553
-
554
- - [ ] **Step 2: 全量测试对比基线**
555
-
556
- Run: `cd "$WT" && npm test`
557
- Expected: `# tests >= 1049`(基线 1047 + 新增 6 个用例)、`# fail 0`、`# pass = tests - 3 skipped`。任何 fail 都必须定位到 Task 1–3 的某个改动并修掉,不得以「基线也有」为由放过——基线是 **0 fail**。
558
-
559
- - [ ] **Step 3: 端到端组合证据(不新建测试,逐条实跑并记录输出)**
560
-
561
- Run: `cd "$WT" && node --test test/warm-host-sweeper.test.mjs`
562
- Expected: PASS,含 `A2: attachedClients>0 永不淘汰`
563
-
564
- Run: `cd "$WT" && node --test test/warm-host-sweep.integration.test.mjs`
565
- Expected: PASS,含 `A6: 手动 sweep 后 idle host 收到 terminate`
566
-
567
- 组合结论(写进 PR 描述):A2/A3 证明「probe 在连时 `attachedClients` 落盘为 0」;warm-host-sweeper A2 证明「`attachedClients>0` 不淘汰」;warm-host-sweep A6 证明「`attachedClients=0` 的 idle host 收到 terminate」。三者相接即「probe 不再延迟 warm-host 回收」。
568
-
569
- - [ ] **Step 4: 确认工作区无残留**
570
-
571
- Run: `cd "$WT" && git status --short`
572
- Expected: 空(除 `docs/superpowers/plans/` 下的本计划文件若尚未提交)
573
-
574
- - [ ] **Step 5: 提交**
575
-
576
- ```bash
577
- git -C "$WT" add docs/superpowers/plans/2026-09-24-probe-attached-clients.md
578
- git -C "$WT" commit -m "docs(plan): issue #130 probe accounting implementation plan"
579
- ```
580
-
581
- ---
582
-
583
- ## Self-Review
584
-
585
- **1. Spec coverage(逐节对照)**
586
-
587
- | spec 节 | 覆盖任务 |
588
- |---|---|
589
- | §2.1 决策 D1(移出 `clients`、不写 host.json) | Task 1(`persist:false`)+ Task 2/3(分支只 delete、不 update) |
590
- | §2.2 决策 D2(抽 `helloBookkeeping`) | Task 1 |
591
- | §2.3 改动点清单 | Task 2 Step 3、Task 3 Step 3(含 `:423-426` / `:1092-1094` / `:864-867` 三处注释对齐与 `host-protocol.mjs` 模块注释) |
592
- | §3 A1–A8 | A1→Task 1;A3/A4/A5/A6(legacy)→Task 2;A2/A4/A5/A6(owned)→Task 3;A7→Task 2/3/4;A8→Task 4 |
593
- | §4 可测性拆分(判定只存在于纯函数) | Global Constraints「判定逻辑收口」+ Task 2/3 的分支代码只消费字段 |
594
- | §5 非目标 | Global Constraints「非目标」 |
595
- | §6 风险(hello 一次且权威) | Task 1 的 `default` 行注释 + Task 2/3 Step 1 的 A4 断言(probe 在连时真实 client 计 1) |
596
-
597
- **2. Placeholder scan**:无 TBD / TODO /「类似 Task N」;每个代码步骤都给了完整代码块。
598
-
599
- **3. Type consistency**:`helloBookkeeping` 的五个字段名(`keepInClients` / `registerReporter` / `flipAttachedEver` / `persist` / `suppressCloseWrite`)在 Task 1 定义、Task 2 与 Task 3 消费,逐字一致;测试辅助函数名(`launchOwnedRunner` / `hostReady` / `stopRunner` / `reapChild` / `waitFor` / `waitForExit` / `send` / `freshRoot`)均取自 `test/pty-runner.integration.test.mjs` 既有定义。
600
-
601
- **4. 已知实现注意点(给 implementer)**
602
-
603
- - Task 3 的新用例插在「原 `:1204` 之后、原 `:1206` 之前」;Task 2 的新用例插在「原 `:1389` 之后」。两个任务都改同一个测试文件,**必须按 Task 2 → Task 3 顺序执行**,否则行号锚点失效(按 `test(...)` 标题定位而非纯行号更稳)。
604
- - `probeHost` 的 import 只在 Task 2 加一次,Task 3 复用。
605
- - 测量脚本 `/tmp/repro-130.mjs` 与 `/tmp/measure-130.mjs` 是 issue 评论里的证据来源,**不进仓库**。