@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,270 +0,0 @@
1
- # Root-cause report / Spec: recognize `ask_user_question` as a pending-question tool
2
-
3
- Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-20T22:10:00+08:00
4
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-20T22:24:00+08:00 — folded in R1-D1..D5
5
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:10:00+08:00 — emitter identified and installed locally; U1 deployment path corrected from npm release to the git-install clone
6
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:22:00+08:00 — R2-D1 reference precision (blocked-event emit site)
7
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:15:00+08:00 — user chose the hybrid guard (name OR questionnaire arg shape); design, acceptance matrix, risks and testability split updated
8
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:35:00+08:00 — R3-D1..D3 (deliverables list, superseded decision row, no-id fallback boundary)
9
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-21T22:52:00+08:00 — local-CR: shape bound wording matches the implementation (empty question string still matches)
10
-
11
- - **Issue**: zhuxixi/pi-agent-board#221
12
- - **Date**: 2026-10-20
13
- - **Status**: draft — awaiting cross-review and user confirmation
14
- - **Type**: bug fix (event-reducer allow-list + regression test); no data-model, schema, or UI change
15
-
16
- ## 1. Problem
17
-
18
- A pi-style session blocked on a tool named `ask_user_question` stays in the **RUNNING**
19
- group with the row summary `Ask_user_question`. It never reaches the **Needs answer**
20
- group (`needs_input`, amber).
21
-
22
- Reproduced at reducer level on `main` (9a197f4):
23
-
24
- ```
25
- actual ask_user_question → semanticState=working, summary="Ask_user_question",
26
- pendingQuestions=[], question=null,
27
- currentTool={name:"ask_user_question"}, needsInput=false
28
- control question → semanticState=needs_input, summary="Which approach?", needsInput=true
29
- ```
30
-
31
- Evidence trail: `issue-221/research/root-cause.md`, issue comment 6292835562.
32
-
33
- ## 2. Root cause
34
-
35
- `src/core/events.mjs:21` defines an exact-membership allow-list:
36
-
37
- ```js
38
- const QUESTION_TOOL_NAMES = new Set(["ask_questions", "question", "questionnaire"]);
39
- ```
40
-
41
- Both gates are exact membership — `events.mjs:86` (`tool_execution_start`) and
42
- `events.mjs:100` (`tool_execution_end`). `ask_user_question` misses both, so
43
- `tool_execution_start` takes the generic branch (`currentTool` set, `semanticState =
44
- "working"`), `preservePendingQuestion()` has nothing to promote, and `deriveSummary()`
45
- falls back to `capitalize(name)`.
46
-
47
- `questionFromArgs()` (`events.mjs:232`) already handles the arg shape
48
- (`args.questions[].question`) — no extraction change is needed.
49
-
50
- **Emitter (confirmed 2026-10-21)**: `ask_user_question` is registered by the pi extension
51
- `@juicesharp/rpiv-ask-user-question` — `ask-user-question.ts:84`
52
- (`ASK_USER_QUESTION_TOOL_NAME = "ask_user_question"`), registered at `:455-459`. It is not a
53
- pi 1.3.0 built-in (pi ships no question tool; `question`/`questionnaire` come from
54
- `pi-question-tool`). The extension is now installed on this machine: v2.13.9,
55
- `~/.pi/agent/settings.json:90`, `~/.pi/agent/npm/node_modules/@juicesharp/rpiv-ask-user-question`,
56
- and the tool is live in the session after `/reload` (session tool list contains
57
- `question`, `questionnaire`, `ask_user_question`). Its arg schema
58
- (`{ questions: [{ question, header, options, multiSelect }] }`) is exactly what
59
- `questionFromArgs` already reads.
60
-
61
- The defect stands regardless of the emitter: any unrecognized question-tool name silently
62
- degrades grouping.
63
-
64
- ## 3. Design
65
-
66
- ### 3.1 Detection: name allow-list OR questionnaire arg shape
67
-
68
- ```js
69
- /** Tool names whose interactive execution blocks on a user answer. */
70
- const QUESTION_TOOL_NAMES = new Set(["ask_questions", "question", "questionnaire", "ask_user_question"]);
71
-
72
- /** Questionnaire args shared by pi/Claude-Code style question tools. */
73
- function isQuestionArgShape(args) {
74
- const questions = Array.isArray(args?.questions) ? args.questions : null;
75
- return Boolean(questions && questions.length > 0 && typeof questions[0]?.question === "string");
76
- }
77
-
78
- function isQuestionToolCall(name, args) {
79
- return QUESTION_TOOL_NAMES.has(name) || isQuestionArgShape(args);
80
- }
81
- ```
82
-
83
- Why both: the name set keeps the known tools working even with empty args
84
- (`ask_questions` is exercised with `args: {}` at `test/events.test.mjs:137`), while the shape
85
- check survives a renamed or newly packaged question tool as long as it keeps the
86
- `questions[0].question` schema. Neither replaces the other.
87
-
88
- **End gate becomes name-agnostic**: `tool_execution_end` carries no `args`, so a
89
- shape-matched start cannot be recognized at end time. Replace
90
- `opts.interactive && QUESTION_TOOL_NAMES.has(event.toolName ?? "")` with
91
- `opts.interactive` and let `removePendingQuestion(status, event.toolCallId)` filter by call
92
- id (it is already a no-op when the id is not pending). Tool-call ids are unique per call, so
93
- this cannot clear someone else's question.
94
-
95
- The guarantee is exact for real worker events, which always carry `toolCallId`
96
- (`pi` dist `agent-session` emits `{toolCallId,toolName,result,isError}`). The pre-existing
97
- no-id fallback (`events.mjs:242/250` map a missing id to the literal `"ask_questions"`) is
98
- unchanged; a malformed event without an id could therefore clear a pending entry that also
99
- lacked one. That path is unreachable from the real event stream and is left as-is.
100
-
101
- No other module reads question-tool names (`rg ask_questions src/` → only `events.mjs`).
102
-
103
- ### 3.2 Regression guard
104
-
105
- Two new reducer tests in `test/events.test.mjs`, next to the existing
106
- `interactive pi question tool …` test:
107
-
108
- **Test A — name path (`ask_user_question`), pinned to the literal name**
109
-
110
- - interactive start → `needs_input`, summary/question = first question text,
111
- `pendingQuestions` length 1, `currentTool === null`, `projectViewState().needsInput === true`;
112
- - interactive end for the same `toolCallId` → `working`, `question === null`,
113
- `pendingQuestions === []`;
114
- - detached reduction (no `interactive`) → `working`, `currentTool.name === "ask_user_question"`,
115
- `pendingQuestions === []`.
116
-
117
- **Test B — shape path (unknown name), plus the negative guard**
118
-
119
- - interactive start with `toolName: "mystery_questionnaire"` and
120
- `args: { questions: [{ question: "Ship it?", header: "Release", options: [{ label: "Yes" }] }] }`
121
- → `needs_input`, question `"Ship it?"`, one pending entry;
122
- - interactive end with the **same unknown name** → `working`, cleared (proves the end gate is
123
- call-id based, not name based);
124
- - negative cases that must stay `working` with an empty `pendingQuestions`:
125
- `{ questions: [] }`, `{ questions: "nope" }`, and a non-question tool (`bash`).
126
-
127
- The literal name in Test A plus the unknown-name case in Test B are the guards: dropping the
128
- name or the shape check fails `npm test` instead of silently regressing the dashboard.
129
-
130
- ### 3.3 Alternatives considered
131
-
132
- | Alternative | Verdict | Reason |
133
- |---|---|---|
134
- | Arg-shape detection instead of a name set | Rejected | `ask_questions` is exercised with `args: {}` (`test/events.test.mjs:135`) and must still be `needs_input`; shape alone cannot cover it. |
135
- | Hybrid (`set OR questions[] shape`) at drafting | Rejected, then superseded | Drafting-time verdict was to keep the diff minimal; the user gate on 2026-10-21 chose the hybrid, so this row is history only — the authoritative decision is the Adopted row below. |
136
- | Configurable allow-list | Rejected | No config surface exists for reducer behavior; over-engineering for a one-name gap. |
137
- | Export the set for tests | Rejected | The reducer is the public surface; tests already drive it directly. Exporting a private constant adds API for no gain. |
138
- | Upstream `rpiv:ask-user:blocked` event instead of name detection | Rejected | Constant defined at `events.ts:38`, emitted at `ask-user-question.ts:64` (`pi.events.emit`) — an in-process bus event on the extension host. The board consumes JSON-mode worker stdout, which never carries pi bus events, so the channel is unreachable. |
139
- | Name set OR arg-shape fallback | **Adopted** (user decision, 2026-10-21) | Survives a rename or a newly packaged question tool as long as the schema stays `questions[0].question`. Cost: any interactive tool with that exact shape is treated as a pending question; bounded by requiring a non-empty `questions[0].question` string and by the `opts.interactive` gate. |
140
-
141
- ### 3.4 Data flow (unchanged, verified)
142
-
143
- | Step | Location | Result |
144
- |---|---|---|
145
- | interactive start, name in set OR questionnaire shape | `events.mjs:86` → `upsertPendingQuestion` → `preservePendingQuestion` | `needs_input`, `currentTool=null`, `question` = first question |
146
- | summary | `derive.mjs:103` | question text wins over tool name |
147
- | projection | `events.mjs:209` | `needsInput: true` |
148
- | interactive end | `events.mjs:100` → `removePendingQuestion` (by call id) | back to `working`, `question=null` |
149
- | inline reply while pending | `src/ui/dashboard.ts:2054 isAgentBusy` + `README.md:167/299` | reply rejected; answer via attach (pre-existing contract, now also applies to this name) |
150
- | detached start/end | `opts.interactive` gate | unchanged `currentTool` behavior |
151
-
152
- ## 4. Acceptance matrix
153
-
154
- | ID | Feature point | Verification | Concrete check | Pass criteria |
155
- |----|---------------|--------------|----------------|---------------|
156
- | A1 | `ask_user_question` start is treated as a pending question | automated (unit) | `npm test` — new test in `test/events.test.mjs` | `semanticState==="needs_input"`, `summary`/`question` equal `"Which approach?"`, `pendingQuestions.length===1`, `currentTool===null`, `projectViewState(s).needsInput===true` |
157
- | A2 | `ask_user_question` end clears the pending question | automated (unit) | same test | `semanticState==="working"`, `question===null`, `pendingQuestions` empty |
158
- | A3 | Detached reduction keeps legacy behavior | automated (unit) | same test (no `interactive` opt) | `semanticState==="working"`, `currentTool.name==="ask_user_question"`, `pendingQuestions` empty |
159
- | A4 | No regression for the existing three names | automated (unit) | `npm test` (existing tests at `test/events.test.mjs:109/135/163/206/220`) | all pass unchanged |
160
- | A5 | Shape fallback: unknown tool name with questionnaire args becomes a pending question | automated (unit) | Test B in `test/events.test.mjs` | `semanticState==="needs_input"`, question `"Ship it?"`, `pendingQuestions.length===1`, `currentTool===null` |
161
- | A6 | End gate clears shape-matched calls without a name match | automated (unit) | Test B — `tool_execution_end` with the same unknown `toolName`/`toolCallId` | `semanticState==="working"`, `question===null`, `pendingQuestions` empty |
162
- | A7 | Misleading args do not trigger the shape fallback | automated (unit) | Test B negative cases: `{questions: []}`, `{questions: "nope"}`, and `bash` | `semanticState==="working"`, `pendingQuestions` empty in all three |
163
- | A8 | Repo-level static gate stays green | automated (static) | `npm run typecheck` | exit 0. Note: `tsconfig.json:9` sets `checkJs:false`, so this gate covers the `.ts` surface and is a repo regression check, not proof of this `.mjs` change; A1–A7 are the real proof |
164
- | U1 | A real board session blocked on `ask_user_question` renders under Needs answer | user test | see §5 | row under **Needs answer** with the question text as summary; end of call returns it to RUNNING |
165
-
166
- Full gate: `npm run verify` (typecheck + perf gate + tests + coverage thresholds + pack dry-run).
167
-
168
- Failure branches: A2/A6 assert clear-on-end, A3 asserts detached stays generic, A7 asserts the
169
- false-positive guard. No error path is added by this change, so no error-path item is owed.
170
-
171
- ## 5. User test (U1) — steps and timing
172
-
173
- Now executable on this machine: the emitter is installed (`@juicesharp/rpiv-ask-user-question`
174
- 2.13.9) and the tool is live after `/reload`. The remaining precondition is the **board code**
175
- itself — the board process loads from the git-install clone, not from this working checkout and
176
- not from npm:
177
-
178
- - `pi list` → `git:github.com/zhuxixi/pi-agent-board` at
179
- `~/.pi/agent/git/github.com/zhuxixi/pi-agent-board` (`~/.pi/agent/settings.json:85`), HEAD
180
- `9a197f4` (= `main` at drafting time). That clone currently has a dirty `package-lock.json`
181
- (4 deleted `"peer": true` lines), which must be discarded before pulling.
182
-
183
- Timing: after the fix merges to `main`.
184
-
185
- 1. Update the clone the board actually loads, then restart the board/Pi:
186
- `cd ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board && git checkout -- package-lock.json && git pull`
187
- (or re-run `pi install git:github.com/zhuxixi/pi-agent-board`); confirm
188
- `git log -1 --oneline` is the merged commit.
189
- 2. Restart Pi / the board service (an already-running pi process keeps the old extension code).
190
- 3. Dispatch a board session whose prompt forces a decision, so the model calls
191
- `ask_user_question` with `questions[0].question` containing a short decision question
192
- (e.g. "Which approach?"). The extension is user-level, so every session sees the tool.
193
- 4. Open the dashboard, then answer via attach.
194
-
195
- Expected observation: the row sits under **Needs answer** (amber) with the question text as
196
- its summary, `pendingQuestions` holds one entry, inline reply is refused (pre-existing
197
- contract); after answering, the row returns to **RUNNING**.
198
-
199
- If the merge has not happened by completion time, U1 is reported as `pending` (not passed);
200
- A1–A5 still prove the reducer contract.
201
-
202
- ## 6. Non-goals
203
-
204
- - No change to `detectNeedsInput()` text heuristics or `message_end` classification.
205
- - The shape fallback does **not** apply to detached (non-interactive) runs: headless sessions
206
- cannot answer, so they keep `currentTool` display.
207
- - No rename of the `"ask_questions"` fallback ids in `upsertPendingQuestion` /
208
- `removePendingQuestion` (`events.mjs:242/250`).
209
- - No config surface or UI affordance for arbitrary question-tool names.
210
- - No change to attach/reply/answer transport; only reducer classification changes.
211
-
212
- ## 7. Risks and boundaries
213
-
214
- - **False positive (accepted)**: any interactive tool whose args are
215
- `{ questions: [{ question: <string> }] }` is treated as a pending question, even if it is
216
- not asking the user. Bounded by the `opts.interactive` gate and by requiring the first
217
- `question` to be a string; an empty or whitespace-only string still matches and falls back
218
- to the generic "Answer the pending question" text, exactly like the name-set path. No
219
- installed extension is known to declare that shape for a non-question purpose today
220
- (research §4), but absence over third-party code is not provable, so the guard is the bound
221
- itself, not a completeness claim.
222
- - **End gate is now name-agnostic**: every interactive `tool_execution_end` calls
223
- `removePendingQuestion(id)`. For real worker events (which always carry `toolCallId`) ids are
224
- unique per call, so this clears the matching entry or does nothing; it cannot clear another
225
- call's question (covered by A6 and by the existing multi-question test at
226
- `test/events.test.mjs:135`). The legacy no-id fallback is unchanged and is documented in §3.1
227
- as the only way a cross-clear could occur.
228
- - **Shape fallback is a heuristic**: a tool that renames its `questions` payload field
229
- (e.g. to `prompts`) still regresses. Accepted; a schema change is a protocol change, not a
230
- rename, and cannot be absorbed by a reducer heuristic.
231
- - **Detached runs** are unaffected — headless sessions cannot answer, so they must keep
232
- `currentTool` display.
233
- - **Only JSON-mode worker events reach the reducer.** The extension's own
234
- `rpiv:ask-user:blocked` bus event is in-process and never appears in worker stdout, so the
235
- board cannot rely on it.
236
- - **Inline reply is rejected while the question is pending** (`README.md:167/299`,
237
- `src/ui/dashboard.ts:2054 isAgentBusy`). This is pre-existing behavior for pending
238
- questions and now applies to `ask_user_question` rows too; answer via attach. Expected,
239
- not a regression.
240
-
241
- ## 8. Testability split
242
-
243
- The reducer is already a pure-function seam: `createRunStatus(config, pid, now)` builds
244
- state, `reduceEvent(status, event, now, opts)` mutates it, `projectViewState(status, now)`
245
- projects it. Two new module-private pure helpers carry the new logic —
246
- `isQuestionArgShape(args)` and `isQuestionToolCall(name, args)` — and are driven through
247
- `reduceEvent`, the public surface, exactly like the existing `questionFromArgs` (which is
248
- also private and tested through the reducer).
249
-
250
- Planned split:
251
-
252
- | Unit | Purity | Test seam |
253
- |---|---|---|
254
- | `isQuestionArgShape(args)` | pure, no I/O | via `reduceEvent` start events (Test B positives/negatives) |
255
- | `isQuestionToolCall(name, args)` | pure, no I/O | via `reduceEvent`; name path in Test A, shape path in Test B |
256
- | start handler branch | side-effect-free over `RunStatus` | `reduceEvent` + `projectViewState` assertions |
257
- | end handler (call-id removal) | side-effect-free over `RunStatus` | `reduceEvent` end events (Test A, Test B, existing multi-question test) |
258
-
259
- Test boundary: `test/events.test.mjs` only. No export is added to `src/core/events.mjs`;
260
- the helpers stay private and are covered through the reducer, matching the existing pattern.
261
-
262
- ## 9. Deliverables
263
-
264
- - `src/core/events.mjs` — add `ask_user_question` to `QUESTION_TOOL_NAMES`; add the private
265
- `isQuestionArgShape(args)` / `isQuestionToolCall(name, args)` helpers; gate
266
- `tool_execution_start` on `isQuestionToolCall`; make `tool_execution_end` clear by
267
- `toolCallId` under `opts.interactive` only.
268
- - `test/events.test.mjs` — Test A (name path) and Test B (shape path + negative guards).
269
- - `docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md` — this design,
270
- committed with the fix (repo precedent: #213/#214/#215 each ship their spec with the PR).
@@ -1,225 +0,0 @@
1
- # Root-cause report / Spec: monotonic foreground state rebuild (stale "Needs answer")
2
-
3
- Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-22T23:50:00+08:00
4
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T00:05:00+08:00 — folded in R1-G1..G3 and R1-D1..D4
5
- Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T00:20:00+08:00 — R2-D1..D3 (A4 clock pin, timestamp-bound wording, forget wiring acceptance)
6
-
7
- - **Issue**: zhuxixi/pi-agent-board#225
8
- - **Date**: 2026-10-22
9
- - **Status**: draft — awaiting cross-review and user confirmation
10
- - **Type**: bug fix (foreground state mirroring); no reducer, schema, or UI change
11
-
12
- ## 1. Problem
13
-
14
- A foreground row that transiently becomes `needs_input` can stay in **Needs answer** for
15
- the rest of the turn after the question is answered. Live evidence (issue #225):
16
-
17
- ```
18
- 22:56:21 needs_input pending=[call_.../1]
19
- 22:56:44 working pending=[] ← tool_execution_end cleared it
20
- 22:56:44 needs_input pending=[call_.../1] ← stale projection re-applied
21
- 22:57:04 idle pending=[] ← agent_end finally rewrote it
22
- ```
23
-
24
- The reducer is correct (#221); the resurrection happens in the foreground mirror.
25
-
26
- ## 2. Root cause
27
-
28
- 1. `syncRowEvent` (`src/runtime/service.mjs:1533`) rebuilds status from disk on every
29
- event: `statusFromRow(row)` (`:1535`).
30
- 2. `statusFromRow` (`:814-843`) copies `semanticState` / `summary` / `question` /
31
- `pendingQuestions` but **not `updatedAt`**, so the rebuild has no freshness signal.
32
- 3. `writeForegroundState` (`:854-880`) stamps a new `updatedAt` through
33
- `projectViewState(status, Date.now(), …)`, and every caller fires it as
34
- `void writeForegroundState(...)` (`:1571`, `:1629`). Two events can therefore have two
35
- `sync_foreground` commands in flight, and the older projection can materialize last.
36
- 4. The next event reduces onto that stale status (it cannot tell it is stale) and writes it
37
- back; the pending entry survives until `agent_end` finalizes the row.
38
- 5. `foregroundPreviewCache` (#113) restores only `latestAssistantPreview` and
39
- `lastAgentActivityAt`, keyed on `lastAgentActivityAt` — tool events never stamp that
40
- field, so question state is uncovered.
41
-
42
- Additional constraint: `mark_completed` / `mark_holding` / `patch_fields`
43
- (`src/core/state-commands.mjs`) do **not** stamp `state.updatedAt`, so a timestamp-only
44
- guard could resurrect a pre-verdict projection over a manual verdict. The manual-verdict
45
- fence (`isManualVerdict`) must gate the adoption.
46
-
47
- ## 3. Design
48
-
49
- ### 3.1 New pure module: `src/core/foreground-projection-cache.mjs`
50
-
51
- Module-level shared instance, same rationale as `foreground-preview-cache` (service
52
- instances are created per call, so a per-instance cache is discarded between events).
53
-
54
- The cache stores a **status-shaped snapshot**, not the `ViewState` projection: the
55
- projection calls the active tool `latestTool`, while `RunStatus` calls it `currentTool` with
56
- a `summary` (`src/core/events.mjs:225`, `src/runtime/service.mjs:826`). Storing the status
57
- keeps the whitelist aligned with `RunStatus` and avoids an implicit shape mapping.
58
-
59
- ```js
60
- createForegroundProjectionCache(): {
61
- nextStamp(viewId): number // strictly increasing per view
62
- remember(viewId, status, stamp): void // snapshot the whitelisted fields
63
- adopt(viewId, status): boolean // copy the snapshot when strictly newer
64
- forget(viewId): boolean
65
- clear(): void
66
- size(): number
67
- }
68
- ```
69
-
70
- - `nextStamp(viewId)` returns `max(Date.now(), lastStamp(viewId) + 1)`. Using it for
71
- `projectViewState`'s `now` argument makes every foreground `updatedAt` strictly increasing
72
- per view, so two events in the same millisecond can never tie and an older projection can
73
- never outrank a newer one inside this process.
74
- - `remember` snapshots this whitelist from the status:
75
- `semanticState`, `processState`, `summary`, `currentTool`, `question`,
76
- `pendingQuestions`, `error`, `lastActivityAt`, `latestAssistantPreview`,
77
- `lastAgentActivityAt`, plus the `stamp`. Nested values (`currentTool`,
78
- `pendingQuestions`) are cloned **on remember**, so a later mutation of the caller's status
79
- cannot leak into the cache.
80
- - `adopt(viewId, status)` compares `entry.stamp` with `status.updatedAt ?? 0`. Only a
81
- strictly newer entry is adopted; it copies the same whitelist (cloning nested values
82
- again, so `adopt` → caller mutation → second `adopt` is stable) and sets
83
- `status.updatedAt = entry.stamp`. Returns whether anything changed.
84
- - Nothing else (evidence `review`, `diagnostics`, `autoState`, `lastVisitedAt`, `steering`,
85
- `followUps`) is touched — those have their own writers and freshness rules.
86
-
87
- ### 3.2 Wiring in `src/runtime/service.mjs`
88
-
89
- | Point | Change |
90
- |---|---|
91
- | `statusFromRow` (`:814`) | add `updatedAt: typeof s.updatedAt === "number" ? s.updatedAt : 0` (in-memory freshness only; `RunStatus` gains an optional `updatedAt` typedef in `src/core/types.mjs`) |
92
- | `writeForegroundState` (`:854`) | `const stamp = foregroundProjectionCache.nextStamp(row.meta.id)`; pass `stamp` instead of `Date.now()` to `projectViewState`; then `foregroundProjectionCache.remember(row.meta.id, status, stamp)` |
93
- | `syncRowEvent` (`:1540`) | after the preview backfill: `foregroundProjectionCache.adopt(row.meta.id, status)` before `reduceEvent` |
94
- | cache eviction (`:775`, `:2131`) | call `foregroundProjectionCache.forget(viewId)` next to the existing `foregroundPreviewCache.forget(viewId)` |
95
-
96
- `row` is loaded from disk at the event entry (`rowForSession` → `loadRow`), before any
97
- `await`, so `row.state` is as fresh as `readState(root, viewId)` at that point.
98
-
99
- The preview cache stays as-is: its #113 semantics (gap-fill when the timestamp ties) are
100
- still useful, and folding it into the new cache is a separate refactor (non-goal).
101
-
102
- ### 3.3 Why not serialize the writes (issue option 2)
103
-
104
- A module-level write queue would fix the ordering at the source, but it adds a new failure
105
- mode (one stalled coordinator round trip blocks every later event) and needs its own
106
- backoff/timeout policy. The monotonic rebuild fixes the *read* side, so it also covers
107
- future writers (dashboard patches, reconcile) and any out-of-order arrival that is not a
108
- `sync_foreground` command. Option 2 remains a possible follow-up, not this fix.
109
-
110
- ### 3.4 Manual verdicts (boundary, no new fence)
111
-
112
- Adoption is a pure comparison: adopt only when `entry.stamp > status.updatedAt`. Because
113
- `nextStamp` can run ahead of the wall clock during a sub-millisecond burst, a writer that
114
- stamps with a bare `Date.now()` *can* compare older than the cache — the timestamp does not
115
- order against every writer.
116
-
117
- Manual verdicts do not rely on that comparison. Under the coordinator, `mark_completed` /
118
- `mark_holding` writes are materialized without stamping `updatedAt`
119
- (`state-commands.mjs:288-334`), and any later `sync_foreground` from `service` is rejected
120
- with `manual_fence` (`state-commands.mjs:282-285`). In coordinator-disabled mode the direct
121
- writers (`completeViewDirect`, `holdViewDirect`, `archiveStateDirect`, reconcile) *do* stamp
122
- `Date.now()`, but that path already lets a foreground event overwrite the semantic state
123
- through `reduceEvent` today, independent of this cache, so adoption adds no new way to
124
- resurrect a verdict. No extra fence is added; the existing `manual_fence` and the
125
- `agent_end` branch fence remain the protections, and A5 pins the observable behavior.
126
-
127
- ### 3.5 Data flow after the change
128
-
129
- | Event | Behavior |
130
- |---|---|
131
- | start question | `reduceEvent` → `needs_input`; write uses stamp S1 and remembers it |
132
- | end question | rebuild from disk (maybe stale) → adopt if cache is newer → `reduceEvent` clears pending → write uses S2 > S1 |
133
- | subsequent tool event | rebuild carries S2 → adopt is a no-op (equal) → `reduceEvent` keeps `working`; never resurrects `needs_input` |
134
- | dashboard write | disk `updatedAt` is newer than the cache → nothing adopted |
135
- | agent_end | finalize as today |
136
-
137
- ## 4. Acceptance matrix
138
-
139
- | ID | Feature point | Verification | Concrete check | Pass criteria |
140
- |----|---------------|--------------|----------------|---------------|
141
- | A1 | `remember`/`adopt` respect strict freshness | automated (unit) | `test/foreground-projection-cache.test.mjs` | newer stamp adopted (`true`); equal and older stamps not adopted (`false`), status unchanged |
142
- | A2 | Whitelist copy is exact and isolated | automated (unit) | same file | all whitelisted fields copied; non-whitelisted untouched; mutating `status.pendingQuestions[0]` after `adopt` does not change a second `adopt`; mutating the status after `remember` does not change the stored snapshot |
143
- | A3 | `nextStamp` is strictly increasing per view | automated (unit) | same file | two calls in a row return `b > a`, even when `Date.now()` is stubbed to a constant; different view ids do not share the counter |
144
- | A4 | Out-of-order materialization no longer resurrects `needs_input` | automated (integration) | `test/service.test.mjs`, coordinator disabled, `Date.now` pinned to a constant for the whole test so the pre-fix same-millisecond tie is deterministic: question start → capture `state.json` (its `updatedAt` is the start stamp) → question end → `writeState(captured snapshot)` (an older stamp) → `tool_execution_start` for `bash` | persisted state is `working`, `pendingQuestions` empty, `needsInput` false. Pre-fix this asserts `needs_input` (the disk rebuild wins the tie), post-fix the monotonic stamp adopts the newer cached status |
145
- | A5 | A manual verdict survives a foreground event | automated (integration) | `test/service.test.mjs`: seed the projection cache with an accepted-but-unmaterialized beat, write `completed` + `autoState: null` with an older `updatedAt`, then a `tool_execution_start` that adopts the cached snapshot | persisted state stays `completed` (coordinator answers `manual_fence`). Pins the fence, not adoption isolation — adoption has no independent observable there |
146
- | A6 | `forget` drops the entry and is wired into archive | automated (unit + integration) | cache test: `remember` → `forget` → `adopt` false; `test/service.test.mjs`: `archiveByState` on a remembered view | `adopt` returns `false` after `forget`; after archiving, `foregroundProjectionCache.size()` no longer counts that view |
147
- | A7 | Existing foreground behavior unchanged | automated (unit/integration) | `npm test` (existing `syncForegroundEvent` and preview-cache tests) | all pass |
148
- | A8 | Repo static gate | automated (static) | `npm run typecheck` | exit 0 (`checkJs:false`, so this is a repo regression gate, not proof of the `.mjs` change) |
149
- | U1 | Live: answering a question returns the row to RUNNING before the turn ends | user test | see §5 | row leaves **Needs answer** within ~1s of the answer and stays RUNNING until the turn ends |
150
-
151
- Full gate: `npm run verify` (typecheck + perf + tests + coverage thresholds + pack dry-run).
152
-
153
- Failure branches: A1/A2 assert the negative side (equal/older stamps are not adopted, nested
154
- values cannot leak), A5 asserts the manual-verdict boundary, A6 asserts cache eviction. No
155
- new error path is introduced.
156
-
157
- ## 5. User test (U1)
158
-
159
- Executable after merge, on this machine (the board clone at
160
- `~/.pi/agent/git/github.com/zhuxixi/pi-agent-board` is updated to the merged commit and Pi
161
- restarted):
162
-
163
- 1. In a session, trigger a question (`ask_user_question` or `question`).
164
- 2. Watch the row enter **Needs answer**; answer it in the dashboard/attach.
165
- 3. Keep watching for the rest of the turn.
166
-
167
- Expected: the row returns to **RUNNING** within about a second of the answer — not at
168
- `agent_end`. Repeat with a nested call (question asked from inside `codemode`) to exercise
169
- the wider window.
170
-
171
- If the environment cannot be updated at completion time, U1 is reported as `pending`.
172
-
173
- ## 6. Non-goals
174
-
175
- - No change to `reduceEvent`, `deriveSummary`, or the question-tool detection (#221 is done).
176
- - No serialization of `sync_foreground` writes (issue option 2) and no coordinator changes.
177
- - No removal or rewrite of `foreground-preview-cache.mjs` (#113 behavior preserved).
178
- - No changes to manual verdicts, auto-state classification, or evidence/diagnostics writes.
179
-
180
- ## 7. Risks and boundaries
181
-
182
- - **Clock ties**: prevented inside this process by `nextStamp` (strictly increasing per
183
- view). Ties are still possible with a *different* writer (another Pi process) whose
184
- `Date.now()` equals ours; then neither side is "strictly newer" and the disk value wins,
185
- which is the pre-existing behavior. Cross-process ordering is not guaranteed.
186
- - **Cross-process writers**: another Pi process mirroring the same view writes its own
187
- `updatedAt`; this cache only protects against out-of-order writes from *this* process.
188
- Outside the supported single-host model; documented, not fixed.
189
- - **In-process writers with a bare `Date.now()`**: the auto-state classifier
190
- (`applyAutoStateToViewState`, `src/core/auto-state.mjs:254`) stamps a wall-clock
191
- `updatedAt`, which can compare older than a cache stamp that ran ahead within the same
192
- millisecond. A late foreground tool event would then adopt the pre-classification
193
- snapshot — but such an event already clobbers the classification through `reduceEvent`
194
- today (it sets `working` and `alive`), so this is not a new resurrection path.
195
- - **Manual verdicts**: protected by the coordinator's `manual_fence` and the `agent_end`
196
- branch fence, not by timestamps. Coordinator materialization does not stamp `updatedAt`
197
- for verdict writes; coordinator-disabled direct writers do stamp `Date.now()`, which can
198
- compare older than a cache stamp that ran ahead of the clock — that path is not ordered by
199
- this cache (see §3.4, A5).
200
- - **Memory**: one small object per view, evicted with the existing `forget` sites.
201
-
202
- ## 8. Testability split
203
-
204
- | Unit | Purity | Test seam |
205
- |---|---|---|
206
- | `createForegroundProjectionCache()` (`nextStamp` / `remember` / `adopt` / `forget`) | pure (Map in/out, tested with a stubbed clock) | direct unit tests (`test/foreground-projection-cache.test.mjs`) |
207
- | `statusFromRow` `updatedAt` passthrough | pure | exercised through `syncForegroundEvent` (A4) |
208
- | adoption in `syncRowEvent` | side-effect-free call + thin I/O shell | `syncForegroundEvent` with coordinator disabled (A4), deterministic `writeState` between events; A5 uses the tracked coordinator |
209
- | stamp assignment in `writeForegroundState` | pure call inside the existing shell | A4 (the repro fails without monotonic stamps: the stale snapshot would tie) |
210
- | write-side `remember` | pure call inside the existing shell | A4 (the repro fails without it) |
211
-
212
- Test boundary: new pure module tests + additions to `test/service.test.mjs`; no new exports
213
- from the service.
214
-
215
- ## 9. Deliverables
216
-
217
- - `src/core/foreground-projection-cache.mjs` — new pure module.
218
- - `src/runtime/service.mjs` — `statusFromRow` `updatedAt`; `nextStamp` + `remember` in
219
- `writeForegroundState`; `adopt` in `syncRowEvent`; `forget` at the eviction sites.
220
- - `src/core/types.mjs` — optional `updatedAt` on the `RunStatus` typedef (in-memory
221
- freshness field).
222
- - `test/foreground-projection-cache.test.mjs` — new unit tests (A1–A3, A6).
223
- - `test/service.test.mjs` — A4/A5 integration tests.
224
- - `docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md` — this
225
- design, committed with the fix.