@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,526 +0,0 @@
1
- # X10 mouse release classification Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Stop decoding an X10/normal mouse release (`Cb = 3`) as a press, so a drag in the legacy attach path copies the selection on release instead of clearing it.
6
-
7
- **Architecture:** One classification fix inside the pure mouse parser (`parseMouseEventPrefix`, X10 branch) plus the tests that pin it: unit rows on the exported pure functions (`parseMouseEvent`, `parseMouseInputChunk`), and a component-level scenario that drives real X10 bytes through the real `PtyAttachComponent` + runner socket and observes the OSC 52 copy. No handler change, no new export, no new env flag, no state.
8
-
9
- **Tech Stack:** Node ESM (`.mjs` runtime, `.ts` UI), `node:test` + `assert/strict`, `@xterm/headless`, the repo's `node --experimental-transform-types` scenario pattern.
10
-
11
- **Spec:** `docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md` (issue #188).
12
- **Worktree / branch:** `.pi/worktrees/issue-188-x10-release` on branch `issue-188-x10-release` (baseline `main` @ `7cf0853`). Every command below runs from that worktree root.
13
-
14
- ## Global Constraints
15
-
16
- - **One edit site.** The behaviour change lives in `parseMouseEventPrefix` (`src/core/pty-scroll.mjs:53-66`). Do not add a handler-side release rule, a new export, a new env flag, or config.
17
- - **Do not touch the SGR branch or the passive branch** (`sgr[5] === "m"` already marks SGR/passive releases).
18
- - **Do not touch the release branch's `shouldCopy` logic** (`src/ui/pty-attach.ts:1081-1088`): geometry-based drag detection is #187, clearing the highlight after a copy is #172. This issue changes neither.
19
- - **Wheel stays `"press"`**: any release rule must exclude bit 6 (`64`), otherwise `64`/`65` wheel reports change classification. `mouseWheelDirection` must keep returning +1/−1 for X10 wheel up/down.
20
- - **Tabs** for indentation, **English** comments and commit messages, conventional commit subjects with the issue number, e.g. `fix(attach): … (issue #188)`.
21
- - **Unit test file:** extend `test/pty-scroll.test.mjs` (do not create a second parser test file).
22
- - **Component test pair:** `test-support/pty-attach-x10-scenario.ts` + `test/pty-attach-x10.test.mjs`, modelled on `test-support/pty-attach-mouse-scenario.ts` + `test/pty-attach-mouse.test.mjs`.
23
- - **Hermetic tests:** the scenario must stub `xclip` on `PATH` so `writePrimarySelection()` never touches the host X11 clipboard.
24
- - **No `git add -A`**: stage files by path.
25
- - **Definition of done:** `npm run verify` exits 0 (typecheck + perf gate + full `node --test test/*.test.mjs` + coverage thresholds + `pack:dry`).
26
- - **Acceptance IDs** (`A1`–`A6`, `U1` from the spec §5) are referenced per task; the mapping must stay bidirectional.
27
-
28
- ### Acceptance ↔ task map
29
-
30
- | Spec ID | What it pins | Task |
31
- |---|---|---|
32
- | A1 | X10 press / motion / release / modifier-release classification | Task 1 |
33
- | A2 | Wheel, middle, right, motion+button-3, non-mouse, passive rows unchanged | Task 1 |
34
- | A3 | Concatenated X10 reports decode in order (and a 7-byte chunk is rejected) | Task 1 |
35
- | A4 | X10 drag release copies (OSC 52 payload) | Task 2 |
36
- | A5 | Click / right button / wheel branches unchanged at component level | Task 2 |
37
- | A6 | `npm run verify` (static + full suite + coverage + pack) | Task 3 |
38
- | U1 | Optional real-terminal check (no X10-only terminal available → `pending`) | Task 3 (disposition) |
39
-
40
- ---
41
-
42
- ### Task 1: Classify the X10 release code in the parser (A1, A2, A3)
43
-
44
- **Files:**
45
- - Modify: `src/core/pty-scroll.mjs:53-66` (the X10 branch of `parseMouseEventPrefix`)
46
- - Test: `test/pty-scroll.test.mjs` (append two `test(...)` blocks; existing assertions stay untouched)
47
-
48
- **Interfaces:**
49
- - Consumes: nothing from earlier tasks.
50
- - Produces: `parseMouseEvent(data)` returns `{ encoding: "x10", button: number, col: number, row: number, action: "press" | "move" | "release" }` for 6-byte X10 reports; `parseMouseInputChunk(data)` returns the per-report array with the same `mouse` shape (unchanged signatures).
51
-
52
- - [ ] **Step 1: Write the failing tests**
53
-
54
- Append to `test/pty-scroll.test.mjs` (it already imports `mouseWheelDirection`, `parseMouseEvent`, `parseMouseInputChunk`):
55
-
56
- ```js
57
- // Issue #188: in the X10/normal encoding (ESC [ M Cb Cx Cy, each byte = value
58
- // + 32) the low two bits of Cb are the button, except value 3, which is the
59
- // protocol's release code and carries no button identity; bit 5 is motion and
60
- // bit 6 is wheel. Classifying 3 as a press made the attach surface clear the
61
- // drag selection on release instead of copying it.
62
- test("parseMouseEvent classifies X10 button codes (issue #188)", () => {
63
- // Cb = 0/1/2: left/middle/right press. col 1, row 2 (body row 0).
64
- assert.deepEqual(parseMouseEvent("\x1b[M !\""), { encoding: "x10", button: 0, col: 1, row: 2, action: "press" });
65
- assert.deepEqual(parseMouseEvent("\x1b[M!!\""), { encoding: "x10", button: 1, col: 1, row: 2, action: "press" });
66
- assert.deepEqual(parseMouseEvent("\x1b[M\"!\""), { encoding: "x10", button: 2, col: 1, row: 2, action: "press" });
67
- // Cb = 32: motion (col 8); Cb = 32|3: motion without a button held.
68
- assert.deepEqual(parseMouseEvent("\x1b[M@(\""), { encoding: "x10", button: 32, col: 8, row: 2, action: "move" });
69
- assert.deepEqual(parseMouseEvent("\x1b[MC!\""), { encoding: "x10", button: 35, col: 1, row: 2, action: "move" });
70
- // Cb = 3: release; 3|4 shift, 3|16 ctrl must stay releases.
71
- assert.deepEqual(parseMouseEvent("\x1b[M#!\""), { encoding: "x10", button: 3, col: 1, row: 2, action: "release" });
72
- assert.deepEqual(parseMouseEvent("\x1b[M#4\""), { encoding: "x10", button: 3, col: 20, row: 2, action: "release" });
73
- assert.deepEqual(parseMouseEvent("\x1b[M'!\""), { encoding: "x10", button: 7, col: 1, row: 2, action: "release" });
74
- assert.deepEqual(parseMouseEvent("\x1b[M3!\""), { encoding: "x10", button: 19, col: 1, row: 2, action: "release" });
75
- // Cb = 64/65: wheel, and the release rule must not capture it.
76
- assert.deepEqual(parseMouseEvent("\x1b[M`!\""), { encoding: "x10", button: 64, col: 1, row: 2, action: "press" });
77
- assert.deepEqual(parseMouseEvent("\x1b[Ma!\""), { encoding: "x10", button: 65, col: 1, row: 2, action: "press" });
78
- assert.equal(mouseWheelDirection("\x1b[M`!\""), 1);
79
- assert.equal(mouseWheelDirection("\x1b[Ma!\""), -1);
80
- });
81
-
82
- test("parseMouseInputChunk decodes a concatenated X10 gesture in order (issue #188)", () => {
83
- assert.deepEqual(parseMouseInputChunk("\x1b[M !\"\x1b[M@(\"\x1b[M#4\""), [
84
- { length: 6, raw: "\x1b[M !\"", mouse: { encoding: "x10", button: 0, col: 1, row: 2, action: "press" } },
85
- { length: 6, raw: "\x1b[M@(\"", mouse: { encoding: "x10", button: 32, col: 8, row: 2, action: "move" } },
86
- { length: 6, raw: "\x1b[M#4\"", mouse: { encoding: "x10", button: 3, col: 20, row: 2, action: "release" } },
87
- ]);
88
- // Seven bytes is not an X10 report: the all-or-nothing chunk parser rejects it
89
- // (#189), which is why every literal above is exactly six bytes.
90
- assert.equal(parseMouseInputChunk("\x1b[M !\"\x1b[M#20\""), null);
91
- });
92
- ```
93
-
94
- - [ ] **Step 2: Run the tests to verify they fail**
95
-
96
- Run: `node --test test/pty-scroll.test.mjs`
97
- Expected: FAIL in both new tests on the release rows, e.g. `action: 'press'` where `'release'` was expected (3 rows in the first test, 1 in the second). The wheel/middle/right/motion rows already pass.
98
-
99
- - [ ] **Step 3: Write the minimal implementation**
100
-
101
- Replace the X10 branch in `src/core/pty-scroll.mjs` (currently `:52-66`) with:
102
-
103
- ```js
104
- // X10/normal mouse: ESC [ M Cb Cx Cy. Cb is encoded as button + 32.
105
- if (input.startsWith("\x1b[M") && input.length >= 6) {
106
- // Cb packs the button (bits 0-1), modifiers (2-4), motion (5) and wheel (6),
107
- // except bit value 3 in bits 0-1: the protocol's release code, which carries
108
- // no button identity. Decoding it as a press made the attach surface clear
109
- // the drag selection on release instead of copying it (issue #188). Wheel
110
- // reports never carry a release, so bit 6 keeps the press classification.
111
- const button = input.charCodeAt(3) - 32;
112
- const motion = (button & 32) !== 0;
113
- const wheel = (button & 64) !== 0;
114
- const release = !motion && !wheel && (button & 3) === 3;
115
- return {
116
- length: 6,
117
- raw: input.slice(0, 6),
118
- mouse: {
119
- encoding: "x10",
120
- button,
121
- col: input.charCodeAt(4) - 32,
122
- row: input.charCodeAt(5) - 32,
123
- action: release ? "release" : motion ? "move" : "press",
124
- },
125
- };
126
- }
127
- ```
128
-
129
- - [ ] **Step 4: Run the tests to verify they pass**
130
-
131
- Run: `node --test test/pty-scroll.test.mjs`
132
- Expected: PASS — every test in the file, including the pre-existing X10 rows (`\x1b[M !!"` → press, `mouseWheelDirection` wheel rows).
133
-
134
- - [ ] **Step 5: Commit**
135
-
136
- ```bash
137
- git add src/core/pty-scroll.mjs test/pty-scroll.test.mjs
138
- git commit -m "fix(attach): decode the X10 mouse release code as release (issue #188)"
139
- ```
140
-
141
- ---
142
-
143
- ### Task 2: Component-level proof that an X10 drag release copies (A4, A5)
144
-
145
- **Files:**
146
- - Create: `test-support/pty-attach-x10-scenario.ts`
147
- - Create: `test/pty-attach-x10.test.mjs`
148
-
149
- **Interfaces:**
150
- - Consumes: Task 1's classification (the scenario fails without it — Step 3 proves that).
151
- - Produces: a scenario that prints one JSON line on stdout:
152
- `{ ok, sawContent, error, cases: { drag, click, rightButton, wheel } }` where each case is `{ osc52: string[], forwarded: string[], paintedRightAfterRelease: boolean, paintedAfterWindow: boolean }`.
153
-
154
- - [ ] **Step 1: Create the scenario**
155
-
156
- Create `test-support/pty-attach-x10-scenario.ts`:
157
-
158
- ```ts
159
- // Component-level X10 mouse-report coverage (issue #188): the REAL
160
- // PtyAttachComponent against a REAL runner socket (runner/pty-runner.mjs) with a
161
- // fake PTY (test-support/fake-pty-pi.mjs, default mode -> one "fake pi ready"
162
- // line) and a fake TUI capturing terminal.write, driven by X10-encoded reports
163
- // through the public handleInput seam. Verifies:
164
- // 1. an X10 drag (press -> motion -> release) copies the selection: exactly one
165
- // OSC 52 write whose payload is the selected text. This is the issue #188
166
- // regression — before the fix the release was decoded as a non-primary
167
- // press, so the selection was cleared and nothing was copied;
168
- // 2. a same-point X10 click copies nothing, keeps its 1-cell highlight right
169
- // after the release and loses it once the pending-click window elapsed;
170
- // 3. an X10 right press + release copies nothing (the release finds no live
171
- // selection);
172
- // 4. an X10 wheel report the local viewport cannot scroll is forwarded to the
173
- // child raw instead of being consumed.
174
- // Hermetic: a PATH-stubbed xclip so writePrimarySelection() never touches the
175
- // host X11 clipboard. Run via `node --experimental-transform-types`.
176
- import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
177
- import { Socket } from "node:net";
178
- import { tmpdir } from "node:os";
179
- import { join, resolve } from "node:path";
180
- import { spawn } from "node:child_process";
181
- import { atomicWriteJson } from "../src/core/atomic.mjs";
182
- import * as P from "../src/core/paths.mjs";
183
- import { createView, readHost } from "../src/core/store.mjs";
184
- import { PtyAttachComponent } from "../src/ui/pty-attach.ts";
185
-
186
- const root = mkdtempSync(join(tmpdir(), "agentview-attach-x10-"));
187
- const viewId = "x10-e2e";
188
-
189
- const binDir = join(root, "bin");
190
- mkdirSync(binDir);
191
- writeFileSync(join(binDir, "xclip"), "#!/bin/sh\nexit 0\n");
192
- chmodSync(join(binDir, "xclip"), 0o755);
193
- process.env.PATH = `${binDir}:${process.env.PATH}`;
194
-
195
- function isAlive(pid: number): boolean {
196
- try {
197
- process.kill(pid, 0);
198
- return true;
199
- } catch {
200
- return false;
201
- }
202
- }
203
-
204
- const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
205
-
206
- async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<boolean> {
207
- const start = Date.now();
208
- while (Date.now() - start < timeoutMs) {
209
- if (predicate()) return true;
210
- await sleep(25);
211
- }
212
- return false;
213
- }
214
-
215
- const meta = createView(root, { id: viewId, name: "x10 attach e2e", cwd: root });
216
- const configPath = P.hostConfigPath(root, viewId);
217
- atomicWriteJson(configPath, {
218
- root,
219
- viewId,
220
- sessionFile: meta.sessionFile,
221
- cwd: process.cwd(),
222
- initialPrompt: null,
223
- piCommand: process.execPath,
224
- piArgsPrefix: [resolve("test-support/fake-pty-pi.mjs")],
225
- model: null,
226
- tools: null,
227
- env: { AGENT_BOARD_ALLOW_PIPE_FALLBACK: "1" },
228
- cols: 80,
229
- rows: 22,
230
- });
231
-
232
- const runner = spawn(process.execPath, [resolve("runner/pty-runner.mjs"), configPath], {
233
- stdio: ["ignore", "pipe", "pipe"],
234
- });
235
- runner.stderr.resume();
236
-
237
- // X10 reports used below (ESC [ M Cb Cx Cy, each byte = value + 32; body row 0
238
- // is terminal row 2): press/motion/release of a full-line drag, a same-point
239
- // click release, a right-button press, and a wheel notch.
240
- const PRESS = "\x1b[M !\"";
241
- const MOTION = "\x1b[M@(\"";
242
- const RELEASE = "\x1b[M#4\"";
243
- const CLICK_RELEASE = "\x1b[M#!\"";
244
- const RIGHT_PRESS = "\x1b[M\"!\"";
245
- const WHEEL_UP = "\x1b[M`!\"";
246
-
247
- const result = {
248
- ok: false,
249
- sawContent: false,
250
- cases: {} as Record<string, Record<string, unknown>>,
251
- error: null as string | null,
252
- };
253
-
254
- try {
255
- const ready = await waitFor(() => {
256
- const host = readHost(root, viewId);
257
- return (
258
- !!host &&
259
- host.state === "alive" &&
260
- !!host.socketPath &&
261
- !!host.childPid &&
262
- isAlive(host.runnerPid) &&
263
- isAlive(host.childPid)
264
- );
265
- }, 10_000);
266
- if (!ready) throw new Error("host never became ready");
267
-
268
- // Intercept component -> runner frames so a forwarded wheel report is observable.
269
- const frames: string[] = [];
270
- const socketProto = Socket.prototype as any;
271
- const origWrite = socketProto.write;
272
- socketProto.write = function (data: any, ...rest: any[]) {
273
- if (typeof data === "string") frames.push(data);
274
- return origWrite.call(this, data, ...rest);
275
- };
276
- const forwardedInputs = () =>
277
- frames
278
- .map((line) => {
279
- try {
280
- return JSON.parse(line);
281
- } catch {
282
- return null;
283
- }
284
- })
285
- .filter((msg) => !!msg && msg.type === "input" && typeof msg.data === "string")
286
- .map((msg) => msg.data as string);
287
-
288
- const writes: string[] = [];
289
- const tui = {
290
- terminal: { rows: 24, cols: 80, columns: 80, write: (d: string) => writes.push(String(d)) },
291
- requestRender: () => {},
292
- };
293
- const theme = { fg: (_c: string, t: string) => t, bold: (t: string) => t };
294
- const component = new PtyAttachComponent(
295
- tui as never,
296
- theme,
297
- {} as never,
298
- () => {},
299
- {
300
- socketPath: readHost(root, viewId)!.socketPath,
301
- screenLogPath: P.screenLogPath(root, viewId),
302
- title: "x10 attach e2e",
303
- } as never,
304
- );
305
-
306
- const deadline = Date.now() + 8000;
307
- while (Date.now() < deadline && !result.sawContent) {
308
- const lines = component.render(80) ?? [];
309
- if (lines.join("\n").includes("fake pi ready")) result.sawContent = true;
310
- else await sleep(40);
311
- }
312
- if (!result.sawContent) throw new Error("session content never rendered");
313
-
314
- const osc52Payloads = () =>
315
- writes
316
- .filter((w) => w.includes("\x1b]52;c;"))
317
- .map((w) => Buffer.from(w.replace(/.*\x1b]52;c;/, "").replace("\x07", ""), "base64").toString("utf8"));
318
-
319
- // Selection paint on the fixture line: selected cells are rendered with inverse
320
- // video (\x1b[0;7;…), while the bare cursor block on an empty row is a
321
- // different sequence (\x1b[7m), so this probe only sees a live selection.
322
- const selectionPainted = () => {
323
- const line = (component.render(80) ?? []).find((l) => l.includes("fake pi ready")) ?? "";
324
- return line.includes("\x1b[0;7;");
325
- };
326
-
327
- const runCase = async (name: string, sequences: string[], settleMs = 400) => {
328
- writes.length = 0;
329
- frames.length = 0;
330
- for (const seq of sequences.slice(0, -1)) {
331
- component.handleInput(seq);
332
- await sleep(15);
333
- }
334
- component.handleInput(sequences[sequences.length - 1]);
335
- // Read the immediate state in the same tick as the release: the pending-click
336
- // window (DOUBLE_CLICK_MS = 260) must not be able to race this probe.
337
- const paintedRightAfterRelease = selectionPainted();
338
- await sleep(settleMs);
339
- result.cases[name] = {
340
- osc52: osc52Payloads(),
341
- forwarded: forwardedInputs(),
342
- paintedRightAfterRelease,
343
- paintedAfterWindow: selectionPainted(),
344
- };
345
- };
346
-
347
- await runCase("drag", [PRESS, MOTION, RELEASE]);
348
- await runCase("click", [PRESS, CLICK_RELEASE]);
349
- await runCase("rightButton", [RIGHT_PRESS, CLICK_RELEASE]);
350
- await runCase("wheel", [WHEEL_UP]);
351
-
352
- result.ok =
353
- (result.cases.drag.osc52 as string[]).length === 1 &&
354
- (result.cases.click.osc52 as string[]).length === 0 &&
355
- (result.cases.rightButton.osc52 as string[]).length === 0 &&
356
- (result.cases.wheel.osc52 as string[]).length === 0 &&
357
- (result.cases.wheel.forwarded as string[]).includes(WHEEL_UP);
358
-
359
- try {
360
- component.dispose();
361
- } catch {}
362
- } catch (err) {
363
- result.error = err instanceof Error ? err.message : String(err);
364
- }
365
-
366
- try {
367
- const pid = readHost(root, viewId)?.childPid;
368
- if (pid) process.kill(pid, "SIGKILL");
369
- } catch {}
370
- try {
371
- runner.kill("SIGKILL");
372
- } catch {}
373
- await sleep(50);
374
- try {
375
- rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 30 });
376
- } catch {}
377
-
378
- console.log(JSON.stringify(result));
379
- ```
380
-
381
- - [ ] **Step 2: Create the test driver**
382
-
383
- Create `test/pty-attach-x10.test.mjs`:
384
-
385
- ```js
386
- import assert from "node:assert/strict";
387
- import test from "node:test";
388
- import { execFileSync } from "node:child_process";
389
- import { fileURLToPath } from "node:url";
390
- import { join } from "node:path";
391
-
392
- const ROOT_DIR = fileURLToPath(new URL("../", import.meta.url));
393
- const SCENARIO = join(ROOT_DIR, "test-support", "pty-attach-x10-scenario.ts");
394
-
395
- // Component-level coverage (issue #188): the REAL PtyAttachComponent against a
396
- // REAL runner socket with a fake PTY, driven with X10-encoded mouse reports
397
- // through the legacy handleInput path. Pins the copy-on-release effect (#188)
398
- // and the unchanged click / right-button / wheel branches.
399
- test("attach X10 mouse: drag release copies; click, right button and wheel do not (issue #188)", () => {
400
- const out = execFileSync(process.execPath, ["--experimental-transform-types", SCENARIO], {
401
- encoding: "utf8",
402
- timeout: 60_000,
403
- });
404
- const parsed = JSON.parse(out.trim().split("\n").filter(Boolean).pop());
405
- assert.equal(parsed.error, null, `scenario error: ${parsed.error}`);
406
- assert.equal(parsed.sawContent, true, "session content must render before mouse dispatch");
407
-
408
- // A4: the X10 drag copies exactly once, with the selected text as payload.
409
- assert.deepEqual(parsed.cases.drag.osc52, ["fake pi ready"], "X10 drag release must copy the selection");
410
-
411
- // A5: a same-point click copies nothing and its highlight clears after the window.
412
- assert.deepEqual(parsed.cases.click.osc52, []);
413
- assert.equal(parsed.cases.click.paintedRightAfterRelease, true, "the click keeps its 1-cell highlight during the pending-click window");
414
- assert.equal(parsed.cases.click.paintedAfterWindow, false, "the pending-click window clears the click highlight");
415
-
416
- // A5: right press + release copies nothing.
417
- assert.deepEqual(parsed.cases.rightButton.osc52, []);
418
-
419
- // A5: an X10 wheel report the local viewport cannot use is forwarded raw.
420
- assert.deepEqual(parsed.cases.wheel.osc52, []);
421
- assert.deepEqual(parsed.cases.wheel.forwarded, ["\x1b[M`!\""]);
422
-
423
- assert.equal(parsed.ok, true);
424
- });
425
- ```
426
-
427
- - [ ] **Step 3: Run the test to verify it passes**
428
-
429
- Run: `node --test test/pty-attach-x10.test.mjs`
430
- Expected: PASS (`ok 1`). If the drag assertion fails, the parser rule from Task 1 is not in place — do not weaken the assertion.
431
-
432
- - [ ] **Step 4: Prove the test is a real regression test**
433
-
434
- Run (restores the pre-fix parser from Task 1's parent commit, then brings the fix back):
435
-
436
- ```bash
437
- git checkout HEAD~1 -- src/core/pty-scroll.mjs
438
- node --test test/pty-attach-x10.test.mjs # expect FAIL: "X10 drag release must copy the selection"
439
- git checkout HEAD -- src/core/pty-scroll.mjs
440
- node --test test/pty-attach-x10.test.mjs # expect PASS
441
- git diff --exit-code src/core/pty-scroll.mjs
442
- ```
443
-
444
- Expected: the first run fails with the drag assertion (`[]` instead of `["fake pi ready"]`), the second passes, and the final `git diff --exit-code` prints nothing (the tree is back to the committed rule).
445
-
446
- - [ ] **Step 5: Commit**
447
-
448
- ```bash
449
- git add test-support/pty-attach-x10-scenario.ts test/pty-attach-x10.test.mjs
450
- git commit -m "test(attach): drive real X10 press/motion/release through the component (issue #188)"
451
- ```
452
-
453
- ---
454
-
455
- ### Task 3: Repo gate and acceptance reconciliation (A6, U1 disposition)
456
-
457
- **Files:**
458
- - No source changes expected. If coverage thresholds fail, extend `test/pty-scroll.test.mjs` (Task 1's style) — never weaken or delete assertions.
459
-
460
- **Interfaces:**
461
- - Consumes: Task 1's parser rule, Task 2's scenario + driver.
462
- - Produces: a recorded A1–A6 evidence set and the U1 disposition for the issue comment / PR body.
463
-
464
- - [ ] **Step 1: Run the repo gate**
465
-
466
- Run: `npm run verify`
467
- Expected: exit code 0 — `typecheck`, the perf gate, the full `node --test test/*.test.mjs` suite, coverage thresholds (lines 85 / funcs 80 / branches 70), and `pack:dry`.
468
-
469
- - [ ] **Step 2: If coverage or the suite fails**
470
-
471
- Read the failing output and fix the cause in the tests, not by loosening thresholds or `assert` calls. A coverage dip on `src/core/pty-scroll.mjs` means an X10 branch is unreached: add the missing case to Task 1's unit block (for example a modifier+wheel combination) and re-run `npm run verify`.
472
-
473
- - [ ] **Step 3: Record the acceptance evidence**
474
-
475
- Collect, verbatim, for the issue comment and PR body:
476
-
477
- ```bash
478
- node --test test/pty-scroll.test.mjs 2>&1 | tail -5
479
- node --test test/pty-attach-x10.test.mjs 2>&1 | tail -5
480
- npm run verify 2>&1 | tail -5
481
- git log --oneline 7cf0853..HEAD
482
- ```
483
-
484
- Map them to the spec matrix: A1–A3 → the `pty-scroll` run; A4–A5 → the `pty-attach-x10` run; A6 → `npm run verify`. Mark **U1 as `pending`** with its reason and recipe pointer (spec §6.3: no X10-only terminal on this machine; the component-level scenario drives the same byte stream through the real socket), and state that it is non-blocking by design.
485
-
486
- - [ ] **Step 4: Commit (only if Step 2 changed files)**
487
-
488
- ```bash
489
- git add test/pty-scroll.test.mjs
490
- git commit -m "test(attach): cover the remaining X10 classification branches (issue #188)"
491
- ```
492
-
493
- If Steps 1–2 left the tree clean, skip this step — the branch is ready for local code review with two commits (plus the spec commit).
494
-
495
- ---
496
-
497
- ## Self-Review
498
-
499
- **1. Spec coverage:**
500
- - §3.1 classification contract → Task 1 Step 3 (exact code, `(button & 96) === 0` and `(button & 3) === 3` rule, wheel/motion exclusion).
501
- - §3.2 parser-layer (no handler patch) → Global Constraints "One edit site".
502
- - §3.3 alternatives → not implemented by design; the handler-side alternative stays rejected.
503
- - §5 A1/A2/A3 → Task 1 Step 1 rows (press, middle `\x1b[M!!"`, right `\x1b[M"!"`, motion `\x1b[M@("`, motion+3 `\x1b[MC!"`, release `\x1b[M#!"`/`\x1b[M#4"`, shift `\x1b[M'!"`, ctrl `\x1b[M3!"`, wheel `\x1b[M\`!"`/`\x1b[Ma!"`, `parseMouseEvent("x")` and the passive rows already covered by the pre-existing test block, chunk + 7-byte rejection).
504
- - §5 A4/A5 → Task 2 (drag copies via OSC 52 payload; click highlight window, right button, wheel forward).
505
- - §5 A6 → Task 3 Step 1.
506
- - §5 U1 → Task 3 Step 3 disposition (`pending`, non-blocking, recipe pointer).
507
- - §6.1 not-changed list → A2's middle/right/wheel rows plus Global Constraints on SGR/passive and `shouldCopy`.
508
- - §6.2 #187/#172/#189 boundaries → Global Constraints ("do not touch `shouldCopy`", chunk all-or-nothing pinned in A3) and Task 2's click row (no retention assertion).
509
- - §6.3 guarantee boundary → no code change needed; A2 pins the unchanged rows.
510
- - §7 testability split → Task 1 (pure, no I/O) vs Task 2 (component seam, public `handleInput`, no private-field assertions; the render probe reads the public `render()` output).
511
- - §8 risks → the wheel regression risk is pinned by A2; the #187/#172 rebases are handled by Task 3 Step 1 re-running `npm run verify`.
512
-
513
- **2. Placeholder scan:** no TBD/TODO/"handle edge cases" steps; every code step carries the full code, every command carries its expected result.
514
-
515
- **3. Type consistency:** `parseMouseEvent`/`parseMouseInputChunk` shapes are used identically in Task 1 and Task 2 (`{ encoding, button, col, row, action }`); the scenario's JSON keys (`osc52`, `forwarded`, `paintedRightAfterRelease`, `paintedAfterWindow`, `cases.drag|click|rightButton|wheel`, `ok`, `sawContent`, `error`) match between the scenario and the driver assertion by assertion.
516
-
517
- ---
518
-
519
- ## Errata (post-implementation, 2026-10-15)
520
-
521
- Two deviations from the task text above happened during execution; the committed code is authoritative.
522
-
523
- 1. **Task 2's `selectionPainted()` had to strip SGR sequences before locating the fixture line.** The plan's contiguous `includes("fake pi ready")` cannot match a 1-cell selection at column 0, because the attribute reset splits the text (`\x1b[0;7;39;49mf\x1b[0;39;49make pi ready`), so the probe would have returned `false` for a correctly painted click. The committed probe matches the fixture text on an SGR-stripped copy and still runs the `\x1b[0;7;` paint test on the raw line (`test-support/pty-attach-x10-scenario.ts`). Approved by the controller mid-task; recorded in `task-2-report.md`.
524
- 2. **One unit row was added after the final whole-branch review.** The review found the `!wheel` guard was not load-bearing in any test (wheel `Cb` 64/65 have low bits 0/1); a row for `Cb = 67` (`64|3`, horizontal wheel) → `"press"` now pins it in Task 1's block (`test/pty-scroll.test.mjs`), commit `ae581b7`.
525
-
526
- Commit sequence as executed: `5af72cb` (spec), `79cd179` (plan), `e16179f` (parser fix + A1–A3), `be3dade` (A4–A5 component scenario), `ae581b7` (review hardening). Everything else ran as written; A6 (`npm run verify`) is green at `ae581b7` (1175 tests, 1172 pass, 0 fail; coverage 91.97 / 81.47 / 89.23).
@@ -1,32 +0,0 @@
1
- # Plan — issue #198 attach wheel forward-fallback
2
-
3
- - Spec: `docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md` (acceptance IDs from its §5)
4
- - Worktree: `.pi/worktrees/issue-198-attach-wheel-forward-fallback` on branch `issue-198-attach-wheel-forward-fallback` (base `main` @ `eab48d0`)
5
- - Flow note: same degradation as #185 — direct implementation in the parent session (the `subagent` tool is unavailable there), TDD discipline kept (red → green).
6
-
7
- ## Tasks
8
-
9
- ### T1 — production-topology regression test (A1, integration) — **red first**
10
- - Add `FAKE_PTY_STREAM_MODE=alt-flood` to `test-support/fake-pty-pi.mjs`: one-shot `\x1b[?1049h`, then absolute-positioned frames (no newline past the last row) fast enough to pass 130 KB before the attach connects.
11
- - New `test-support/pty-attach-wheel-fallback-scenario.ts` + `test/pty-attach-wheel-fallback.test.mjs`: pin `buffer.type === "normal"` and local room `0`, then require the wheel to be forwarded.
12
-
13
- ### T2 — implementation (A1)
14
- - `src/ui/pty-attach.ts` `handleMouse` wheel branch: `route === "none"` → `undefined`; `"local"` + `tryScrollBy` → handled; otherwise ( `"child"`, or `"local"` that cannot move) forward when connected, else `undefined`. Comments cite #183/#185/#198.
15
-
16
- ### T3 — keep branch coverage honest (A2, A3)
17
- - Remove the per-tick `?1049h` re-assertion from the `alt` fixture mode (it masked this bug) and force the legacy replay path in that scenario (`AGENT_BOARD_TERMINAL_SNAPSHOT=0`), documenting the premise in the header; restore the env afterwards.
18
- - Update #183's scenario comment + assertion message to the new meaning (disconnected half of the rule).
19
-
20
- ### T4 — docs (A6)
21
- - Rewrite the `README.md` PTY-attach wheel sentence to the parity rule.
22
-
23
- ### T5 — gate (A4, A5)
24
- - `npm test`, then `npm run verify` (typecheck, perf gate, tests, coverage, pack:dry).
25
-
26
- ### T6 — user verification (U1, post-merge)
27
- - Merge → `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board pull --ff-only` → restart pi → wheel vs `PageUp`/`PageDown` in a real session (this time the production topology is what the new scenario encodes, so the local gate is meaningful).
28
-
29
- ## Out of scope
30
-
31
- - Conveying the alternate-screen flag through the runner's terminal model / snapshot DTO (correctness no longer depends on it).
32
- - Any change to middle-click, selection mirroring, kill switches, or `AGENT_BOARD_WHEEL_LINES` semantics.