@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,204 +0,0 @@
1
- # Spec — Issue #121: 性能门禁脱离覆盖率插桩(A11)
2
-
3
- > 状态:**已确认**(2026-09-20 会话确认;同目录调研件保留在 `~/.claude/github-issue-driven/zhuxixi/pi-agent-board/issue-121/`)
4
- > 认领:zhuxixi(2026-09-14);R3 复核于 2026-09-20 HEAD `794c755`
5
- > 调研:`~/.claude/github-issue-driven/zhuxixi/pi-agent-board/issue-121/research/01-prior-art-and-precedent.md`、`02-measurements.md`、`03-revalidation-at-head.md`
6
- > 上游:issue #121(本 spec 的目标);#95(真实进程预算与串行化,**不在本 spec 范围**);#31/PR #32(CI 结构来源);#117(性能用例引入方)
7
-
8
- ## 1. 问题陈述
9
-
10
- `main` 连续四次 CI 红,失败点全部是 **Coverage 步骤里的 A11 性能断言**,而同一次 run 的 Unit tests 步骤全绿:
11
-
12
- ```
13
- not ok 809 - A11: paced stream (80ms interval, ~5s wall) stays within thresholds
14
- error: 'paced feed p99 11.379ms exceeds 8ms'
15
- ```
16
-
17
- `main` 启用了分支保护(必需检查 `Test (Node 22)` / `Test (Node 24)`,`enforce_admins: true`),所以这道红持续卡住所有后续 PR。
18
-
19
- 目标:让性能门禁**继续真实生效**,但只在**它能产生有效测量的环境**里决定 CI;把无效环境(覆盖率插桩、并行争抢)下的性能断言从判定路径上移除。
20
-
21
- ## 1.5 根因(已完成,含实测证据;不再重复调试)
22
-
23
- 见 `research/02-measurements.md` 与 `research/03-revalidation-at-head.md`。结论两条:
24
-
25
- 1. **插桩路径是无效测量环境(R3 收紧后的表述)**:同一份代码在 Coverage 步骤测到 `paced p99` = 1.541ms / 3.838ms(绿)与 **11.379ms**(红),跨度 7.5×。R3 在 HEAD `794c755` 上用**单文件、机器空闲、无并行套件竞争**的 c8 运行复现了失败——5 次中 3 次超 8ms(**13.904** / 6.057 / 6.295 / **10.755** / **13.221**),同期无插桩 4 次为 2.376 / 2.073 / 2.659 / 2.107ms(余量 3.0–3.9×,很稳)。→ **插桩本身就是判定失败的充分项,CI 并行只是放大器**;8ms 在插桩下测的是 c8 的采样开销,不是终端模型的代码开销。对照 09-14 本机插桩实测(3.1–6.5ms,4 次全过),余量正在收窄,「偶尔侥幸通过」就是当前形态。
26
- 2. **阈值本来就不是为 CI 标定的**:`#117` 的 plan 原文写的是 `… (80×24 + 256 scrollback **on dev hardware**)`,且该 plan 已预设处置规则:`if CI shows systematic misses, **re-baseline with evidence (not silently relax)**`。本 spec 的设计约束即此句。
27
-
28
- 附带事实(影响设计):
29
- - 性能用例自 #117 起在 CI 每次运行被执行**两次**(Unit tests + Coverage),且其负载已被证实会拖慢套件内其他真实进程测试(plan 记录:为它把 `pty-runner.integration` 的 `waitFor` 默认值从 3s 提到 10s)。
30
- - 把性能用例排除后,覆盖率**仍远离阈值**(实测 Lines 92.2 / Funcs 91.16 / Branch 79.58,阈值 85/80/70;`terminal-model.mjs` 仍为 100%)→ 「排除会掉阈值」的顾虑已被实测否定。
31
-
32
- ## 2. 设计决策
33
-
34
- ### D1:性能断言只在专用 perf 入口里决定 CI(**opt-in 门禁**)
35
-
36
- 性能断言默认**不测量**,仅在显式开启时测量:
37
-
38
- - 新环境变量 `AGENT_BOARD_PERF_GATE=1` 表示「允许性能断言决定结果」。
39
- - 未开启时,三个性能用例以 `{ skip: reason }` 跳过(reason 里写明入口 `npm run test:perf`),**不执行测量循环**(因此也不贡献 CPU 负载)。
40
- - 新入口 `npm run test:perf` 是唯一权威执行路径。
41
-
42
- **为什么 opt-in 而不是 opt-out**(备选方案对照):
43
-
44
- | 方案 | 优点 | 为什么不选 / 代价 |
45
- |---|---|---|
46
- | **opt-in(选定)** | 测量环境受控;默认套件不含性能负载;语义明确「只有专用入口才是判定」 | 本地 `npm test` 不再顺带守性能 → 由 `verify` + CI step 兜住 |
47
- | opt-out(默认测,CI 覆盖率步骤关掉) | 本地默认守性能 | CI 的 Unit tests 步骤仍在并行环境里判定同一阈值 → 噪声问题原样保留;且默认路径依旧把性能负载压给其他测试 |
48
- | 放宽阈值 | 改动最小 | **被 plan 明文禁止**(not silently relax);放宽到噪声带之上等于失去回归检出能力 |
49
- | `--test-skip-pattern` 按名字排除 | 不动测试文件 | 名字耦合(改名即静默失效);只治 CI,不治本地 `npm run test:coverage` |
50
-
51
- ### D2:覆盖率与并行套件都不测量性能(不改 glob)
52
-
53
- `test` / `test:coverage` 两个脚本**保持原样**(`node --test test/*.test.mjs`)——因为 D1 的门禁会让性能断言在未 opt-in 时自然跳过。这样:
54
-
55
- - 覆盖率步骤不再被性能断言判定;
56
- - 并行套件不再承担性能用例的 CPU 负载(消除 #117 已实测过的那类连带 flake);
57
- - 不改文件路径 → **不使 #91 的 A11 验收行、plan、VERIFY.md 中已记录的命令路径失效**,也把与 #95 的 `ci.yml` 冲突面缩到「新增一个 step」这一处。
58
-
59
- ### D3:权威测量跑在「独立 step、单文件、串行」环境
60
-
61
- CI 新增一个 step(**不新增 job**,避免改分支保护配置):
62
-
63
- ```yaml
64
- - name: "Perf gate (serial, no coverage)"
65
- run: npm run test:perf
66
- ```
67
-
68
- - 位置:`Typecheck` 之后、`Unit tests` 之前——此时机器最安静(本 job 尚无可争抢的测试进程),且失败快速暴露。
69
- - step 内只跑 `test/terminal-model-perf.test.mjs` 一个文件;**首轮不携带 `--test-concurrency=1`**——该 flag 需 Node 21+,而 `package.json` 声明 `engines: node>=20`,且单文件时它是 no-op(2026-09-20 用户裁决:YAGNI,删掉;将来新增第二个 perf 文件时加回,并行测量必须保持不可能)。
70
- - **升级路径(写进 plan 的残留账)**:若该 step 在连续 5 次运行中仍抖动 → (a) 先按 D4 用证据重设界;(b) 仍不行才升级为独立 job(独立 runner)并把它加入必需检查(需改分支保护,属人工配置步骤)。
71
-
72
- ### D4:基线按证据重设,禁止静默放宽
73
-
74
- - 首轮**维持现有数值**(feed p95 ≤ 5ms / p99 ≤ 8ms、capture ≤ 50ms、hydrate ≤ 100ms)。
75
- - perf step 必须保留现有 `burst:` / `paced:` 数值日志行(趋势证据,本次已存在,不新增打印)。
76
- - 规则:**任何阈值调整必须附证据**——至少包含连续 N≥5 次专用 step 的实测分布,且新界需高于实测噪声上限并留有 ≥2× 系数;调整记录写回本 issue。禁止出现「阈值悄悄变大」的 diff。
77
-
78
- ### D5:`NODE_V8_COVERAGE` 作为机械兜底门禁
79
-
80
- c8 会给子进程设置 `NODE_V8_COVERAGE`(本机实测值 `<repo>/coverage/tmp`,与 KB 笔记 `202608250830231905` 一致)。因此:
81
-
82
- - 门禁判定为**两个环境变量的纯函数**:即使有人开了 `AGENT_BOARD_PERF_GATE=1`,只要 `NODE_V8_COVERAGE` 存在就**拒绝测量**(reason 写明「正在被覆盖率插桩」)。
83
- - 入口脚本在此情形下**不静默跳过,而是响亮失败**(退出码非 0 + 明确错误),避免「在插桩下跑 perf 门禁得到全绿」这种假信号。
84
-
85
- ## 3. 组件契约
86
-
87
- ### 3.1 门禁纯函数(新文件 `test-support/perf-gate.mjs`)
88
-
89
- `test-support/**` 已在 `.c8rc.json` 的 exclude 中,不影响覆盖率口径。
90
-
91
- ```js
92
- /**
93
- * @param {Record<string,string|undefined>} env
94
- * @returns {{run: boolean, reason: string}}
95
- */
96
- export function perfGateDecision(env = process.env)
97
- ```
98
-
99
- 真值表(域:`AGENT_BOARD_PERF_GATE` ∈ {未设置, `"1"`, `"0"`, 其它值} × `NODE_V8_COVERAGE` ∈ {未设置, 已设置} = **8 种组合**;下表按规则合并成 5 行,A1 必须覆盖全部 8 种):
100
-
101
- | `AGENT_BOARD_PERF_GATE` | `NODE_V8_COVERAGE` | `run` | `reason` 要点 |
102
- |---|---|---|---|
103
- | 未设置 | 未设置 | false | 需要 `AGENT_BOARD_PERF_GATE=1`(指向 `npm run test:perf`) |
104
- | 未设置 | 已设置 | false | 同上(插桩不改变「未 opt-in」的判定) |
105
- | `"1"` | 未设置 | **true** | 空 |
106
- | `"1"` | 已设置 | false | 正在被覆盖率插桩,拒绝测量 |
107
- | `"0"` / 其它值 | 任意 | false | 需要 `AGENT_BOARD_PERF_GATE=1` |
108
-
109
- ### 3.2 入口脚本(新文件 `scripts/run-perf-gate.mjs`)
110
-
111
- - 副作用隔离层:只做「读环境 → 判定插桩 → 执行或拒绝」,**不含**测量/断言逻辑,也**不**调用 `perfGateDecision` 做静默跳过——入口是 opt-in 路径,进来的人就是来测量的。
112
- - 分层契约(硬约束):`perfGateDecision` 只服务**测试文件**(决定用例跳过与否);入口脚本只做**插桩检测**——`NODE_V8_COVERAGE` 存在 → 打印错误并 `exit 1`(响亮失败,**不**清除该变量后照跑)。若实现成「调纯函数得 run=false → 静默 exit 0」,插桩下的 perf 门禁就会假绿,这正是 D5 要堵的洞。
113
- - 否则 `spawn(process.execPath, ["--test", "--test-concurrency=1", "test/terminal-model-perf.test.mjs"], { stdio: "inherit", env: { ...process.env, AGENT_BOARD_PERF_GATE: "1" } })` 并透传退出码。
114
- - 跨平台:脚本内设置环境变量,**不用** npm 脚本里的内联 `VAR=1 cmd` 写法(Windows cmd 不支持;本仓库有 Windows 支持面)。
115
-
116
- ### 3.3 性能测试文件(改 `test/terminal-model-perf.test.mjs`)
117
-
118
- - 顶层求值 `const gate = perfGateDecision(process.env)`,三个用例各自 `test(name, { skip: gate.run ? false : gate.reason }, fn)`。
119
- - 阈值常量(`FEED_P95_LIMIT` 等)**留在本文件**,不下移到 helper:它们是被测契约,helper 只决定「是否测量」。
120
- - 断言与测量逻辑其余部分不动(含 ring overflow 用例:它无延迟断言,但因同文件跳过而一并跳过——记录为已知取舍,见 §6)。
121
-
122
- ### 3.4 脚本与 CI(改 `package.json`、`.github/workflows/ci.yml`)
123
-
124
- ```json
125
- "test": "node --test test/*.test.mjs", // 不变
126
- "test:coverage": "c8 node --test test/*.test.mjs", // 不变
127
- "test:perf": "node scripts/run-perf-gate.mjs", // 新增
128
- "verify": "npm run typecheck && npm run test:perf && npm test && npm run test:coverage && npm run pack:dry"
129
- ```
130
-
131
- `verify` 把 perf 放在覆盖率之前(先安静测量,再插桩跑全量),并保持与 CI 步骤顺序一致。
132
-
133
- ## 4. 可测性拆分设计(硬约束,实现阶段不得重新耦合)
134
-
135
- | 单元 | 性质 | 怎么测 | 覆盖的验收项 |
136
- |---|---|---|---|
137
- | `perfGateDecision(env)` | **纯函数**,无副作用、不读全局 | 直接传 env 对象遍历真值表 | A1 |
138
- | `scripts/run-perf-gate.mjs` | **副作用层**(spawn / env / 退出码),不含决策逻辑与测量 | 用 `child_process` 注入 `NODE_V8_COVERAGE` 验证拒绝路径(A2);正常路径由 A3 端到端验证 | A2 / A3 |
139
- | `test/terminal-model-perf.test.mjs` | 消费门禁 + 执行测量与断言 | 跳过路径由 A4 验证(默认套件汇总里出现 skipped 与 reason);测量路径由 A3 验证(真实数值行 + 3 passed) | A3 / A4 |
140
- | CI 接线与脚本契约 | 静态 | 读 `ci.yml` / `package.json` 断言关键行(A5) | A5 |
141
-
142
- 边界声明:**决策(纯)↔ 执行(副作用)↔ 测量(断言)三层不得合并**;不允许把 `perfGateDecision` 内联回测试文件,也不允许把阈值下移到 helper。
143
-
144
- ## 5. 验收矩阵
145
-
146
- ### 自动化验证
147
-
148
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
149
- |---|---|---|---|---|
150
- | A1 | 门禁真值表 | 自动化(unit,新文件 `test/perf-gate.test.mjs`) | `node --test test/perf-gate.test.mjs` | §3.1 域的 **8 种组合**全部覆盖,`{run, reason}` 与表逐行一致(`reason` 只断言关键子串,不锁全文) |
151
- | A2 | 插桩下拒绝执行 | 自动化(unit/integration,同文件内 `child_process`) | 以 `NODE_V8_COVERAGE=/tmp/x` 跑 `node scripts/run-perf-gate.mjs` | 退出码 ≠ 0,stderr 含明确拒绝说明,且**没有**执行测量 |
152
- | A3 | 权威入口真实测量 | 自动化(integration) | `npm run test:perf` | 退出码 0;输出含 `burst:` 与 `paced:` 数值行;`pass 3 / fail 0 / skipped 0` |
153
- | A4 | 默认套件不再测量性能 | 自动化(static + 行为) | `npm test` 与 `npm run test:coverage` 各跑一次 + `rg "perfGateDecision" test/terminal-model-perf.test.mjs` | 3 个 perf 用例出现在 skipped 列表且 reason 指向 `npm run test:perf`(reason 可见性已实测:Node 24 spec reporter 与 Node 22 TAP 均显示 `# SKIP <reason>`);**不对 skipped 总数做硬断言**(套件将来新增其它 skip 属正常);无 perf 断言失败;grep 命中 |
154
- | A5 | CI 接线与脚本契约 | 自动化(static) | 读 `.github/workflows/ci.yml` + `package.json` | perf step 存在且位于 Unit tests 之前;`test`/`test:coverage` 两行为原值(glob 未改);`test:perf` 指向入口脚本 |
155
- | A6 | 覆盖率阈值仍过 | 自动化(build) | `npm run test:coverage` | 退出码 0;All files:Lines ≥ 85、Functions ≥ 80、Branches ≥ 70;R3 在 HEAD `794c755` 实测基线 92.46 / 91.41 / 80.07(Lines / Functions / Branches),排除 perf 文件后 941 pass / 0 fail |
156
- | A7 | 并行套件稳定性不倒退 | 自动化(多次运行) | `npm test` 连续 3 轮 | 3 轮全绿;记录每轮 `pass/fail/skipped` 计数。若出现失败,只允许落在既有 flake 台账(#95 家族、#122 引入的 A5 mid-stream 断言)内且逐条对账——凡涉及本改动触及文件(perf-gate / run-perf-gate / terminal-model-perf / package.json / ci.yml)的失败即不通过 |
157
- | A8 | 文档与脚本契约一致 | 自动化(static) | 读 `README.md` / `VERIFY.md` / `package.json` | README 的 `npm run verify` 说明行与 VERIFY.md §0 静态检查块均含 `npm run test:perf` 及「性能断言默认跳过、需专用入口」说明;`package.json` 的 `verify` 含 `test:perf` 且顺序在 `test` / `test:coverage` 之前 |
158
-
159
- ### 用户实测 / 合并后观测
160
-
161
- | ID | 功能点 | 验收方式 | 步骤与观察 | 通过标准 |
162
- |---|---|---|---|---|
163
- | U1 | 合并后 main 连续绿 | 用户实测(合并后 CI 观测) | `gh run list --branch main --limit 5`;对绿 run 执行 `gh run view <id> --log \| grep -E "burst:\|paced:"` | main 连续 ≥2 次绿,且 perf step 日志中能看到真实 `burst:` / `paced:` 数值行(证明门禁在跑而非 skip)。**前置条件:A5 mid-stream 断言缺陷(#122 引入的独立 issue,见 research/03 §四)也必须已合入——否则红与本 issue 无关,会误判验收失败。** |
164
- | U2 | 基线证据收集与判定 | 用户实测(合并后证据汇总) | 收集 perf step 连续 5 次运行的 `paced p99` 值,写入本 issue | 5 次全部通过 → 维持现阈值并记录分布;若仍抖动 → 按 D4 用证据重设界(或按 D3 升级为独立 job) |
165
-
166
- ## 6. 非目标
167
-
168
- - **不改阈值数值**(除非 U2/D4 触发,且必须附证据)。
169
- - **不碰 #95 范围**:真实进程用例的墙钟预算、并行套件串行化、A10 恢复链验证方式,均不在本 spec。
170
- - **不新增依赖**、不改生产代码(`src/`、`runner/`)。
171
- - **不新增 CI job、不改分支保护配置**(首轮;升级路径见 D3)。
172
- - **不引入 `--test-skip-pattern`**(理由见 D1 对照表第 4 行)。
173
- - 已知取舍(记录、不视为缺陷):ring overflow 用例无延迟断言,但同文件跳过 → 默认套件里它也不再跑;其行为由 `test/terminal-model.test.mjs` 与 `test/terminal-snapshot.test.mjs` 覆盖,A6 的实测证明本模块覆盖率不降。
174
- - 已知取舍(记录):`scripts/run-perf-gate.mjs` 会随 `package.json` 的 `files` 字段发布到 npm(`scripts/` 目录已有 `patch-vulns.mjs` / `release_helper.mjs` 的先例);它只被 `npm run test:perf` 调用,不影响扩展运行时。
175
-
176
- ## 7. 风险与回退
177
-
178
- | 风险 | 缓解 |
179
- |---|---|
180
- | opt-in 门禁被遗忘(有人删掉 CI step) | skip reason 写明入口;README/VERIFY.md 记录;A5 静态断言把接线钉在 `ci.yml` |
181
- | 覆盖率口径变化 | 已实测(§1.5 附带事实 + A6 基线),余量 ≥7 点 |
182
- | CI 上 perf step 仍抖动 | 两级升级路径(D3):先证据重设界,再独立 job + 必需检查 |
183
- | 本地 `npm run verify` 变重(+~7s)且机器繁忙时 perf 门禁可能 flake | R3 无插桩实测 2.07–2.66ms(≥3× 余量);真撞上按 D4 处理,不静默放宽 |
184
- | 与 #95 改同一 `ci.yml` 区域 | 两 issue 均已写明**串行合入**;本改动把冲突面压到「新增一个 step」 |
185
- | U1 被 A5 断言红挟持 | A5(`test/terminal-snapshot.integration.test.mjs:179`,测试断言过严)已认定为独立缺陷;其修复**必须同期合入**才能观测到「main 连续绿」,否则 U1 会误判。本 issue 自身的绿的判据是「perf step 通过 + Coverage 步骤不再判定性能断言」 |
186
- | Windows 兼容 | 入口用脚本设环境变量,不写内联 env;不新增依赖 |
187
-
188
- **回退**:本改动只涉及测试文件、新脚本、`package.json` 脚本、`ci.yml` 与文档,无数据迁移、无生产行为变化 → 回退 = revert 单个 PR。
189
-
190
- ## 8. 文档更新
191
-
192
- - `README.md`:`npm run verify` 说明行补上 perf 门禁(现文仅写 typecheck/tests/coverage/pack)。
193
- - `VERIFY.md` §0「Static checks」:补 `npm run test:perf`(并说明「性能断言默认跳过,需专用入口」)。
194
- - 本 issue 评论:U2 的基线分布与最终判定。
195
-
196
- ## 9. 迁移顺序(step 6 的 plan 将据此拆 task)
197
-
198
- 1. `perf-gate.mjs` 纯函数 + 真值表单测(A1)
199
- 2. `scripts/run-perf-gate.mjs` 入口脚本 + 拒绝路径测试(A2)
200
- 3. `test/terminal-model-perf.test.mjs` 接门禁、skip reason(A4 前半)
201
- 4. `package.json` 脚本与 `verify`(A3/A5 前半)
202
- 5. `ci.yml` 新增 perf step,置于 Unit tests 之前(A5 后半)
203
- 6. 文档(README / VERIFY.md)→ A8
204
- 7. 验收扫尾:A3 / A4 / A6 / A7 / A8 逐条执行并记录;提交 spec/plan 之外的证据到 issue
@@ -1,43 +0,0 @@
1
- # Spec: A5 mid-stream 断言窗口修正(issue #132)
2
-
3
- ## 根因(issue + 调研双确认,非猜测)
4
-
5
- `runner/pty-runner.mjs:342-345`:未处理完 `subscribe_terminal` 的 socket 仍收 legacy 广播行(同形 `{type:"output", seq, data}`)。`test/terminal-snapshot.integration.test.mjs` A5 mid-stream 测试(:241)把 socket B 收到的全部 output(含订阅前 legacy 行)当协议序列统计 → 头部混入一条订阅前行(seq 4);尾部追平(:227)只等 clientA 进度没等 socket B → 尾部少一条。两端叠加 → 负载敏感 CI 偶红。**不是协议违约**(A 路真实 client 全部断言通过)。
6
-
7
- ## 设计(单点修正,两处断言加固)
8
-
9
- ### D1 统计窗口限定(修法一,issue 首选)
10
-
11
- `:241` 改为只统计 `snapshot_begin` 之后到达的 output:`messagesB.filter(m => m.type === "output" && m.seq >= endB.nextSeq)`——窗口从 `endB.nextSeq`(快照后第一条协议输出)起,订阅前 legacy 行天然排除(legacy 行 seq 也来自同一条轴,但快照判定前的行 seq ≤ snapshotSeq < nextSeq,全部被过滤掉;注意 issue 样本里那条 seq=4 行 snapshotSeq=4,4 < nextSeq=5 ✓)。
12
-
13
- ### D2 尾部追平显式化(修法二)
14
-
15
- `:227` 的 `waitFor(clientA.getLastSeq() >= readyA.nextSeq + ticks)` 之后,加显式 `waitFor(seqsB_now.length >= ticks)`(socket B 自己收满 ticks 条协议 output),再断言连续。这样尾部缺口消失,不再借 clientA 进度推断 socket B 的追平状态。
16
-
17
- ### 不变式保留
18
-
19
- - 连续性断言形态不变(`seqsB === [nextSeq, nextSeq+1, ...]`——窗口限定后就是纯协议流,严格连续依然正确)
20
- - 至少 ticks 条断言保留(D2 等待保证)
21
- - A 路断言不动(本来全绿)
22
- - 30s 上限(A5 两个用例的 waitFor 天花板)不动——那是 #95 的负载层问题,本 issue 只修断言语义
23
-
24
- ## 验收矩阵
25
-
26
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
27
- |----|--------|----------|----------|----------|
28
- | A1 | 统计窗口限定 | 自动化验证(integration) | `node --test test/terminal-snapshot.integration.test.mjs` | A5 mid-stream 绿;断言语义变更为从 nextSeq 起(代码审查可证) |
29
- | A2 | 尾部显式追平 | 自动化验证(integration) | 同上 | waitFor 显式等 socket B 收满协议条数(代码审查可证) |
30
- | A3 | 负载下不再复现 | 自动化验证(stress) | 并行全量跑 3 轮 `node --test test/*.test.mjs`(高争用条件) | A5 mid-stream 不再出现 "subscriber seqs strictly contiguous, no dup" 失败 |
31
- | A4 | 既有断言语义保留 | 自动化验证(integration) | 全量 | 其余断言不变绿 |
32
-
33
- A3 的「3 轮并行全量」是概率验证而非根除证明(flake 本质如此),如实记录。若 3 轮里 A5 mid-stream 仍挂,说明窗口修正漏了路径,须回炉。
34
-
35
- ## 可测性拆分设计
36
-
37
- 本工单是测试自身的语义修正,没有新生产代码。可测性 = 断言本身即是测试。改动局限在单个 test case 体内,不动 helper/wireClient/runner。
38
-
39
- ## 非目标
40
-
41
- - 不动 runner 广播逻辑(协议 attach 判定后停写 legacy 行是另一设计决策,可能破坏 legacy 客户端在订阅判定期间看到输出的既有行为——#130 面板提案里相关,不在本 issue)
42
- - 不动 A5 burst 的 30s 上限、不动 subscribe_terminal 15s waitFor(#95 范畴)
43
- - 不动 subscribe/dedup 协议语义
@@ -1,95 +0,0 @@
1
- # issue #127 spec —— cwd 选择器 browse 模式 Tab 补全失效
2
-
3
- > 状态:**已确认**(2026-09-23 用户拍板:选项 B shell 式下钻;`~` 展开不纳入本次范围)
4
- > 调研留痕:research/01-kb-design-history.md、02-root-cause-code.md、03-test-infra.md(结论已评论到 issue)
5
-
6
- ## 根因报告(systematic-debugging Phase 1–3 结论)
7
-
8
- **直接原因**:`src/ui/dashboard.ts:598-608` `handleLaunchPickerKey()` 的 Tab 分支内层条件是
9
- `launch.cwdPickerMode === "favorites" && launch.cwdSuggestions.length > 0`。
10
- browse 模式下条件不成立,控制流落到分支末尾 `return`——Tab 键被消费但什么都不做。
11
-
12
- **促成原因**:`src/core/launch-options.mjs:358` `filterCwdCandidates()` 做原始子串匹配、不做 `~` 展开,
13
- 而收藏榜存绝对路径,所以任何 `~/...` 路径式输入必然零匹配,`nextCwdPickerState()`(:372)翻到 browse 模式。
14
- browse 的候选列表本身是对的(`listDirectorySuggestions()` 内部已 `expandHome`)——
15
- 用户看到「候选对、Tab 死」,正是这条链路。
16
-
17
- **可稳定复现**:结构性缺陷,无竞态——browse 模式下按 Tab 100% 走空 return。
18
- 修复前先写探针测试复现(Phase 4 要求)。
19
-
20
- **历史脉络**:browse 是 issue #22(PR #23)之前就有的旧能力;Tab 补全是 #22 只给 favorites 模式新加的,
21
- 旧模式漏接。设计语义应保持 #22 的决定:Tab = 补全进输入框,Enter 才确认。
22
-
23
- ## 关键设计发现(issue 建议 1 单独不成立)
24
-
25
- browse 补全后重算 `listDirectorySuggestions("/abs/dir")`,返回的 suggestions[0] 恒等于「目录自身」
26
- (代码里 `if (existsDir(resolved)) out.push(resolved)` 在最前)。若只做建议 1(补全 + 重算),
27
- 下一次 Tab 会补全到自身 = 无进展,**无法像 shell 一样连续 Tab 下钻**。
28
- issue 自己也意识到这点(建议 2「可选:追加路径分隔符或等价处理」)——结论是建议 2 不是可选,是下钻体验的必要组成。
29
-
30
- ## 设计方案(两个选项,待用户拍板)
31
-
32
- ### 选项 A:最小镜像(favorites 同款逻辑铺到 browse)——未采用
33
-
34
- Tab 分支去掉 `cwdPickerMode === "favorites"` 限制,两模式共用:
35
- 取 `cwdSuggestions[cwdSuggestionIndex] ?? cwdSuggestions[0]` 写入 cwdQuery,`nextCwdPickerState()` 重算,index 指向已补全项。
36
-
37
- - 效果:`~/wo` → Tab → `/home/elling/work`;此后 Tab 无进展(补全自身),下钻需 ↓ 选子目录再 Tab。
38
- - 优点:改动最小、语义与 favorites 完全一致、无新行为。
39
- - 缺点:不符合 issue 描述的「连续按 Tab 逐层下钻」预期。
40
-
41
- ### 选项 B:shell 式下钻(**已采用**)
42
-
43
- 在 A 的基础上加两条纯函数语义(browse 模式专属):
44
-
45
- 1. **无进展前进**:若高亮候选与当前 query 解析到同一目录(忽略尾部分隔符),Tab 循环前进到下一个候选。
46
- 2. **补全即到位**:browse 模式补全时给目录路径追加 `path.sep` 写入 query——
47
- 重算后列表第一项仍是该目录自身(Enter 可选它),连续 Tab 经规则 1 自然进入子目录,实现纯 Tab 下钻。
48
-
49
- - favorites 模式行为一字不动(只删模式守卫,不动其分支语义)。
50
- - 边界:候选只剩自身时下钻自然停止(循环回自身 = no-op);补全后若路径命中收藏榜子串,状态机自动翻回 favorites,可接受。
51
-
52
- ### 范围外(单独的改进,本 issue 不含)——已确认不纳入
53
-
54
- - issue 建议 3(收藏匹配前先 `~` 展开):正交的 UX 改进,改变模式翻转条件,建议另开 issue 或用户明确点名再加。
55
-
56
- ## 改动面(选项 B)
57
-
58
- 1. `src/core/launch-options.mjs`:新增两个纯函数(见可测性拆分)。
59
- 2. `src/ui/dashboard.ts`:
60
- - `handleLaunchPickerKey()` Tab 分支删模式守卫,browse 走新 helper(约 :598-608);
61
- - browse 底部提示(:1586)补 `tab complete`,与 favorites(:1582)对齐。
62
-
63
- ## 可测性拆分设计(实现硬约束)
64
-
65
- 新逻辑全部沉到 `launch-options.mjs` 纯函数层,dashboard 只做接线:
66
-
67
- | 函数 | 职责 | 副作用 | 测试方式 |
68
- |------|------|--------|----------|
69
- | `sameResolvedDir(a, b)` | 两路径串是否解析到同一目录(expandHome + resolve + 去尾部分隔符) | 无(纯字符串运算) | unit |
70
- | `browseTabCompletion(query, suggestions, index)` | 返回 `{ query, usedIndex }`:含无进展前进、目录追加分隔符 | 仅 `existsDir` 读 fs(与模块既有风格一致,测试用 tmpdir 真实目录) | unit |
71
-
72
- dashboard.ts 的 Tab 分支瘦身成:取候选 → 调 helper → 写 query → `nextCwdPickerState()` 重算 → 设 index。
73
- 这层接线用既有探针模式覆盖(test-support/*.ts + `node --experimental-transform-types` 子进程),
74
- 不新造测试基建。实现不得把这两条语义重新耦回 dashboard.ts(保持纯函数边界)。
75
-
76
- ## 验收矩阵
77
-
78
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
79
- |----|--------|----------|----------|----------|
80
- | A1 | browse 模式 Tab 补全高亮候选进输入框并重算状态 | 自动化(integration,探针) | `node --test test/dashboard-cwd-tab.test.mjs` | 补全后 cwdQuery 等于高亮候选路径;修复前此测试必失败 |
81
- | A2 | 连续 Tab 逐层下钻(选项 B 专属) | 自动化(integration,探针) | 同上探针:构造 tmp 目录树 `root/work/app`,连续 Tab | cwdQuery 依次深入 `work/` → `work/app/` |
82
- | A3 | favorites 模式 Tab 行为不回归 | 自动化(integration,探针) | 同上探针:ranked 非空 + 子串命中场景 | Tab 仍补全高亮收藏项,模式保持 favorites |
83
- | A4 | 纯函数 helper 边界 | 自动化(unit) | `node --test test/launch-options.test.mjs` | sameResolvedDir / browseTabCompletion 用例全过(含 `~`、尾部 `/`、单候选循环、index 越界) |
84
- | A5 | browse 提示文案含 tab | 自动化(integration,探针渲染输出断言) | 同上探针渲染 launch 视图 | browse 模式提示行含 `tab complete` |
85
- | A6 | 全量测试无回归 | 自动化(unit+integration 混合) | `npm test`(node --test test/*.test.mjs) | 全绿;另跑 `npm run typecheck` 通过 |
86
- | U1 | 真实 TUI 手感 | 用户实测 | 开 agent-board → Start session → cwd 输 `~/wo` → Tab 补全为 `~/work/`(或绝对路径)→ 连续 Tab 下钻 → Enter 确认 → 启动 session | 补全/下钻符合 shell 手感;启动的 session cwd 正确 |
87
-
88
- 说明:U1 无法自动化的原因——补全「手感」(连续 Tab 节奏、渲染刷新是否顺眼)只有人在真实终端能判断;
89
- 探针已覆盖功能正确性,U1 是最终 UX 确认,时机为 PR 合并前。
90
-
91
- ## 非目标
92
-
93
- - 不改 Enter/上下键/Esc 语义;不动 cwd-stats 计数;不动 favorites 排序与 home 兜底。
94
- - 不做模糊匹配(fuzzy subsequence)——#22 已明确否决。
95
- - 不做 issue 建议 3 的 `~` 展开进收藏匹配(除非用户点名纳入)。
@@ -1,63 +0,0 @@
1
- # Spec: attach 终端能力透传——亮暗探测与颜色方案通知(issue #128)
2
-
3
- ## 根因(三缺口,调研钉死)
4
-
5
- attach 客户端用 headless xterm 重建屏幕,宿主 Pi 的终端能力查询被本地模型消费、从不转发真实终端(缺口 A);即使转发,996/2031 应答会被本地 pi-tui `consumeTerminalColorSchemeReport` **无条件消费**,到不了宿主(缺口 B);宿主首次探测发生在 runner spawn 时(attach 前),纯转发只救 /settings 重选,救不了首帧(缺口 C)。
6
-
7
- ## 设计(四组件,全部在 attach 客户端侧)
8
-
9
- ### D1 出站转发器:`extractOscQuerySequences`(纯函数)
10
-
11
- - 转发:OSC 11 查询 `\x1b]11;?` + BEL/ST 定界;`\x1b[?2031h`、`\x1b[?2031l`
12
- - 不转发:OSC 11 **设置** `\x1b]11;rgb:...`(防污染本地配色);kitty/DA 协商 `\x1b[>7u`、`\x1b[?u`、`\x1b[c`(见非目标)
13
- - 结构照抄 `extractOsc52Sequences` 的 carry 模式(跨 chunk 前缀保留,独立 `oscQueryCarry`),接入 `forwardTerminalProtocols` 第三提取器
14
- - 转发是追加行为:序列照常喂 headless 屏幕模型(xterm 忽略 OSC 11 查询,无副作用)
15
-
16
- ### D2 亮暗桥:`onTerminalColorSchemeChange` → 宿主
17
-
18
- - attach 时 `this.tui.onTerminalColorSchemeChange(scheme => this.send({type:"input", data: toColorSchemeReport(scheme)}))`;detach 时退订(生命周期对称)
19
- - 原理:本地 pi-tui 消费 996 报告/2031 通知时**同步通知**该监听器——桥把消费事件重打包为颜色方案报告(`\x1b[?997;Ps n`,2=light 1=dark——报告码点是 997,996 是查询 DSR;task 1 实测对齐 pi-tui terminal-colors.js:19)写宿主 pty,宿主 pi-tui 同 parser 消费并通知宿主的 query/2031 监听
20
- - `toColorSchemeReport(scheme)` 纯函数;格式对齐 pi-tui `parseTerminalColorSchemeReport` 的接受形态(实现时从其源码对齐)
21
-
22
- ### D3 首帧回放:attach settle 时主动发一条探测
23
-
24
- - attach settle 后向本地终端写一条 `\x1b]11;?\x07`(`tui.terminal.write`)
25
- - 应答**不需要截获**:本地 pending=0(正常场景)时穿透消费链 → attach 组件 handleInput 兜底 → `send({type:"input"})` → 宿主收到 → 主题翻转
26
- - 失败模式(探测无应答/本地 pending>0 截获):静默保持现状(dark),不重试
27
- - 仅 settle 时一次;后续宿主自身重探测(/settings 重选)由 D1 转发覆盖
28
-
29
- ### D4 应答天然路径(零代码,验收依据)
30
-
31
- kitty flags/DA/OSC 11 应答不被本地消费链 ①②④ 消费 → 流到 attach 组件兜底转发(既有行为,issue 正文已确认)——D1/D3 的应答回程都靠它。
32
-
33
- ## 验收矩阵
34
-
35
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
36
- |----|--------|----------|----------|----------|
37
- | A1 | 查询提取器 | 自动化验证(unit) | `node --test test/terminal-query-sequences.test.mjs` | 查询(BEL/ST)转发、设置形态不转发、2031h/l 转发、跨 chunk carry 正确 |
38
- | A2 | scheme→报告序列 | 自动化验证(unit) | 同上 | `toColorSchemeReport` 输出与 pi-tui parser 接受形态一致(light/dark 两态) |
39
- | A3 | 出站转发接线 | 自动化验证(smoke) | `node --test test/pty-attach-detach-gate.test.mjs`(扩展场景) | 远端 output 含 OSC 11 查询 → 本地 terminal.write 收到;含 `rgb:` 设置 → 不写 |
40
- | A4 | 亮暗桥接线 | 自动化验证(smoke) | 同上 | 伪 tui 触发 colorScheme listener → socket 收到 `type:"input"` 且 data 为颜色方案报告(`\x1b[?997;2n` / `\x1b[?997;1n`);detach 后触发不再发 |
41
- | A5 | 首帧回放 | 自动化验证(smoke) | 同上 | attach settle → 本地 terminal.write 恰好一条 `\x1b]11;?\x07`;detach/未 settle 不写 |
42
- | A6 | kitty/DA 不转发 | 自动化验证(smoke) | 同上 | 含 `\x1b[>7u\x1b[?u\x1b[c` 的远端 output 不写本地终端 |
43
- | A7 | 桥生命周期 | 自动化验证(unit) | 同 A4 用例内 | 注册/退订对称,无泄漏监听 |
44
- | U1 | 重选恢复 | 用户实测 | WezTerm Latte 浅色 + attach 会话 → /settings → Theme → Automatic | 主题变 light-warm(accent 色对) |
45
- | U2 | 首帧正确 | 用户实测 | 浅色终端 attach 一个新会话,不进 /settings | 进入即 light-warm(回放生效) |
46
- | U3 | 实时跟随 | 用户实测 | attach 中切系统亮暗(KDE appearance) | 会话主题实时跟随(2031 桥) |
47
-
48
- ## 可测性拆分设计(硬约束)
49
-
50
- - `extractOscQuerySequences(input) → {sequences, carry}`:纯函数,独立文件 `src/core/terminal-query-sequences.mjs`(与提取器家族同址),单测直测(A1)
51
- - `toColorSchemeReport(scheme) → string`:纯函数,同文件,单测直测(A2);格式常量从 pi-tui `parseTerminalColorSchemeReport` 源码对齐并在测试注释标注来源
52
- - 桥/回放/转发接线:attach 组件内三个小方法(`attachColorSchemeBridge`/`replayBackgroundQuery`/forward 接线),副作用(tui.terminal.write、send)通过既有 smoke harness 的伪 tui/socket 断言(A3-A7),不在单测层 mock 组件内部
53
-
54
- ## 非目标(已知未覆盖,如实记录)
55
-
56
- - **kitty/DA 键盘协议协商**:宿主协商仅在 spawn 时(attach 前已错过,转发无效);且转发 `\x1b[>7u` 会污染真实终端 kitty 栈顶(本地 pi 与宿主共享 stdin,flags 不一致时本地输入解析错乱)。修饰键编码差异保持现状(modifyOtherKeys 回退)
57
- - **OSC 10/12 查询**:pi 0.87.0 实测不发,不做 speculative 转发
58
- - **本地 pi pending>0 边角**:本地探测超时且上游 counter bug 未清零时,回放应答被本地截走一次(宿主下次重探测即恢复);修上游(counter 递减)超出本仓范围
59
- - **kill switch**:沿用既有 env 约定(`AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0` 关闭全部新行为,默认开)
60
-
61
- ## 降级路径
62
-
63
- 全部新行为 best-effort:转发写失败静默(沿用 forwardTerminalProtocols catch);桥/回放失败不影响 attach 主体;kill switch 一键回退现状。
@@ -1,57 +0,0 @@
1
- # Spec: 终端无关的无条件 detach 逃生键(issue #126)
2
-
3
- 状态:draft(等用户确认)
4
- 日期:2026-09-23
5
-
6
- ## 问题
7
-
8
- `Ctrl+←` 是 attach 表面唯一的无条件 detach 键(#89 引入),但它的可达性依赖「终端愿意把 Ctrl+Left 编码为转义序列发送」——macOS Terminal.app 默认不发任何字节、WezTerm-macOS 把它消费为 tab 切换(#126 实测)。D1(#135)后 `←` 只在 `editorEmpty === true` 时 detach,于是在坏终端 + 草稿/未知编辑器状态下用户无键可逃,只能 Ctrl+C/D 连带杀 child——触碰 #48「attach 永远可退出」红线。
9
-
10
- ## 设计决策
11
-
12
- **新增 `Ctrl+\`(裸字节 0x1c)作为无条件 detach 键**,形成三层逃生梯队:
13
-
14
- 1. `←` — 智能层:`editorEmpty === true` 时 detach,否则转发(D1 契约不变)
15
- 2. `Ctrl+←` — 快捷层:好终端保留(不依赖 child 状态,已实现)
16
- 3. `Ctrl+\` — **无条件层**:任何状态(含 editorEmpty=false/null、connected 与否)立即 detach,且不依赖终端对修饰键的编码
17
-
18
- ### 为什么是 Ctrl+\(被丢弃的选项)
19
-
20
- - 0x1c 是单字节 ASCII 控制字符(FS):raw mode 下**所有**终端原样发送(kitty legacy ctrl-mapping 官方表;pi-tui keys.js 头注释引用同一映射);没有任何主流终端默认占它做快捷键;SIGQUIT 语义只存在于 cooked mode,board/pi/child 全链路 raw mode 不受影响。
21
- - 实测证据(spec review 全部复验):pi-tui `matchesKey` 对**三种键盘模式编码**全部原生匹配——裸字节 `\x1c`、kitty CSI-u `\x1b[92;5u`、modifyOtherKeys `\x1b[27;5;92~`(pi 本体开启 modifyOtherKeys/kitty 探测,WezTerm 等真实环境发的正是后两种);pi 本体 dist 全量 rg 无 `ctrl+\` 绑定;本仓 src/runner/test 无占用。
22
- - **已知限制一(键盘布局)**:「所有终端」的断言只覆盖终端协议层,不覆盖键盘布局层——德式/北欧等布局中 `\` 位于 AltGr 层(如 AltGr+ß),Ctrl+\ 未必产出 0x1c。布局受限场景由梯队兜底:这些布局高发的 Linux 现代终端上 `Ctrl+←`(快捷层)可靠;`editorEmpty=true` 时 `←`(智能层)也始终在。README 已知代价中如实写明。
23
- - **已知限制二(Ctrl+4 别名)**:legacy ASCII 映射中 Ctrl+4 同样产出 0x1c,故 legacy 模式下按 Ctrl+4 也会 detach。Pi 不使用 Ctrl+4,风险可忽略,但属于「从 child 抢键」代价的一部分,文档如实记录。
24
- - 被丢弃:Esc(#89 否决:vim 冲突)、双击 ←(#89 否决:误触)、F 键(零星终端绑定冲突 + Pi 未来可能占用)、鼠标 detach 区(误触 + 可发现性差,不承担红线)、只修文案(不根治)。
25
- - 已知代价(文档化):从 child 抢 Ctrl+\。Pi 无此绑定;仅 child 内 vim terminal-mode 的 Ctrl+\ Ctrl+N 前缀受影响(极小众)。
26
-
27
- ### 不做的事(Non-goals)
28
-
29
- - 不翻转 D1 的 `editorEmpty=null → 转发` 契约(坏终端由无条件层兜住,不动刚钉死的语义)。
30
- - 不移除 `Ctrl+←`(好终端保留为快捷层)。
31
- - 不改断线路径(`!connected` 已无条件逃生)。
32
- - 不为 0x1c 做可配置化(YAGNI;若未来 child 需要 Ctrl+\,届时加配置项)。
33
-
34
- ## 组件契约
35
-
36
- `src/ui/pty-attach.ts` `handleInput`:在 `Key.ctrl("left")` 分支之前新增分支——
37
- `if (matchesKey(data, "ctrl+\\")) { this.detach(); return; }`(0x1c 裸字节直达,无终端编码依赖)。
38
- header(render)与 loading banner 文案更新:`Ctrl+← detach` → `Ctrl+← / Ctrl+\ detach`;README 按键段(:96/:195 区域)补无条件键说明与已知代价。
39
-
40
- ## 验收矩阵
41
-
42
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
43
- |----|--------|----------|----------|----------|
44
- | A1 | Ctrl+\ 任何状态无条件 detach | 自动化验证(unit,detach-gate smoke 场景) | `node --test test/pty-attach-detach-gate.test.mjs`:注入 `\x1c` 于 editorEmpty=true/false/null × connected=true/false 四象限 | 四象限全部 detach(唯一无条件断言集) |
45
- | A2 | Ctrl+\ 不受 child 草稿门禁影响 | 自动化验证(unit) | 同 A1 的 editorEmpty=false 用例显式 pin(草稿在、仍 detach) | 草稿态 detach 成功 |
46
- | A3 | 0x1c 与既有键无匹配冲突 | 自动化验证(unit) | 注入 `\x1b[1;5D`(ctrl+left)、`\x1b[D`(left)、`q`、`\x1c` 各自独立断言行为 | ctrl+left 仍 detach;left 按 D1 门禁;q/可打印键转发;0x1c detach |
47
- | A4 | 文案与 README 同步 | 自动化验证(unit + static) | detach-gate 测试断言 header 含 `Ctrl+\`;rg README 两处按键段含无条件键说明 | 断言通过 |
48
- | U1 | 真实坏终端实测 | 用户实测(mac 环境,**pending**:本机 Linux 无法执行) | macOS Terminal.app(默认设置)attach 会话 + 编辑器含草稿 → 按 Ctrl+\ | detach 回 dashboard,child 存活(重新 attach 草稿还在) |
49
- | U1b | Linux 等效实测(U1 的本机可执行替代) | 用户实测 | WezTerm(kitty 开启,CSI-u 路径)与 xterm(legacy 路径)各一次:attach 会话 + 编辑器含草稿 → 按 Ctrl+\ | 两种终端均 detach,child 存活(重新 attach 草稿还在) |
50
-
51
- ## 可测性拆分设计
52
-
53
- - A1-A3 全部走既有 `test-support/detach-gate-smoke.ts` 真组件 harness(真实 xterm buffer → handleInput → didDetach()/转发包内容),新增 4 个场景(四象限)+ 1 个冲突隔离场景组;连接态用 smoke 既有 `connected=true` 显式模式。无需新纯函数(改动是单分支拦截,行为面全部由组件级 harness 覆盖)。
54
- - A1 的四象限注入应同时覆盖**三种编码形态**(裸 `\x1c` / kitty `\x1b[92;5u` / modifyOtherKeys `\x1b[27;5;92~`)至少各一条,防止未来键解析层变更悄悄断掉某一模式(spec review 实测三种编码当前均被 `matchesKey` 原生匹配)。
55
- - A4 的 header 断言沿用既有 `headerMentions*` 模式。
56
- - 测试边界:不改 `shouldEscapeAttach`(它的语义是 ← 门禁,与 Ctrl+\ 分支正交);smoke 场景不得依赖终端模拟层(0x1c 是裸字节,天然无模拟依赖)。
57
- EOF
@@ -1,98 +0,0 @@
1
- # Spec(v3,已确认):#140 control-reconcile 偶红收敛 —— 断言窗口化与口径对齐
2
-
3
- - issue: zhuxixi/pi-agent-board#140
4
- - 根因报告:`research/root-cause.md`(含复现脚本 `research/repro-140.test.mjs`,产品代码零改动)
5
- - 状态:已确认(2026-09-24,用户批准 v3)
6
- - 变更记录:v1→v2(首轮评审)与 v2→v3(终审)见文末 §8
7
-
8
- ## 1. 问题
9
-
10
- `test/control-reconcile.integration.test.mjs` 的两条断言在负载下偶红(签名 A `:295`、签名 B `:383`),**产品行为正确**:
11
-
12
- - 签名 A:wire 上出现重复 seq。机制是 runner 无法原子「停止裸广播 + 开始 ring 重放」——订阅前已写入 socket 的 stray 若在客户端**发出订阅之后**才投递,就落在 `resyncing` 分支(既不计入 `strayHighWater`,又被当成重放起点)。客户端按 `seq <= lastSeq` 丢弃重放的那一份 ⇒ **UI 仍恰好一次**。
13
- - 签名 B:`assert.ok(h.outputSeqs().at(-1) >= ready2.nextSeq)` 不是有效不变量——`outputSeqs()` 是跨 socket 累加器,`ready2` 的 `waitFor` 因旧事件已存在而**立即返回**(取到旧 baseline)。新 runner 的 seq 从 1 重启,只要断言时刻最后一条 output 是快照前的 stray,断言即假。
14
- - 共同根因:`attachClientOverSocket()` 的 `messages` / `events` 是闭包级单数组,`switchSocket()` 不清空、不分段 ⇒ 断言把「重连前残留 / 窗口外消息」与「新流」混在同一集合里比较。
15
-
16
- 对 issue 三条出口的对账:命中「断言过严/把窗口外残留计入」;**未**命中「runner 真实重复写 seq」(重复 = 一次订阅前裸广播 + 一次重放);**不属** #95 墙钟类(两条均为断言失败,326ms/359ms,非超时)⇒ 不重分类。
17
-
18
- **证据强度(v2 补)**:「UI 恰好一次在 wire 重复下成立」共 4 次观测(基线自然交错 1 次 + 150ms 确定性交错 3 次,marker guard 全部通过,其中 3 次窗口内有 6–7 个重复);「老断言不完备」已从推断升级为观测——150ms 交错下 wire 含 6 个重复而老 `deepEqual` 通过(前 3 个恰好被 strays 填满)。
19
-
20
- ## 2. 目标 / 非目标
21
-
22
- **目标**:让该文件 A2 相关断言不再依赖消息到达时序;同时保住对真实回归的检测力;把与真实保证不符的注释改正。
23
-
24
- **非目标**:
25
- - 不改产品行为(唯一产品改动是注释,见 D6)。
26
- - 不做 F2(runner 侧为每 socket 记裸广播高水位、重放起点取 `max(sinceSeq, broadcastHighWater)`):只改 runner 会让客户端 **UI 冻结**:客户端 `resyncing` 对 `seq !== lastSeq + 1` 的 chunk 一律忽略,且该状态没有 gap 检测 / resync 触发路径,直到下一次重连才能恢复(v3 措辞精确化),必须同时改客户端游标语义;而收益仅「wire 无重复」,UI 早已正确 ⇒ 留给未来协议级设计(本 spec 只记录评估结论,不实现)。
27
- - 不动 #95 的墙钟预算策略。
28
-
29
- ## 3. 决策
30
-
31
- | ID | 决策 | 理由 |
32
- |---|---|---|
33
- | D1 | 在测试 harness 内加**窗口/标记 API**:recorder 抽成文件顶部的 **`createRecorder()` 纯数据结构**(`push` / `mark` / `since`),`attachClientOverSocket` 只是它的一个消费者(socket 绑定) | 根治「跨 socket 累加器」;纯数据结构让窗口语义真正可单测(见 D2/A2) |
34
- | D2 | 判定逻辑抽成**纯函数**(文件顶部,与 harness 并列):`distinctSeqsFrom(seqs, from)`、`hasWireOverlap(seqs)`、`isContiguousFrom(distinct, from, count)` | 判定收口、可单测、避免断言内联逻辑再次漂移 |
35
- | D3 | 签名 A 的断言改为设计真正保证的性质:**去重后连续**(无 gap)+ 既有的 **marker guard(UI 恰好一次)**;测试内注释写明「wire 允许在广播→重放交接处重复,UI 重复才是 bug」 | 设计只保证 UI 级 exactly-once;老断言(要求 wire 无重复)既过强(重复合法)又不完备(只查前 3 个 seq,窗外重复抓不到,已观测证实)。**检测力等价性论证**:对「重放起点偏高(跳过 d+1)」与「gap」两类真实回归,新断言与老断言同样变红(去重后首个 distinct ≠ d+1 即失败);唯一放弃检测的是「wire 重复」——恰是合法行为,其 UI 级真回归由 marker guard 覆盖 |
36
- | D4 | 签名 B 改为**条件式等待 + 客户端级不变量**:等 `snapshotReady` **数量增长**后取新事件的 `nextSeq`;断言 `h.client.getLastSeq()` 到达新 baseline 并继续前进;如需 wire 级证据,用 **mark 窗口 / 该 socket** 的切片 | `waitFor(存在任意旧事件)` 不是等待;跨 socket 的 `.at(-1)` 不是「新流的最后一条」 |
37
- | D5 | 新增**确定性交错用例**(harness 侧注入,产品代码不动),注入设计如下(v2 修正):<br>① **late-subscribe**:延迟**出站**的重连订阅写。参数依据:steady tick = 25ms(`test-support/fake-pty-pi.mjs:10`),**延迟 ≥ 4× tick 周期(150ms ≈ 6 tick)**——实测 3/3 每次恰好 6 个 strays 落窗,确定性成立;不可改成 ~40ms(≈ 1.6 tick,实测 2/5 命中,回到不确定)。优先实现为现有 A 测试内**同一条连接的第二次 reconnect**(注入只对第二次生效),新增 spawn = 0。<br>② **slow-consumer(epoch)**:**相对延迟**——只延迟 snapshot 路径消息(`deferFeed: msg => msg.type?.startsWith("snapshot") ? X : 0`),让 epochReset 立即触发而快照滞后到达。⚠️ **统一延迟无效**:stray/reply/snapshot 各后移同样 X,`[epochReset, 快照投递]` 窗口宽度不变(已从时序推演确认)。此用例必须新 spawn runner(epoch 换代无法复用),+2。<br>**非空转断言的观测点(v2 修正)**:重叠**滞后到达**——`waitFor(≥3)` 返回时重放尚未落地(实测该时刻 duplicates=0)。因此非空转必须写成**条件式等待**:① 用 `waitFor(() => 窗口内出现重复, 上限)` 后再断言不变量;② 的中间态检查在 epochReset 触发后立即执行,非空转条件是**此刻新 snapshotReady 事件尚不存在**(相对延迟保证快照必然未投递,确定性成立);注意 stray 是否先到达属时序不确定(新 runner 首个 tick 与 hello/reconcile 往返是竞速,两种结局都观测过),**不**作为非空转条件(v3 修正) | 把偶红变成必红的确定性覆盖;非空转断言防止用例静默退化为「什么都没覆盖」 |
38
- | D6 | 产品侧仅改一处注释:`src/core/terminal-attach-client.mjs` 中 `(wire-level no-dup)` 的表述 → 精确为「覆盖**已观察到的** stray;UI 级 exactly-once 才是保证,wire 可能重复在途 stray」;顺带核对该测试内 `:283-285` 的注释(「replay 恰好从 cursor 起 / stray 高水位起」同样只对已观察到的 stray 成立) | 注释即契约:这条过度承诺正是过严断言的来源(与 #130 同型根因) |
39
- | D7 | 降级路径:若确定性交错用例在本地/CI 出现抖动(非空转等待超时等),退化为「自然交错 + 稳健断言(D3/D4)」,移除该用例并在 PR 描述记录原因 | 不为了确定性而引入新的不稳定 |
40
-
41
- ## 4. 可测性拆分设计(自动化项必答)
42
-
43
- | 层 | 单元 | 怎么测 |
44
- |---|---|---|
45
- | 纯函数(无副作用) | `distinctSeqsFrom` / `hasWireOverlap` / `isContiguousFrom` | 直接单测:给定 seq 数组(含重复、缺口、空窗、边界 `from`)断言返回;不依赖 socket、不依赖时序 |
46
- | 纯数据结构(副作用仅限内部数组) | `createRecorder()`(push/mark/since)——窗口 API 的唯一新增记录侧逻辑 | 单测 mark 语义(mark 之前的消息/事件不得进入窗口);socket 绑定由集成用例覆盖 |
47
- | 时序边界 | 交错注入点(`deferWrite` / `deferFeed` 钩子,默认 0 = 行为不变) | 由 D5 两个确定性用例覆盖;默认关闭保证既有用例零影响;注入只延迟**写或投递**,`sent` 的记录顺序不变(不扰动 wire-order 断言) |
48
- | 端到端 | 两条既有 A2 用例(改断言后)+ 两个确定性用例 | `node --test` 按名过滤,重复运行验证稳定 |
49
-
50
- 测试边界声明:**不**新增产品侧纯函数(无产品逻辑变更);新增纯函数/纯数据结构全部位于测试文件内,不得反向引入产品依赖。<br>**plan 阶段审计项(v3 新增)**:全文件 grep recorder 累加器的所有断言使用(`outputSeqs` / `eventsOf` / `echoCount` 等全局计数),凡跨越断连/重连边界的断言一律改走窗口 API——例如 `echoCount` 目前恰好没事只是因为 echo chunk 的 seq 落在重放范围之外(seq ≤ 断连游标),是巧合不是设计。
51
-
52
- ## 5. 验收矩阵
53
-
54
- | ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
55
- |---|---|---|---|---|
56
- | A1 | 判定谓词行为 | 自动化(unit) | `node --test --test-name-pattern="seq window predicates" test/control-reconcile.integration.test.mjs` | 重复/缺口/空窗/边界用例全部符合预期 |
57
- | A2 | 窗口 API 语义 | 自动化(unit) | 同上(`createRecorder` 的 mark 语义用例) | 窗口内不含 mark 之前的消息/事件 |
58
- | A3 | 签名 A 断言口径(自然交错) | 自动化(integration) | `node --test --test-name-pattern="A2: reconnect wires"` | 去重后连续(`[d+1,d+2,d+3]`)+ marker guard 通过 + 客户端游标推进 ≥ d+3 |
59
- | A4 | 确定性交错 ①(late-subscribe) | 自动化(integration) | 同上(150ms 出站订阅延迟,注入只对 reconnect 生效) | `waitFor(窗口出现重复)` 命中(**非空转**)⇒ 去重后无 gap + UI 恰好一次 |
60
- | A5 | 签名 B 断言口径 | 自动化(integration) | `node --test --test-name-pattern="A2 epoch"` | 等**新** `snapshotReady`(数量增长);客户端游标到达 `nextSeq-1` 并继续前进 |
61
- | A6 | 确定性交错 ②(slow consumer) | 自动化(integration) | 同上(snapshot 路径相对延迟 X) | epochReset 后立即检查中间态:**新 snapshotReady 事件尚不存在**(**非空转**,确定性成立)+ 新断言通过 |
62
- | A7 | 检测力(mutation 检查) | 自动化(integration,临时副本) | 在临时副本里分别制造「丢掉一个 chunk」与「对 UI 重复 emit 一次」 | 新断言分别变红:no-gap 断言捕获丢包、marker guard 捕获 UI 重复 ⇒ 证明未空转 |
63
- | A8 | 稳定性 | 自动化(integration) | 对受影响用例(两条 A2 + 两条确定性,共 4 个)连续 **100** 次孤立运行(约 5 分钟) | 0 失败。统计边界:若签名 A 的真实失败率仍为基线 1/10,100 次全绿概率 ≈ 0.003% ⇒ 证伪力足够。真正的保证来自「失败模式被机制性移除 + A4/A6 确定性覆盖」,A8 是 sanity |
64
- | A9 | 静态与全量 | 自动化(static/build) | `npm run typecheck`;全量 `node --test` | typecheck 0 error;测试通过数不下降,失败仅既有 flake(需隔离复跑归因) |
65
- | — | 用户实测 | — | — | **无 U 项**:本 issue 无用户可见行为变更(产品侧仅注释),故不设用户实测;这是结论而非兜底分类 |
66
-
67
- ## 6. 风险与缓解
68
-
69
- | 风险 | 缓解 |
70
- |---|---|
71
- | 放宽 wire 断言后,真实的「同路径重复写」回归可能漏检 | marker guard 保持为 UI 级权威;A4/A6 的非空转断言确保交错被真实覆盖;A7 mutation 检查证明断言仍能变红;测试注释写明「wire 允许、UI 不允许」 |
72
- | 确定性注入自身引入新的不稳定(非空转等待超时 = 反向 flake) | 注入参数有硬依据(≥4× tick,tick=25ms 实测);非空转为有界条件等待;抖动时按 D7 降级并记录 |
73
- | 新增真实进程用例加剧 #95 的并行挤压 | late-subscribe 复用现有 runner(+0 spawn);epoch 用例 +2 不可避免;净增 spawn = 2 |
74
- | 断言改动掩盖「客户端游标未推进」这类真问题 | A3/A5 显式断言客户端游标推进(不只看 wire) |
75
-
76
- ## 7. 范围核实记录(v2 新增)
77
-
78
- - 跨 socket 录制器模式(`attachClientOverSocket` / `switchSocket`)**全仓仅此一个文件**(grep 证实)⇒ 无需姊妹文件审计,范围判断成立。
79
- - flake「账本」无独立文件(docs/ 下无 ledger);issue 所述账本实际是 #95 的追踪。本修复无需账本动作;PR 描述注明「#140 两条签名已机制性消除(断言口径修正 + 窗口化),非运气性通过」。
80
-
81
- ## 8. 评审修正记录(v1 → v2)
82
-
83
- 1. **[必修] A4 观测点**:重叠滞后到达(`waitFor(≥3)` 返回时 duplicates=0,实测),非空转改为条件式等待后再断言。
84
- 2. **[必修] A6 注入设计**:统一延迟无效(不改变相对时序,推演确认);改为 snapshot 路径相对延迟。
85
- 3. **[应修] A8 统计效力**:30 次 → 100 次,并如实声明「机制性移除为主、A8 为 sanity」。
86
- 4. **[应修] D5 参数依据**:写明 tick=25ms 与「≥4× tick」余量,禁止回退到 ~40ms。
87
- 5. **[建议] runner 复用**:late-subscribe 用例复用现有 A 测试的连接(+0 spawn),净增 spawn 降为 2。
88
- 6. **[建议] recorder 纯数据结构**:`createRecorder()` 与 socket 绑定解耦,A2 的 unit 定位名副其实。
89
- 7. **[建议] D3 等价性论证**:写明对 gap / 起点偏高两类回归检测力不降,防未来评审者误判。
90
- 8. **[核实] 范围两件事**:录制器模式无姊妹文件;账本无需动作(见 §7)。
91
-
92
- ### v2 → v3(终审)
93
-
94
- 9. **[必修] A6 非空转条件**:stray 到达时机是竞速(新 runner 首 tick 25ms vs hello/reconcile 往返 ~1–3ms),非确定性;改写为「测试推进时新 snapshotReady 事件尚不存在」(相对延迟保证,确定性)。
95
- 10. **[应修] A8 口径**:全文件 100 次 ≈ 12 分钟 → 收窄为受影响 4 用例 100 次 ≈ 5 分钟,统计边界不变。
96
- 11. **[应修] F2 否决措辞**:「卡死」→「UI 冻结(resyncing 无 gap 检测 / resync 触发路径,直至下一次重连)」。
97
- 12. **[建议] plan 阶段审计项**:全文件 recorder 累加器断言审计(`echoCount` 目前安全是巧合而非设计)。
98
- 13. **[核对] tick 参数契约**:注释 25ms 与 `setInterval(..., 25)` 实参一致(`fake-pty-pi.mjs:10-16`)✓。