@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,190 +0,0 @@
1
- # Spec:probe 在连接窗口内计入 attachedClients(issue #130)
2
-
3
- 日期:2026-09-24 · 状态:**approved(用户确认 D1=A / D2=A / 范围=A)**
4
- 基线:`main @ 3dad7f9`(issue 行号基于 `794c755`,已逐条核对未漂移)
5
- 调研:issue #130 评论「根因确认 + 确定性复现」/ `~/.claude/github-issue-driven/zhuxixi/pi-agent-board/issue-130/research/root-cause.md`
6
-
7
- ## 1. 根因(systematic-debugging 结论)
8
-
9
- ### 1.1 一句话根因
10
-
11
- `runner/pty-runner.mjs` 的 `clients` 这个 Set 同时承担两个角色——**广播目标**与**已连接真实客户端计数**
12
- (`attachedClients: clients.size`,legacy `:256` / owned `:611`)。hello 分类把 reporter 从 `clients`
13
- 移了出去(#103),**却漏了 probe**:probe 只在 owned 路径被 `markProbe()` 标记(`:1096-1098`)、
14
- legacy 路径完全不处理(`:427-434`),于是 probe 从 connect 到 close 一直被计入 `clients.size`。
15
-
16
- ### 1.2 两条路径的差异(决定危害大小)
17
-
18
- | | 计数窗口 | 窗口内的错值如何被修正 |
19
- |---|---|---|
20
- | **legacy** | connect(`:375`)→ close(`:389/:396`) | close 处理器**无条件 `update()`** → 立即落盘正确值 |
21
- | **owned** | connect(`:869`)→ close(`:883/:899`) | close/error **对 probe 提前 return**(`:888`/`:898`,为抑制写放大)→ 错值要等下一次心跳(`HEARTBEAT_MS = 1000`,`:1055-1070`)才回落 |
22
-
23
- ### 1.3 实测暴露度(`/tmp/measure-130.mjs`:真实 `probeHost` 按 `HOST_PROBE_RETRY_MS = 150` 节奏压测,5ms 采样 host.json)
24
-
25
- 单次 12s / 80 次 probe 跑(宿主与采样器并行争抢 CPU)时:
26
-
27
- ```
28
- [owned] attachedClients!=0 in 195/2344 samples (8.32%)
29
- [owned] longest contiguous non-zero run ~975ms ← ≈ 一个心跳周期
30
- [owned] 落盘写入:1/12 次心跳记录了 attachedClients!=0
31
- [legacy] attachedClients!=0 in 0/2340 samples (0.00%)
32
- ```
33
-
34
- 随后 3 × 20s(~400 次 probe)单跑均为 0 命中。**结论:**
35
-
36
- 1. **owned 是真实暴露面,legacy 不是。** legacy 的 close 刷盘把错值寿命压到 probe 生命周期内,实测 0 命中;
37
- owned 因为 close 抑制,**一次命中就要错满一个心跳周期(~975ms)**。
38
- 2. 命中频率低但非零,且随宿主 CPU 负载(event loop 延迟)上升——正是 attach 解析进行时的工作状态。
39
- 3. 消费方周期是 60s(`src/index.ts:67` `AGENT_BOARD_SWEEP_INTERVAL_MS` 默认 60000),
40
- 所以最坏后果是「空闲宿主多活一个 sweep 周期(~1 分钟)」,方向保守、不误杀、不丢数据——与 issue 的评估一致。
41
-
42
- ### 1.4 影响链已端到端证实
43
-
44
- ```
45
- attachedClients=0 -> {"ttlEvicted":["v1"],"excessEvicted":[]} ← 到点即回收
46
- attachedClients=1 -> {"ttlEvicted":[],"excessEvicted":[]} ← 永不回收
47
- ```
48
-
49
- (`selectIdleHostsToEvict`,`src/core/warm-host-sweeper.mjs:53`;同款复查在 `src/runtime/service.mjs:807`。)
50
-
51
- ### 1.5 还有一条结构性理由(比 1.3 的暴露度更值得修)
52
-
53
- **owned 路径今天把「probe 停止计数」这件事挂在「观察到 close」上。** 一旦 close/error 事件永远不来
54
- (对端半开:进程卡死而非退出,issue 已点名的尾部风险),计数就永久挂住。改成「probe hello 即移出」后,
55
- 清理点从**生命周期末端**移到**握手早期**,probe 只要成功握手就不会留下计数——这是本修复的主要价值,
56
- 1.3 的暴露度收窄是副产品。
57
-
58
- ### 1.6 一句话根因之外
59
-
60
- - issue 的「注释与实现不符」判断正确:`:1092-1094` 与 `:423-426` 的注释都声称 probe 不得让 `attachedClients` 非 0,
61
- 实现不符;`src/core/host-protocol.mjs` 模块注释同款。
62
- - `attachedEver` **无任何生产消费方**(全仓 grep 仅 runner 写入 + `src/core/types.mjs:144` 注释 + 测试断言),
63
- 本 issue 的危害面完全落在 `attachedClients`。
64
-
65
- ## 2. 修复设计
66
-
67
- ### 2.1 决策 D1:probe hello 时把 socket 移出 `clients`,**不写 host.json**
68
-
69
- 采纳 issue 建议 1 的「移出 `clients`」半条,**明确不采纳**其「并刷新一次计数」半条。理由:
70
-
71
- | 取向 | 后果 |
72
- |---|---|
73
- | 移出 `clients`(做) | 内存计数在 hello 处理后立刻正确;后续心跳不可能再持久化 probe 的 1;清理点从 close 前移到 hello(§1.5) |
74
- | 移出时写 host.json(不做) | 解析期每 150ms 一轮 probe → 最多 ~6.7 次/秒的 fenced host.json 写入。这正是 `:864-867` 注释要防的写放大,reporter 的 `ownedUpdate` 是常驻连接的一次性写入才可接受 |
75
- | 不写会不会留下错误持久值 | 不会:`clients.size` 已在内存中正确,下一次心跳(≤1000ms)落盘正确值;错值寿命与 legacy 同量级,不引入新退化 |
76
-
77
- 残留窗口收窄为 `connect → hello 处理`(一次本地 socket RTT,且不依赖 close 事件)。**接受**该残留:
78
- 它在允许的 1 个心跳周期内自愈,且消费方是「非 0 就不杀」的保守判据。
79
-
80
- ### 2.2 决策 D2:把 hello 记账策略抽成纯函数
81
-
82
- 现状:两条 hello 分支各自内联判断(`:427-434` vs `:1095-1105`),已经漂移——owned 有 `markProbe`、legacy 没有;
83
- 注释声称的 probe 语义在两边都不成立。新增纯函数收拢决策(与 #103 抽出 `classifyClientHello` 同款做法):
84
-
85
- ```js
86
- // src/core/host-protocol.mjs(与 classifyClientHello 同文件)
87
- /**
88
- * Socket bookkeeping implied by a hello classification (issue #103 §C / #130).
89
- *
90
- * ...probe 必须 persist:false + suppressCloseWrite:true 的理由注释...
91
- * @param {"probe" | "editor-reporter" | "client"} kind
92
- * @returns {{ keepInClients: boolean, registerReporter: boolean,
93
- * flipAttachedEver: boolean, persist: boolean, suppressCloseWrite: boolean }}
94
- */
95
- export function helloBookkeeping(kind) { ... }
96
- ```
97
-
98
- | kind | keepInClients | registerReporter | flipAttachedEver | persist | suppressCloseWrite |
99
- |---|---|---|---|---|---|
100
- | `probe` | false | false | false | **false** | true |
101
- | `editor-reporter` | false | true | false | true | false |
102
- | `client` | true | false | true | true | false |
103
-
104
- 两条分支改为消费该描述符(副作用仍留在 runner,纯函数零 IO):
105
-
106
- ```js
107
- const kind = classifyClientHello(msg);
108
- const book = helloBookkeeping(kind);
109
- if (!book.keepInClients) { clients.delete(socket); terminalSubscriptions.delete(socket); }
110
- if (book.registerReporter) editorReporters.add(socket);
111
- if (book.suppressCloseWrite) socket.markProbe?.(); // legacy 无此抑制点,markProbe 为 undefined → 跳过
112
- if (book.flipAttachedEver) update({ attachedEver: true }); // owned: ownedUpdate(...)
113
- else if (book.persist) update(); // owned: ownedUpdate(...)
114
- ```
115
-
116
- `terminalSubscriptions.delete` 一并做:probe 从不 `subscribe_terminal`,留着只会让它继续收 `output` 原始行
117
- (`child.onData` 的 `terminalSubscriptions` 循环,legacy `:341` / owned `:991`)。收窄语义,与 reporter 对称,
118
- 已核对无副作用(probe 读到第一行数据就 `destroy()`,`src/core/host-probe.mjs:47-130`)。
119
-
120
- **若用户认为 D2 过度设计**:退化为「两条分支各自内联同样的修复 + 镜像注释」,A2/A3/A4 仍成立,
121
- 只是 D2 对应的 A1 单测与「两侧不漂移」的结构保证消失。**建议保留 D2。**
122
-
123
- ### 2.3 改动点清单(`runner/pty-runner.mjs`)
124
-
125
- | 位置 | 改动 |
126
- |---|---|
127
- | `:422-437` legacy hello 分支 | 改为消费 `helloBookkeeping`;probe 时 `clients.delete` + `terminalSubscriptions.delete`,**不** `update()` |
128
- | `:1091-1108` owned hello 分支 | 同上;probe 时补 `clients.delete` + `terminalSubscriptions.delete` + `markProbe()`,**不** `ownedUpdate()` |
129
- | `:423-426` legacy 注释 | 与实现对齐:probe「不翻转 `attachedEver`、不长期占计数、不写 host.json」,而非「从不计数」 |
130
- | `:1092-1094` 注释 | 同款对齐 |
131
- | `:864-867` owned probe 注释 | 补一句:probe 在 hello 时即移出 `clients`(#130),close 抑制只负责「不写」 |
132
- | `src/core/host-protocol.mjs` | 新增 `helloBookkeeping` + 模块注释对齐(把「keep bookkeeping-only connections out of attachedClients」的承诺落到显式策略) |
133
-
134
- ## 3. 验收矩阵
135
-
136
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
137
- |----|--------|----------|----------|----------|
138
- | A1 | hello 记账策略纯函数 | 自动化(unit) | `node --test test/host-protocol.test.mjs` | 三行决策表逐字段断言(probe: keepInClients/persist/flipAttachedEver 均 false 且 suppressCloseWrite true;reporter: registerReporter true、persist true、keepInClients false;client: keepInClients/persist/flipAttachedEver 均 true)。既有 `classifyClientHello` 用例保持全绿 |
139
- | A2 | owned:probe 在连时不计入 `attachedClients` | 自动化(integration) | `node --test test/pty-runner.integration.test.mjs`(新用例) | probe 握手完成后(见到第 2 条 `hello`,证明分类路径已跑完)保持 socket 连接,跨过至少一次心跳落盘,读到的 `attachedClients === 0`;`attachedEver !== true` |
140
- | A3 | legacy:同 A2 | 自动化(integration) | 同上(legacy 分支新用例,配置走 `P.hostConfigPath` 无 `instanceId`) | 同 A2 |
141
- | A4 | 判别性:probe 与真实 client 并存时计数恰为 1 | 自动化(integration) | 同上两条用例内 | probe 仍在连时,真实 client 的 hello 所触发的落盘写入里 `attachedClients === 1`(**修复前为 2**);client 断开后回落 `0` |
142
- | A5 | 错值在一个心跳内自愈(D1 不做即时刷盘的安全性) | 自动化(integration) | 同上两条用例内 | probe 断开后再跨一个心跳落盘,`attachedClients === 0` |
143
- | A6 | probe 应答能力零回归 | 自动化(integration) | 新用例内调用真实 `probeHost(socketPath, {...})` | `classification === "ready"`;且 `probeHost` 之后 `attachedClients === 0` |
144
- | A7 | 既有 probe / reporter 语义零回归 | 自动化(integration) | `node --test test/pty-runner.integration.test.mjs` | 既有用例 `:1159`、`:1206`、`:1289` 全绿(reporter 计数隔离、probe 不翻 `attachedEver`、legacy reporter/probe 隔离) |
145
- | A8 | 静态 + 全量 | 自动化(static + 全量 unit) | `npm run typecheck && npm test` | typecheck 0 error;与基线(实现前在 `main@3dad7f9` 记录,见 plan)对比**无新增失败** |
146
-
147
- **端到端效果的验收由组合证据给出**(不新建慢速 E2E,避免 flake):
148
-
149
- ```
150
- A2/A3(probe 在连 → attachedClients 落盘为 0)
151
- + test/warm-host-sweeper.test.mjs A2(attachedClients>0 → 不淘汰)
152
- + test/warm-host-sweep.integration.test.mjs A6(attachedClients=0 的 idle host → 收到 terminate)
153
- ⇒ probe 不再延迟 warm-host 回收
154
- ```
155
-
156
- 无需 `用户实测` 项:本改动是进程内记账语义,全部功能点都能在 unit/integration 层稳定判真,
157
- 不存在需要真机/外部服务才能观察的部分。
158
-
159
- ## 4. 可测性拆分设计(实现硬约束)
160
-
161
- | 拆分单元 | 位置 | 形态 | 测试边界 |
162
- |---|---|---|---|
163
- | `helloBookkeeping(kind)` | `src/core/host-protocol.mjs`(新增) | **纯函数**:入参三值字符串,出参为每次调用新建的记账描述符对象(不冻结,各次调用互不共享实例,别名安全);零 IO、零 socket | `test/host-protocol.test.mjs` 表驱动直接单测 → A1 |
164
- | `classifyClientHello(msg)` | `src/core/host-protocol.mjs`(既有,不改) | 纯函数 | 既有单测保持 → A1 |
165
- | hello 分支副作用 | `runner/pty-runner.mjs` 两处 | 只消费描述符、只做 `clients`/`terminalSubscriptions`/`editorReporters`/`update` 的副作用调用;**判定逻辑一律不得回流进分支** | 真实进程 integration → A2–A7 |
166
- | 计数落盘点 | 不改(`clients.size`,legacy `:256` / owned `:611`) | 保持单一来源 | 由 A2–A5 观测 |
167
-
168
- **硬约束**:D2 要求「probe 不写 host.json」这一决策只存在于 `helloBookkeeping` 一处;两条分支不得再出现
169
- `if (kind === "probe")` 式的判定。这样 A1 的单测就是该不变量的完整证明,integration 只证明接线正确。
170
-
171
- ## 5. 非目标
172
-
173
- - **不动 owned close/error 的 probe 写抑制**(`:888`/`:898`)。它负责「不放大写入」,D1 已让「不计入」与它解耦。
174
- - **不改 legacy close 时的无条件 `update()`**。既有行为,且它正是 legacy 实测 0 命中的原因。
175
- - **不改 `attachedEver` 语义**(无消费方;probe 本就不该翻,已正确)。
176
- - **不重构 `clients` 的「广播目标 / 计数来源」双角色**。这是根因层架构气味(§1.1),
177
- 但拆开需要改广播语义,超出保守修复边界——记为已知债,不在本 issue 处理。
178
- - **不处理「probe 连上但从不发 hello」**:分类必须依赖 hello(握手前无法区分 probe 与尚未 hello 的真实客户端),
179
- 该窗口由 `HOST_PROBE_TIMEOUT_MS = 250`(`src/core/host-probe.mjs:16`)与 probe 自身的 `destroy()` 兜底。
180
- - 不改 warm-host 回收策略本身(TTL / maxWarm / sweep 周期)。
181
-
182
- ## 6. 风险
183
-
184
- | 风险 | 对策 |
185
- |---|---|
186
- | hello 被同一 socket 重复发送且身份改变(先 `probe` 后 `client`)→ 永久漏计(危险方向:可能误杀活跃客户端) | 既有协议假设「hello 一次且权威」;reporter 的 `clients.delete` 已在 #103 依赖同一假设。在代码注释中显式写明该前提,并由 A4 钉住「probe 仍在连时真实 client 计 1」 |
187
- | D1 不即时刷盘 → 某次心跳仍可能落盘错值 | 接受:错值寿命 ≤ 1 个心跳周期,消费方是「非 0 就不杀」的保守判据;A5 钉住自愈 |
188
- | D2 新增描述符与 runner 实际副作用脱节(描述符说 `persist:false` 但分支仍写了) | A2/A3 用真实落盘观测(不是看描述符):probe 在连时跨心跳读到的必须是 0 |
189
- | `terminalSubscriptions.delete` 影响 probe 回包 | probe 只读第一行数据即 `destroy()`(`src/core/host-probe.mjs`);A6 用真实 `probeHost` 断言 `ready` |
190
- | Windows 命名管道路径下的时序差异 | 本改动不引入路径/平台分支;A2–A5 走既有跨平台 integration 基建(`P.hostEndpointPathFor`) |
@@ -1,394 +0,0 @@
1
- # Issue 145 Spec — 第 8 个语义状态 `holding`(搁置)+ 人工判定锁解封回归修复
2
-
3
- > Issue: zhuxixi/pi-agent-board#145
4
- > 基线:`main@c93307d`(chore: release 0.9.0 (#146))
5
- > 调研记录:`research/round1-fence-and-prereq-gap.md`、`research/round2-ui-and-callsites.md`
6
- > 状态:待用户审阅(未提交任何仓库改动)
7
-
8
- ## 1. 意图与目标
9
-
10
- 看板的 **Done 分组不可信**:`completed` 被迫同时承载「真收工」和「先搁置」两种含义,用户无法回答「哪些是真的收工了」。
11
-
12
- 本 spec 交付两件事,按两个独立功能点验收:
13
-
14
- 1. **`F-hold`:新增第 8 个语义状态 `holding`**,人工独占——只有人能写、只有人能解,自动分类器永不产出,不参与自动续跑,不阻塞 attach 与 warm host 回收。
15
- 2. **`F-lift`:修复人工判定锁(manual fence)在用户交互路径上的解封缺口**。这是既有功能的**回归**(不是新功能依赖):`service.syncForegroundEvent` 的 JSDoc 明写「Without this, a row that was completed/needs_input **can keep looking stale** after the user types a follow-up in the real Pi session」,而 fence 在 manually-judged 行上把这段能力完全废掉。
16
-
17
- `F-hold` 的「说话即恢复」依赖 `F-lift`;但 `F-lift` 有独立价值,因此独立验收。
18
-
19
- 成功判据:面板能回答「哪些是真收工」;对已判定(Done / 搁置)的行说话,行状态跟随实际会话,不再出现「面板显示 Done 而 agent 在跑」。
20
-
21
- ## 2. 非目标
22
-
23
- - **批量搁置**:select 模式不加 `h`,不做批量 holding 入口。
24
- - **顶栏 / footer 计数**:holding 不进 `HeaderCounts`,也不进 `index.ts` 的 `setStatus`。
25
- - **`mark_completed` 的 source 限制**:既有行为不动(只有新增的两个 kind 加用户来源守卫)。
26
- - **恢复原状态**:`h` 解除搁置后回落 `idle`(Needs instructions),不记账、不恢复搁置前的状态。
27
- - **`EvidenceOutcome` 之外的新契约**:不改 evidence 的其他字段。
28
- - **多轮乱序 / coordinator 崩溃窗口**:既有架构行为,非本次回归。
29
- - **`isManualCompletion` 的窄语义保留**:第 5.3 节给出结论(无窄语义调用点,直接改名)。
30
-
31
- ## 3. 决策记录
32
-
33
- ### 已拍板(用户 2026-09-29 确认)
34
-
35
- | # | 决策 | 结论 |
36
- |---|---|---|
37
- | D1 | 恢复模型 | **说话即恢复 + `h` 显式 toggle**。speak 走 F-lift 的解封机制;`h` 做 holding ⇄ idle 切换 |
38
- | D2 | F-lift 修复范围 | **对所有人工判定生效**(completed 与 holding 一视同仁) |
39
- | D3 | fence 调用点策略 | **全部宽化**:14 个调用点统一用宽谓词 `isManualVerdict()` |
40
- | D4 | `d` 对 holding 的语义 | **转成真 `completed`**(零改动:`confirmDone` 只挡 completed,多选过滤也只排除 completed) |
41
- | D5 | 展示位置与计数 | GROUP_ORDER 插在 `idle` 之后、`completed` 之前;顶栏不计数 |
42
- | D6 | 按键与文案 | 键 `h`(已验证 list 模式空闲);标签 `On hold` |
43
- | D7 | 可转态范围 | **任何非 busy 状态**(与 `mark_completed` 的守卫对齐) |
44
- | D8 | evidence 契约 | `outcome: "holding"` + `ready: true` |
45
-
46
- ### 实现级决策(本 spec 提出,待审阅)
47
-
48
- | # | 决策 | 结论与理由 |
49
- |---|---|---|
50
- | D9 | 命令层形态 | **两个独立 kind**:`mark_holding`、`clear_holding`。分开而非一个带 boolean 的 kind——「解除搁置」是独立语义(把行交回自动管理、回到 Needs instructions),不是「非 holding」 |
51
- | D10 | `clear_holding` 目标状态 | `idle`。`idle`(Needs instructions)的语义就是「run 结束了、没提出问题、等你下一句指令」,正是解除搁置后的状态 |
52
- | D11 | summary 处理 | `mark_holding` 与 `clear_holding` **都保留既有 summary**,与 `mark_completed` 同构。状态变化由分组标题承载,不虚构 summary 规则 |
53
- | D12 | 服务端解封机制 | **把镜像命令的 source 从 `service` 换成 `dashboard-user`**,不新增 command kind。依据:`sync_foreground` 的 source 白名单已含 `dashboard-user`;`stale_run` 守卫要求 `command.runId` 非空而该命令 `runId: null`,所以换 source **只穿过 `manual_fence` 这一道**(已逐行核对 `decideStateTransition` 守卫顺序) |
54
- | D13 | 解封触发信号 | 子 Pi 的 **`InputEvent.source === "interactive"`**。pi 的既有类型 `InputSource = "interactive" \| "rpc" \| "extension"`,语义就是「用户输入 / RPC 注入 / 扩展注入」。比板子侧猜用户意图可靠 |
55
- | D14 | 非用户来源的命令拒绝理由 | 新增 `source_not_allowed` 并加入 `DECIDED_REJECT_REASONS`。不复用 `field_not_allowed`(那是字段白名单语义)或 `manual_fence`(那是「行已被 fence」语义) |
56
- | D15 | 窄谓词处置 | `isManualCompletion` **改名为 `isManualVerdict`**,不保留旧函数(无窄语义调用点,保留即死代码) |
57
- | D16 | `canAutoDrain` 可测性 | 从 `service.mjs` **提取到 `src/core/warm-host-sweeper.mjs`** 并导出。它是纯行谓词(`isAgentBusy` + 状态白名单的组合),该模块已托管同类谓词(`hasPendingQuestions`、`isAgentBusy`)且是既有单测归宿(`test/warm-host-sweeper.test.mjs:105`)。模块名略有拉伸,是接受的取舍 |
58
-
59
- **决策的衍生行为(评审补充,需要知晓)**:
60
-
61
- - **D10 的衍生**:`clear_holding` 落到 `idle` 后,该行立即重新满足 `canAutoDrain`——若此前有排队 follow-up,下一次 reconcile(面板打开 / session_start / 各类触发点)会自动投递开跑。这与「解除搁置 = 交回自动管理」一致(un-hold 即 resume),但用户可能没预料到「按个 h 就自己跑起来了」;属设计内行为,A8 已锁定其谓词基础。
62
- - **D7 的衍生**:从 `needs_input`(无 pendingQuestions、进程已退出的那种)置搁置会清掉 `question` 字段。上下文不丢——D11 保留 summary,且 attach 进真实会话后问题上下文仍在会话文件里;只是面板不再展示该问题。与 `mark_completed` 同构。
63
-
64
- ## 4. 契约
65
-
66
- ### 4.1 词表与类型契约
67
-
68
- | 常量/类型 | 位置 | 变更 |
69
- |---|---|---|
70
- | `SemanticState` typedef | `src/core/types.mjs:6` | 手写联合类型加入 `"holding"`(**issue 正文漏项**;不加入会被 `GROUP_LABELS` 的 excess property check 拦住,但报错指向 `GROUP_LABELS` 而非根因) |
71
- | `SEMANTIC_STATES` | `src/core/types.mjs:40` | 在 `idle` 之后插入 `"holding"` |
72
- | `GROUP_ORDER` | `src/core/types.mjs:53` | 在 `idle` 与 `completed` 之间插入 `"holding"` |
73
- | `GROUP_LABELS` | `src/core/types.mjs:64` | `holding: "On hold"` |
74
- | `EvidenceOutcome` typedef | `src/core/types.mjs:14` | 加入 `"holding"`(该词表已含全部 7 个语义状态 + `unknown`/`in_progress`/`ready`,holding 走同一模式) |
75
- | `STAGE_RGB` | `src/ui/dashboard.ts:1920` | 加 holding 色(建议 `[251, 191, 36]` 琥珀系,与 needs_input 的 `[245,158,11]` 区分)。**唯一编译期护栏**(`satisfies Record<keyof typeof GROUP_LABELS, ...>`) |
76
-
77
- **不变项**:`AutoStateKind`(`src/core/auto-state.mjs:21`)保持 `needs_input|in_progress|done`;`semanticStateForAutoKind`(`:56`)永不返回 `holding`。
78
-
79
- ### 4.2 命令契约
80
-
81
- 新增两个 kind,加入 `STATE_COMMAND_KINDS`(`src/core/state-commands.mjs:36-50`)。
82
-
83
- `mark_holding`:
84
-
85
- ```
86
- source 守卫: command.source !== "dashboard-user" → reject("source_not_allowed")
87
- busy 守卫: currentState.processState === "alive" → reject("busy")
88
- apply mutate: state: {
89
- semanticState: "holding",
90
- processState: "exited",
91
- needsInput: false,
92
- hasError: false,
93
- question: null,
94
- pendingQuestions: [],
95
- error: null,
96
- autoState: null,
97
- }
98
- status: { autoState: null }
99
- ```
100
-
101
- `clear_holding`:
102
-
103
- ```
104
- source 守卫: command.source !== "dashboard-user" → reject("source_not_allowed")
105
- 状态守卫: currentState.semanticState !== "holding" → reject("no_change")
106
- busy 守卫: currentState.processState === "alive" → reject("busy")
107
- apply mutate: state: {
108
- semanticState: "idle",
109
- processState: "exited",
110
- needsInput: false,
111
- hasError: false,
112
- question: null,
113
- pendingQuestions: [],
114
- error: null,
115
- autoState: null,
116
- }
117
- status: { autoState: null }
118
- ```
119
-
120
- 契约要点:
121
- - `autoState: null` 是两个方向的**共同 fence 信号**,让 fence 语义与 `mark_completed` 完全同款。
122
- - mutate 返回**完整字段集**(不是 diff)。coordinator 的 `materialize`(`runner/state-coordinator.mjs:346`)做 `{ ...state, ...mutate.state }` 稀疏合并,因此重复提交是幂等的,不需要 `no_change` 去重。
123
- - `status: { autoState: null }` 沿用 `mark_completed` 的「patch 存在 ≠ 必须建文件」语义(`STATUS_BOOTSTRAP_KINDS` 不含这两个 kind),不为无 status 文件的 legacy 行伪造文件。
124
- - 两个 kind 都加入 `LAST_ACTIVITY_STAMP_KINDS`(`runner/state-coordinator.mjs:72`),与 `mark_completed` 同构。
125
-
126
- **协议版本**:新增 command kind ⇒ **`COORDINATOR_PROTOCOL_VERSION` 从 2 bump 到 3**(`src/core/coordinator-protocol.mjs:13`)。不 bump 的后果是 #108 的复现:detached 常驻的旧 coordinator pong 正常 → 被复用 → 新 kind 全部 `unknown_kind` 静默拒收,holding 写入永久失败且只有 warn 级 diagnostic。bump 后 `ensureCoordinator` 会 SIGTERM 旧实例再拉起(journal + boot replay 保证崩溃安全)。
127
-
128
- **payload 校验**:两个 kind 都**不**新增 `validateCommand` 的 payload case(与 `mark_completed` 相同走 fall-through),payload 允许为 `{}`。
129
-
130
- ### 4.3 fence 契约(谓词泛化)
131
-
132
- `src/core/auto-state.mjs`:
133
-
134
- ```js
135
- export function isManualVerdict(state) {
136
- return Boolean(state && (state.semanticState === "completed" || state.semanticState === "holding") && state.autoState == null);
137
- }
138
- ```
139
-
140
- `isManualCompletion` 删除,14 个生产调用点全部改用 `isManualVerdict`(逐点判定见调研 round 2 第 4 节;结论是**无任何调用点需要窄语义**,每处意图都是「用户已作出终态判定,自动化别再动这一行」)。
141
-
142
- 权威守卫(`src/core/state-commands.mjs:280`)随之覆盖 holding:
143
-
144
- ```js
145
- if (command.source !== "dashboard-user" && isManualVerdict(currentState)) {
146
- return reject("manual_fence");
147
- }
148
- ```
149
-
150
- 守卫顺序不变:`unknown_view` → `revision_conflict` → `stale_run` → `manual_fence` → kind 分支。
151
-
152
- ### 4.4 解封契约(F-lift)
153
-
154
- **触发条件**:`syncRowEvent` 收到 `event.type === "input" && event.source === "interactive"`。
155
-
156
- **动作**:该次 `writeForegroundState` 发出的 `sync_foreground` 命令,source 用 `"dashboard-user"` 而非 `"service"`。
157
-
158
- **覆盖面**:两条用户路径都会命中——attach 会话里直接敲字、`reply` 注入到存活 host。机制依据:`pty-runner` 对两者都执行 `child.write(msg.data)`(`runner/pty-runner.mjs:443`、`:1122-1138`),字节进入子 Pi 的 stdin,pi 一律报 `source: "interactive"`。
159
-
160
- **不触发**:`rpc` / `extension` 来源的 input 事件,以及 `before_agent_start` / `agent_start`(这两个事件不带 source 字段,验证自 pi 的 d.ts)。
161
-
162
- **会触发的三类投递**(三者都经 `child.write()` 进 PTY 字节,pi 一律报 `interactive`;评审时逐条核实):
163
-
164
- | 投递路径 | 性质 | 行为 |
165
- |---|---|---|
166
- | attach 会话里用户直接敲字 | 用户 | 解封——设计目标 |
167
- | `reply` 的 `row.hostActive` 分支(`service.mjs:1617`) | 用户 | 解封——设计目标(D1 说话即恢复) |
168
- | `drainNextFollowUp` 的 `row.hostActive` 分支(`service.mjs:1338-1360`)经 `sendHostInput` 注入 | 自动 | **也会解封**——见下 |
169
-
170
- **自动续跑交互(评审发现,初稿此处结论写反了已修正)**:`canAutoDrain` 包含 `completed`,所以手动完成的行若有排队 follow-up,reconcile 会自动投递;该投递也是 PTY 字节,同样报 `interactive`,因此 F-lift 会在自动投递上触发,行从 Done 翻成 Running。
171
-
172
- 这是**既有不一致的修正**而非新破坏:对**没有存活 host** 的 completed 行,`drainNextFollowUp` 走 `launchHost` → `markQueued`(source `dashboard-user`,`service.mjs:217`)——**今天就已经解除 fence**、行进 Queued/Running;只有**有存活 host** 的 completed 行才出现「follow-up 实际执行、行冻在 Done」(本机诊断即此病态现场)。F-lift 把两条路径收敛到同一语义:行跟随现实。
173
-
174
- **holding 不受自动投递影响**:`canAutoDrain` 白名单不含 holding,自动投递根本不会发生。已知窄竞态:`claimNextFollowUp` 之后、`sendHostInput` 落地之前用户恰好置搁置——此时注入仍触发 F-lift,行转 working。窗口为毫秒级,且此刻 follow-up 已被投递、行必然执行,转 working 是诚实状态;接受并记录。
175
-
176
- **`ensureHost` 的 `markQueued: false` 保持不变**:attach 只看不敲不解封。issue 改动清单暗示要动这条路径;本设计刻意不动它,把解封时机后移到真实输入事件,语义更准(看不是意图,说才是)。
177
-
178
- **时序说明**:`input` 事件先于 `before_agent_start` / `agent_start` 到达,所以后两者落地时行已解封。若跨 socket 顺序反转(当前实现中 `writeForegroundState` 同步算出投影、socket 写按调用顺序发出,实际不会反转),后到者会收到一次 `manual_fence` 拒绝并留一条 info 级诊断,**最终状态仍收敛正确**——这是接受的降级。
179
-
180
- ### 4.5 不变量契约(白名单,判定逻辑不改,只补测试)
181
-
182
- | 谓词 | 位置 | 现状 | holding 的行为 |
183
- |---|---|---|---|
184
- | `isAgentBusy(row)` | `src/core/warm-host-sweeper.mjs:23-26` | `row.alive && (st === "queued" \|\| st === "working" \|\| hasPendingQuestions(row))` | **白名单**,holding 自然为 `false` → 可 attach、可标 done、warm host 照常按 TTL 回收 |
185
- | `canAutoDrain(row)` | `src/runtime/service.mjs:2278-2281`(按 D16 迁到 `warm-host-sweeper.mjs`) | `!isAgentBusy(row) && (st === "idle" \|\| st === "completed")` | **白名单**,holding 自然为 `false` → 搁置行不被排队 follow-up 自动唤醒 |
186
-
187
- 两个谓词都是白名单,因此 holding 的排除是**结构性**的。本次只补回归测试锁定该性质,不动判定表达式(`canAutoDrain` 按 D16 换位置,表达式逐字符不变)。
188
-
189
- **reconcile 安全性(评审核实)**:`reconcile()` 的 `looksActive`(`service.mjs:2023`)= `processState === "alive" || semanticState ∈ {queued, working}`——holding 行是 `exited`,不在其中,**整个 reconcile 循环跳过搁置行**,不会把它 finalize 成 failed/idle。F-lift 之后行进入 alive 路径,与既有前台镜像行为一致:host 存活时 `!row.hostAlive` 不成立、不触发 host 收尾分支;host 干净退出后由 `reconcile_finalize` 以 `idle` 收敛(均为既有语义)。reconcile 末尾的 follow-up drain 循环用 `canAutoDrain` 门控,holding 被排除。
190
-
191
- ### 4.6 已核实的实现前提
192
-
193
- 以下三条在 spec 审查期逐行核实,是本设计成立的前提,不需要改动:
194
-
195
- - **`validateCommand` 只做全局 source 检查**(`src/core/state-commands.mjs:158`:`COMMAND_SOURCES.includes(raw.source)`),**没有 per-kind 的 source 限制**。所以 `sync_foreground` 携带 `source: "dashboard-user"` 能通过校验(D12 前提)。
196
- - **`COORDINATOR_PROTOCOL_VERSION` 全仓按名引用,无硬编码字面量**(唯一出现 `protocolVersion: 1` 的是 `test/coordinator-client.test.mjs:399` 的「旧实例」故意构造)。bump 到 3 后,`test/coordinator-client.test.mjs:371` 的 `assert.equal(pong.protocolVersion, COORDINATOR_PROTOCOL_VERSION)` 自动适配。
197
- - **`evidence.ready` 只有展示与过滤两处消费**(`src/ui/dashboard-evidence.mjs:16` 拼字符串、`src/core/rows.mjs:344` 的 `review:ready` 过滤器),**不参与任何功能门禁**。因此 D8 取 `ready: true` 不会意外唤醒任何自动行为。
198
- - **D12 在三层全部验证通过(评审复核)**:① `validateCommand` 无 per-kind source 限制(见上);② coordinator 外壳对 `command.source` **零消费**(`rg '\.source\b' runner/state-coordinator.mjs` 无命中,source 只在决策层 `decideStateTransition` 起作用);③ `stale_run` 守卫要求 `command.runId` 非空而 `sync_foreground` 恒为 `runId: null`(`service.mjs:767`)。所以换 source 的全部效果就是穿过 `manual_fence` 这一道。
199
- - **`projectViewState` 透传 `semanticState` 与 `autoState`**(`src/core/events.mjs:204`、`:220`),因此 input 分支算出的 `working` 投影落地后 `isManualVerdict` 为 false,行脱离 fence 的机制闭环。
200
-
201
- ### 4.7 命名消歧约定
202
-
203
- 「hold」在本仓库 attach 子系统已有既定含义:shrink-and-hold jiggle 协议(`src/core/pty-attach-jiggle-controller.mjs` 出现 39 次、`src/ui/pty-attach.ts` 9 次,指「attach 时把子终端缩到 (cols-1, rows-1) 并保持到收到 full clear」)。
204
-
205
- 约定:
206
- - 语义状态在**代码标识符**里一律用 `holding` / `isHolding` / `markHolding` 这类完整词,**禁止**出现裸 `hold` 标识符。
207
- - 注释与诊断消息里提到本状态时带上限定,如「on-hold semantic state」「搁置」,不写裸「hold」,避免与 jiggle 协议混淆。
208
- - 该约定是文档纪律,无法机制强制;实现期的 CR 检查项之一。
209
-
210
- ### 4.8 UI 契约
211
-
212
- | 位置 | 变更 |
213
- |---|---|
214
- | `dashboard.ts:355` `handleListKey` | 加 `if (data === "h") return this.toggleHold();` |
215
- | `dashboard.ts` `handlePeekKey` | 加 `h` |
216
- | `dashboard.ts` `handleSessionKey` | 加 `h`(对齐 `d` 的三处可用性) |
217
- | `dashboard.ts:967` `confirmDone` | **不改**。holding 行天然通过(只挡 `completed`) |
218
- | `dashboard.ts:985` `confirmDoneSelection` | **不改**。过滤条件只排除 `completed` |
219
- | `dashboard.ts:1035` `confirmDeleteState` / `1017` `handleDeleteKey` / `confirmDeleteSelection` | **不改**。走 `rowState(row)` / `isAgentBusy`,自动泛化 |
220
- | `service.markCompletedMany` | **不改**。只跳过 `completed` |
221
- | `renderHelp`(`:1648`)+ hint 行(`:1316`) | 加 `h hold / unhold` 条目 |
222
- | `HeaderCounts` / `headerStageSummary` / `index.ts:93-99` | **不改** |
223
- | `src/runtime/service.mjs` | 新增 `holdView(viewId)` / `clearHoldView(viewId)`,模板 `completeView`(`:600`):本地 `isAgentBusy` 快速预检查 + 提交命令 + `coordinator_disabled` 时的 `*Direct` 回退 |
224
- | `src/ui/dashboard.ts` | 新增 `toggleHold()` |
225
-
226
- 新 UI 动作 `toggleHold()`:
227
- - 当前状态是 `holding` → 发 `clear_holding`,notice「Resumed — needs instructions」
228
- - 否则 → 发 `mark_holding`,notice「On hold」
229
- - `isAgentBusy` → notice「Wait for the active run to finish before placing on hold」(与 `confirmDone` 同款措辞风格)
230
-
231
- ### 4.9 降级契约(`AGENT_BOARD_COORDINATOR=off` 逃生门)
232
-
233
- - `holdView`/`clearHoldView` 需要 `holdViewDirect`/`clearHoldViewDirect` 直写 helper(与 `completeView` 的 `coordinator_disabled` 回退分支 `service.mjs:618-621` 并列),并在 `test/architecture-writer-boundary.test.mjs` 的 `WRITE_STATE_ALLOWLIST` 说明文字里归入既有的 coordinator_disabled 豁免条目(该文件的既有条目文字已概括 `*Direct` helpers,机制上不需要扩展,但文字保真度需要确认)。
234
- - `job-runner.mjs:235/240` 的 legacy 直写守卫改用 `isManualVerdict`(宽语义)。
235
- - **解封在直写模式下天然无操作**:`syncForegroundState` 的 `coordinator_disabled` 分支直接 `writeState(root, projected)`(`:772`),本来就不经过 fence。行为与 0.9.0 一致,只做回归验证。
236
-
237
- ## 5. 数据流
238
-
239
- ### 5.1 搁置写入
240
-
241
- ```
242
- dashboard 按 h
243
- → service.holdView(viewId)
244
- → isAgentBusy(row) 本地预检查(快,给即时 UI 反馈)
245
- → sendStateCommand(mark_holding, source="dashboard-user")
246
- → validateCommand(kind 在 STATE_COMMAND_KINDS)
247
- → decideStateTransition
248
- unknown_view → revision_conflict → stale_run → manual_fence(用户来源直接过)
249
- → case "mark_holding": source 守卫 → busy 守卫 → apply
250
- → journal append + fsync → materialize({...state, ...mutate.state})
251
- → notice + refresh
252
- ```
253
-
254
- ### 5.2 说话即恢复(F-lift)
255
-
256
- ```
257
- 用户在 attach 会话里敲字 / reply 注入到存活 host
258
- → pty-runner: child.write(data) → 子 Pi stdin
259
- → 子 Pi 触发 pi.on("input") { source: "interactive" }
260
- → src/index.ts syncForeground → service.syncHostedEvent / syncForegroundEvent
261
- → syncRowEvent(row, event)
262
- reduceEvidence 直写(不受 fence 影响)
263
- event.type === "input" && event.source === "interactive"
264
- → status 置 working / alive / "Running…"
265
- → writeForegroundState(row, status, { source: "dashboard-user" })
266
- projectViewState → sync_foreground(source="dashboard-user", runId=null)
267
- → 通用 manual_fence 守卫放行(用户来源)
268
- → materialize:semanticState 变 "working" → 行脱离 fence
269
- → 后续 tool/message/agent_end 事件照常镜像(行已 unfenced)
270
- ```
271
-
272
- ## 6. 改动清单
273
-
274
- ### 词汇层(机械,多数静默失败)
275
-
276
- - `src/core/types.mjs`:`SemanticState:6`、`EvidenceOutcome:14`、`SEMANTIC_STATES:40`、`GROUP_ORDER:53`、`GROUP_LABELS:64`
277
- - `src/core/rows.mjs`:`stateGlyph:67`、`stateColor:93`(**两者都有 `default` 兜底,漏改不报错**)
278
- - `src/core/derive.mjs`:`GENERIC_STATUS_TEXT:16`(加入 `holding: new Set(["On hold"])`,遵循该表只增不减的兼容模式)、`fallbackStatusText:46`
279
- - `src/core/evidence.mjs:223-224`:`outcome`、`ready`
280
- - `src/ui/dashboard.ts:1920-1933`:`STAGE_RGB`(唯一编译期护栏)
281
-
282
- ### 锁层(高风险)
283
-
284
- - `src/core/auto-state.mjs`:`isManualCompletion:200` → `isManualVerdict`;`:214`、`:239` 两个调用点
285
- - `src/core/state-commands.mjs`:`DECIDED_REJECT_REASONS:71`(加 `source_not_allowed`)、`STATE_COMMAND_KINDS:36`(加两个 kind)、泛化守卫 `:280`、新增两个 kind 分支(模板 `mark_completed:284-304`)
286
- - `src/core/coordinator-protocol.mjs:13`:`COORDINATOR_PROTOCOL_VERSION` 2 → 3
287
- - `runner/state-coordinator.mjs:72`:`LAST_ACTIVITY_STAMP_KINDS` 加 `mark_holding`、`clear_holding`
288
- - `src/runtime/service.mjs`:新增 `holdView`/`clearHoldView`(模板 `completeView:600`)+ 两个 `*Direct` helper;`syncRowEvent:1444` 的来源判定;`writeForegroundState:745` 增加 source 参数;`canAutoDrain:2278` 迁出
289
- - `src/core/warm-host-sweeper.mjs`:接收 `canAutoDrain`
290
- - `runner/job-runner.mjs`:8 个调用点(`:187`、`:235`、`:240`、`:471`、`:501`、`:659`、`:680`、`:714`)
291
- - `runner/state-runner.mjs:34`
292
-
293
- ### UI / 文档
294
-
295
- - `src/ui/dashboard.ts`:`toggleHold`、`handleListKey`、`handlePeekKey`、`handleSessionKey`、`renderHelp`、hint 行
296
- - `README.md`:状态表(`:204-213`)、主列表键位表、`d`/`X`/多选的说明段、过滤示例
297
- - `CHANGELOG.md`:由 release 工具生成,本 PR 不手改
298
-
299
- ### 测试
300
-
301
- - `test/state-commands.test.mjs`(决策层,最大改动面)。注意 `:53-58` 是 `STATE_COMMAND_KINDS` 快照断言,加两个 kind 必须同步更新
302
- - `test/auto-state.test.mjs`(谓词真值表)
303
- - `test/rows.test.mjs`(glyph/color/过滤别名/分组顺序)
304
- - `test/derive.test.mjs`(`fallbackStatusText`/`GENERIC_STATUS_TEXT`)
305
- - `test/evidence.test.mjs`(outcome/ready)——**issue 正文未列,本次补**
306
- - `test/warm-host-sweeper.test.mjs`(`isAgentBusy` + `canAutoDrain`)——**issue 正文未列,本次补**
307
- - `test/service.test.mjs`(`holdView`/`clearHoldView`/解封路径)
308
- - `test/host-input.test.mjs`(`canAutoDrain` 门禁回归)——**issue 正文未列,本次补**
309
- - `test/state-coordinator.integration.test.mjs`(A8 回归 + 新 kind 的真实协调器路径)
310
- - `test/runner.integration.test.mjs`(post-exit 不覆盖、follow-up 不续跑)
311
- - `test/dashboard-navigation.test.mjs`(`h` 键接线,A21;复用 `test-support/navigation-wrap.ts` 探针基座)——**issue 正文未列,评审补**
312
- - `test/coordinator-client.test.mjs`(协议版本 bump 的既有断言自动适配,需确认)
313
-
314
- ## 7. 风险与缓解
315
-
316
- | 风险 | 后果 | 缓解 |
317
- |---|---|---|
318
- | **fence 泛化漏掉一个调用点** | 用户搁置/完成后几十秒,迟到的自动分类把它改回去,且无报错 | D3 全宽化 + D15 改名(旧函数不存在 ⇒ 漏改会 typecheck 失败而非静默)。这是本次最大的风险,也是「改名而非并存」的主要理由 |
319
- | **协议版本未 bump** | 旧 coordinator 静默 `unknown_kind` 拒收,holding 写入永久失败(#108 复现) | 列为硬性改动项;A 矩阵加一条断言 `COORDINATOR_PROTOCOL_VERSION >= 3` |
320
- | **解封信号被误用** | 程序化注入解除用户搁置 | 只认 `source === "interactive"`;A 矩阵补 `rpc`/`extension` 反例 |
321
- | **解封削弱 A8 不变量** | 迟到的自动分类重新能覆盖人工判定 | 解封只换 `sync_foreground` 的来源,`sync_foreground` 不携带 autoState 分类结果;A8 既有用例作为回归门禁(A5) |
322
- | **`canAutoDrain` 迁址引入循环依赖** | 构建失败 | `warm-host-sweeper.mjs` 已被 service.mjs 依赖,方向不变 |
323
- | **过滤别名碰撞** | `s:h` 命中歧义 | 已分析:现有 7 状态无一以 h 开头;D6 标签 `On hold` 归一化 `onhold` 无碰撞 |
324
- | **与 attach 子系统既有「hold」术语撞名** | 注释/诊断里把 shutdown-and-hold jiggle 协议与搁置状态混为一谈,排障时误导 | 4.7 的命名消歧约定;列为 CR 检查项 |
325
- | **F-lift 在自动投递的 follow-up 上也触发**(评审发现,见 §4.4) | 用户标完成的行因排队 follow-up 自动从 Done 翻成 Running,可能被视为「状态自己变了」 | 已论证是对既有不一致的修正(host-less 路径今天就会经 `mark_queued` 解封);U7 的对照步骤实测确认 |
326
-
327
- ## 8. 可测性拆分设计(自动化验证类功能点)
328
-
329
- | 功能点 | 独立单元 | 测试边界 |
330
- |---|---|---|
331
- | 人工判定谓词 | `isManualVerdict(state)` — 纯函数,导出 | 真值表:`{completed, holding} × {autoState null / 非 null} × {其他 5 状态} × {null/undefined}` |
332
- | 命令决策 | `decideStateTransition(command, state, status, now)` — 已是纯函数,零 I/O | 构造 command/state 直接断言 `{action, reason, mutate}`;守卫顺序用「同时满足多个拒绝条件」的用例锁定 |
333
- | 来源守卫 | 同上(`source_not_allowed` 分支) | 对**未 fence** 的行发非用户来源的 `mark_holding` → 仍拒绝(证明不是靠 fence 顺带拦住) |
334
- | 分类器不产出 holding | `semanticStateForAutoKind(kind)` — 纯函数 | 遍历 `AUTO_STATE_KINDS` 断言返回值永不为 `holding` |
335
- | busy 契约 | `isAgentBusy(row)` — 纯函数,已导出 | holding 行为 `false`;`idle`/`completed`/`failed`/`stopped` 在 `alive: false` 下同样为 `false`(原语义不回归) |
336
- | 续跑门禁 | `canAutoDrain(row)` — 纯谓词(D16 提取并导出) | 6 个语义状态 × `alive` 真/假的矩阵断言;holding 恒为 `false`;`idle`/`completed` 非 busy 时恒为 `true`(防白名单被改成黑名单) |
337
- | 渲染词表 | `stateGlyph` / `stateColor` / `fallbackStatusText` — 纯函数 | 断言 holding 的返回值**不等于** `default` 分支值(`"?"` / `"text"` / `undefined`),把「静默失败」变成显式断言 |
338
- | 编译期护栏 | `STAGE_RGB satisfies Record<keyof typeof GROUP_LABELS, ...>` | `npm run typecheck`;另加一条断言 `COORDINATOR_PROTOCOL_VERSION >= 3` |
339
- | evidence 契约 | `finalizeEvidence(snapshot, status, now)` — 纯函数 | 构造 status 断言 outcome/ready |
340
- | 过滤别名 | `parseFilter(query)` — 纯函数 | `s:holding`/`s:hold`/`s:onhold` 各只命中 holding;反向断言不命中其他 7 状态 |
341
- | 服务层动作 | `createService(opts)` 注入 fake `sendStateCommand` | 断言提交的命令 kind/source/payload 形状;`coordinator_disabled` 分支走 `*Direct` |
342
- | 解封集成 | 真实 coordinator(复用 `startTrackedCoordinator` + `waitFor` 基座) | 见 A3/A4/A5/A6 |
343
- | 直写模式不回归 | `service(root, { coordinatorDisabled-ish })` 或注入 | 既有 `syncForegroundEvent` 用例全部保持绿 |
344
-
345
- 关键:**解封的集成用例必须能红**。做法是用「fenced 行 + interactive input 事件」构造场景,在未实现 F-lift 时该用例必然得到 `manual_fence` 拒绝与冻结的行状态,实现后收敛到 `working`——红/绿自证,不依赖时序竞态、非 flaky。
346
-
347
- ## 9. 验收矩阵
348
-
349
- ### 自动化验证
350
-
351
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
352
- |----|--------|----------|----------|----------|
353
- | A1 | `isManualVerdict` 真值表 | unit | `node --test test/auto-state.test.mjs` | holding/completed × autoState 的 4 组合为 true/false 正确;其余 5 状态恒 false;`null`/`undefined` 为 false |
354
- | A2 | `mark_holding` 决策 | unit | `node --test test/state-commands.test.mjs` | 非 busy 行 apply,mutate 字段集与 `mark_completed` 逐字段同形 + `semanticState: "holding"` + `autoState: null`;`processState: "alive"` → `reject("busy")` |
355
- | A3 | `clear_holding` 决策 | unit | 同上 | holding 行 apply → `semanticState: "idle"`;非 holding 行 → `reject("no_change")`;alive → `reject("busy")` |
356
- | A4 | 来源守卫(独立于 fence) | unit | 同上 | 对**未 fence** 的 idle 行发 `source: "job-runner"` 的 `mark_holding`/`clear_holding` → `reject("source_not_allowed")`;`source_not_allowed` ∈ `DECIDED_REJECT_REASONS` |
357
- | A5 | fence 泛化 · 决策层 | unit | 同上 | 非用户来源的任意 kind 撞 holding 行 → `reject("manual_fence")`;用户来源同一命令放行 |
358
- | A6 | fence 泛化 · 分类器 | unit | `node --test test/auto-state.test.mjs` | `applyAutoStateToViewState`/`applyAutoStateToStatus` 对 holding 行返回 `false` 且不改字段 |
359
- | A7 | 分类器永不产出 holding | unit | 同上 | 遍历 `AUTO_STATE_KINDS`,`semanticStateForAutoKind` 返回值永不为 `holding`;`STATE_COMMAND_KINDS`/`AUTO_STATE_KINDS` 词表快照 |
360
- | A8 | busy / 续跑门禁 | unit | `node --test test/warm-host-sweeper.test.mjs test/host-input.test.mjs` | `isAgentBusy(holding) === false`;`canAutoDrain(holding) === false`;`canAutoDrain(idle 非 busy) === canAutoDrain(completed 非 busy) === true`(白名单未被改成黑名单) |
361
- | A9 | 渲染词表无静默兜底 | unit | `node --test test/rows.test.mjs test/derive.test.mjs` | `stateGlyph`/`stateColor`/`fallbackStatusText` 对 holding 的返回值不等于各自 default 分支值 |
362
- | A10 | 分组顺序与标签 | unit | `node --test test/rows.test.mjs` | `GROUP_ORDER` 中 `idle < holding < completed`;`GROUP_LABELS.holding === "On hold"`;holding 行落在独立分组 |
363
- | A11 | 过滤别名 | unit | 同上 | `s:holding`/`s:hold`/`s:onhold`/`s:on` 各只命中 holding;均不命中其他 7 状态 |
364
- | A12 | evidence 契约 | unit | `node --test test/evidence.test.mjs` | holding → `outcome === "holding"`、`ready === true`;completed/idle 既有取值不变 |
365
- | A13 | 协议版本 | unit + build | `node --test test/coordinator-client.test.mjs` + `npm run typecheck` | `COORDINATOR_PROTOCOL_VERSION >= 3`;pong 断言自动适配新版本 |
366
- | A14 | 解封 · 集成(**红/绿自证**) | integration | `node --test test/state-coordinator.integration.test.mjs` | fenced holding 行 + `input{source:"interactive"}` → 行收敛 `working`,且 `diagnostics.jsonl` 无 `manual_fence` 拒绝记录;**未实现 F-lift 时该用例必失败** |
367
- | A15 | 解封 · 反例 | unit + integration | 同上 + `test/service.test.mjs` | `input{source:"rpc"}` / `input{source:"extension"}` / `before_agent_start` 撞 fenced 行 → 仍 `manual_fence` 拒绝,行保持 holding |
368
- | A16 | 解封 · 既有 completed 回归 | integration | `node --test test/service.test.mjs` | Done 行 + interactive input → 解封并收敛 `working`(证明 F-lift 对所有人工判定生效,非仅 holding) |
369
- | A17 | A8 不变量不回归 | integration | `node --test test/state-coordinator.integration.test.mjs` | 既有「迟到 `auto_state_classified` → `manual_fence`」用例(`:308-310`、`:542-577`)保持绿;coordinator 重启后 journal 重放仍拒 |
370
- | A18 | runner 侧不覆盖 / 不续跑 | integration | `node --test test/runner.integration.test.mjs` | post-exit pass 不覆盖 holding;holding 行不被 `drainQueuedFollowUp` 自动续跑 |
371
- | A19 | 直写模式不回归 | unit | `node --test test/service.test.mjs` | `coordinator_disabled` 下 `syncForegroundEvent` 行为与 0.9.0 一致;`write_state` 架构守卫(`test/architecture-writer-boundary.test.mjs`)保持绿 |
372
- | A20 | 全量回归 | build | `npm run verify` | typecheck + perf gate + tests + coverage 阈值(lines 85 / funcs 80 / branches 70)+ pack:dry 全绿 |
373
- | A21 | `h` 键接线(评审补) | unit | `node --test test/dashboard-navigation.test.mjs`(新增用例,复用 `test-support/navigation-wrap.ts` 探针基座注入 fake service) | list/peek/session 三模式按 `h` 各自提交 `mark_holding` / `clear_holding`;busy 行只给 notice 不发命令;`isPrintable` 兜底不再拦截 `h` |
374
-
375
- ### 用户实测
376
-
377
- | ID | 功能点 | 验收方式 | 操作步骤 | 观察结果 / 通过标准 |
378
- |----|--------|----------|----------|---------------------|
379
- | U1 | `h` 置搁置 | 用户实测 | 在面板 list 选中一个非 busy 行,按 `h` | 行进入 **ON HOLD** 分组(位置在 NEEDS INSTRUCTIONS 之后、DONE 之前);顶栏与 footer 计数不变;notice 提示已搁置 |
380
- | U2 | `h` 解除搁置 | 用户实测 | 对 ON HOLD 行再按 `h` | 行回到 **NEEDS INSTRUCTIONS** 分组;notice 提示已恢复 |
381
- | U3 | 说话即恢复(holding) | 用户实测 | 对 ON HOLD 行按 `→` attach,在真实会话里敲一句话提交 | 行变为 **RUNNING**;该行 `diagnostics.jsonl` **不再新增** `sync_foreground rejected (manual_fence)` |
382
- | U4 | 说话即恢复(既存回归) | 用户实测 | 对 **DONE** 行 attach 后敲字提交 | 同上(行变 RUNNING、无新 manual_fence 拒绝)。这条是本 PR 修的既有 bug,与 holding 无关也应通过 |
383
- | U5 | `d` 转真完成 | 用户实测 | 对 ON HOLD 行按 `d` 并确认 | 行进入 **DONE** 分组;`state.json` 的 `semanticState === "completed"` |
384
- | U6 | 搁置行可删 | 用户实测 | 对 ON HOLD 行按 `X` | 该状态全部 inactive 行被归档;live 行跳过 |
385
- | U7 | 搁置行 + follow-up 不自动唤醒(含 Done 行对照) | 用户实测 | 给 ON HOLD 行排队一条 follow-up(先使其 busy 时 reply,或 `delivery: "queue"`),再置搁置,等待;**对照**:另一条 Done 行同样排队 follow-up 且 host 存活,等待 reconcile | 主行保持 ON HOLD,不被自动唤醒,队列计数保留;对照行 follow-up 被投递且行变 RUNNING(§4.4 自动投递交互的实测) |
386
-
387
- **验收纪律**:U 项未执行前不得宣称验收完成;自动化项通过不能替代 U 项。
388
-
389
- ## 10. 开放问题
390
-
391
- 1. **`STAGE_RGB.holding` 的具体色值**:建议 `[251, 191, 36]`(琥珀系,与 needs_input 的橙 `[245,158,11]` 区分)。属视觉偏好,可在实现期调整。
392
- 2. **U7 的可操作性**:构造「holding + 排队 follow-up」需要特定前置(该行存在 queued follow-up 且进程已退出)。若实测难以构造,降级为 A18 覆盖 + 标记 U7 为 pending。
393
- 3. **`test/architecture-writer-boundary.test.mjs` 的说明文字**:新增两个 `*Direct` helper 后,该文件的豁免条目文字是否需要在注释里枚举新名称(机制上不需要,文字保真度需要)。
394
- 4. **`canAutoDrain` 迁址 vs 原地导出**:D16 选了迁址。若审阅认为模块名拉伸不可接受,退回「在 service.mjs 原地 `export function canAutoDrain`」——测试边界不变,只是归宿不同。