@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,125 +0,0 @@
1
- # issue #87 plan:legacy 死 host 安全回收实现
2
-
3
- 日期:2026-09-08 · spec:docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md
4
-
5
- ## 任务拆解
6
-
7
- ### T1:纯决策函数 `canFinalizeLegacyHost`(A1)
8
-
9
- **文件**:`src/core/host-coordination.mjs`(新增导出,放 `classifyProbeResult` 附近)
10
-
11
- ```js
12
- /**
13
- * Whether a legacy (pre-instanceId) host record can be safely finalized as
14
- * exited: the runner pid is provably dead (a reused pid reads alive →
15
- * conservative false) AND the endpoint is unreachable. Satisfies spec §10.1's
16
- * "never recovered" conservatism — recovery only fires when identity is
17
- * certain (issue #87).
18
- * @param {{ host: HostStatus|null, hostPid: number|null|undefined, hostPidAlive: boolean, probeClassification: string }} input
19
- * @returns {boolean}
20
- */
21
- export function canFinalizeLegacyHost({ host, hostPid, hostPidAlive, probeClassification }) {
22
- if (!host || host.instanceId != null) return false;
23
- if (host.state !== "starting" && host.state !== "alive") return false;
24
- if (hostPid == null || hostPidAlive) return false;
25
- return probeClassification === "missing" || probeClassification === "stale";
26
- }
27
- ```
28
-
29
- **测试**:`test/host-coordination.test.mjs` 新增真值表用例(在文件既有风格下):
30
- - 全条件满足(starting/alive × missing/stale 四组合)→ true
31
- - instanceId 非 null → false
32
- - state ∈ {exited, failed, stopping} → false
33
- - hostPid null / hostPidAlive true → false
34
- - classification ∈ {ready, starting, occupied, unknown} → false
35
- - host null → false
36
-
37
- ### T2:resolver 两处改造(A2-A5)
38
-
39
- **文件**:`src/runtime/service.mjs`
40
-
41
- 1. **import 更新**:`readHostPid` 加入 store.mjs import 列表;`canFinalizeLegacyHost` 加入 host-coordination.mjs import(L28 现有 `canReplaceHost` 同处)。
42
-
43
- 2. **新增模块内 helper**(resolver 函数前,模块作用域):
44
-
45
- ```js
46
- /**
47
- * Finalize a provably-dead legacy host as exited so the resolver loop's next
48
- * iteration sees hostActive=false and claims a fresh new-protocol host
49
- * (issue #87). Legacy records have no instanceId — no concurrent owner exists,
50
- * so an unfenced writeHost is safe; the claim path is serialized by the
51
- * host-start lease.
52
- * @returns {boolean} true when finalized (caller should `continue` the loop).
53
- */
54
- function finalizeDeadLegacyHost(root, viewId, host, probeClassification) {
55
- const hostPid = Object.hasOwn(host, "runnerPid") ? host.runnerPid : readHostPid(root, viewId);
56
- if (!canFinalizeLegacyHost({ host, hostPid, hostPidAlive: isAlive(hostPid), probeClassification })) return false;
57
- writeHost(root, { ...host, state: "exited", endedAt: Date.now(), lastSeenAt: Date.now(), error: "legacy host finalized: runner pid dead (issue #87)" });
58
- try {
59
- appendDiagnostic(root, viewId, {
60
- source: "service", level: "info", code: "legacy_host_finalized",
61
- message: `Finalized stale legacy host (pid ${hostPid} dead, probe ${probeClassification}) — next attach claims a fresh host`,
62
- details: { hostPid, probeClassification, previousState: host.state },
63
- });
64
- } catch { /* best effort */ }
65
- return true;
66
- }
67
- ```
68
-
69
- 注意:`writeHost` 签名是 `(root, host)`(host 内含 viewId),与 updateOwnedHost 不同。appendDiagnostic 签名参照文件内现有调用(`appendDiagnostic(root, viewId, {...})`,如 recoverHost 附近用法——实现时以现有调用为准)。
70
-
71
- 3. **resolver legacy alive 分支**(现 L963 附近):
72
-
73
- ```js
74
- } else if (legacy) {
75
- // Legacy host unreachable. spec §10.1 says never recover — unless the
76
- // runner pid is provably dead, in which case finalize and self-heal
77
- // (issue #87).
78
- if (finalizeDeadLegacyHost(root, viewId, host, probe.classification)) {
79
- await sleepFnImpl(HOST_PROBE_RETRY_MS);
80
- continue;
81
- }
82
- return pending(sessionFile, "legacy host unreachable — manual restart needed");
83
- }
84
- ```
85
-
86
- 4. **resolver starting-legacy 分支**(现 L953-957 附近,`withinGrace || legacy` 等待分支):
87
-
88
- ```js
89
- if (withinGrace || legacy) {
90
- // A legacy starting host whose runner is provably dead would wait out
91
- // the grace window forever (withinGrace is always true for legacy) —
92
- // finalize it now instead (issue #87).
93
- if (legacy && finalizeDeadLegacyHost(root, viewId, host, probe.classification)) {
94
- await sleepFnImpl(HOST_PROBE_RETRY_MS);
95
- continue;
96
- }
97
- // Normal cold start (or a legacy starting host — legacy is never recovered,
98
- // spec §10.1): wait out the grace window.
99
- await sleepFnImpl(HOST_PROBE_RETRY_MS);
100
- continue;
101
- }
102
- ```
103
-
104
- (此分支 probe 已在上方 L939 执行,classification 可得。)
105
-
106
- **测试**:`test/host-resolver.test.mjs` 新增 4 个测试(fixtures 复用 hostRecord/scriptProbe/instantSleep/resolverService;launchHost override 模仿现有 adoption 测试的成功 spawn 模式):
107
-
108
- - **A2 自愈闭环**:legacy alive host(hostRecord:instanceId:null、state:"alive"、**无 runnerPid 属性**——用 delete 或构造时排除)+ host-pid.json 写死 pid(writeHostPid 或 readHostPid 对应写法,参照 store.mjs 导出;用不可能存活的 pid 如 999999——注意须确认该 pid 在测试机不存在,现有 fixture aliveHost 已用 999999 表达"死 pid",同法)+ scriptProbe(["missing"]) + launchHost override 成功 spawn → 断言:resolveAttachTarget 最终返回 kind:"pty"(新 host);readHost 状态变迁——最终 host 是新 instance(launchHost 写入 starting);diagnostics 含 legacy_host_finalized。
109
- - 实现细节:claim/launch 后 readHost 读到的是新 host 记录,验证 finalize 发生要靠 diagnostics 断言 + resolve 结果非 pending。
110
- - **A3 保守分支**:同上但 host-pid.json 写 `process.pid`(活 pid)→ 断言返回 pending(reason 含 "legacy host unreachable")且 host.json 未被改写(state 仍 alive)。
111
- - **A4 starting-legacy 回收**:legacy starting host(claimAt 久远超 grace,pid 死)+ probe missing + launchHost spawn 成功 → 断言不自 deadline timeout、最终 kind:"pty"、diagnostics 含 legacy_host_finalized。
112
- - **A5 unknown 不回收**:pid 死 + scriptProbe(["unknown"]) → 断言 pending 且不写盘。
113
-
114
- ⚠️ fixture 要点:hostRecord 默认写 `runnerPid: null`——这会让 `Object.hasOwn(host, "runnerPid")` 为 true,fallback 不到 host-pid.json。legacy fixture 必须构造**没有 runnerPid 属性**的记录(hostRecord 加 `legacy: true` 选项删除 instanceId/runnerPid 等字段,或新写 `legacyHostRecord` helper),并配 `writeHostPid(root, viewId, pid)` 写镜像。
115
-
116
- ### T3:全量回归(A6)
117
-
118
- `npm test` + `npm run typecheck`(基线 602)。
119
-
120
- ## 验收对账
121
-
122
- - A1 → T1 真值表单测
123
- - A2/A3/A4/A5 → T2 四个集成测试
124
- - A6 → T3
125
- - U1(用户实测,合并前):本机 6 个真实卡死 legacy view attach 验证——主 session 合并前提示用户执行,或合并后实测反馈。
@@ -1,56 +0,0 @@
1
- # issue #86 plan:spawn 异步 error 兜底实现
2
-
3
- 日期:2026-09-08 · spec:docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md
4
-
5
- ## 任务拆解
6
-
7
- ### T1:launch.mjs spawnDetached helper + 4 处收敛(A1, A2)
8
-
9
- **改动**(`src/core/launch.mjs`):
10
- 1. 文件底部(或 import 后)新增模块内 helper:
11
- ```js
12
- function spawnDetached(command, args, cwd) {
13
- const child = spawn(command, args, {
14
- cwd,
15
- detached: true,
16
- stdio: "ignore",
17
- env: process.env,
18
- // Windows: detached children get their own console window unless
19
- // suppressed (CREATE_NO_WINDOW; no-op on POSIX) — issue #49.
20
- windowsHide: true,
21
- });
22
- // Swallow async spawn failures (e.g. transient ENOENT on the node binary):
23
- // without an 'error' listener the EventEmitter rethrows as uncaughtException
24
- // and takes down the whole host Pi process (issue #86). Callers already
25
- // record state "failed" via the pid == null branch.
26
- child.on("error", () => {});
27
- child.unref();
28
- return child;
29
- }
30
- ```
31
- 2. `launchRun` / `launchHost` / `launchTitle` / `launchAutoState` 四处的 `spawn(...)` + `child.unref()` 收敛为 `const child = spawnDetached(node, [opts.runnerScript, configPath], config.cwd);`,返回值逻辑(`child.pid ?? null`、writePid 等)不变。
32
-
33
- **测试**(`test/launch.test.mjs`,复用现有注入模式):
34
- - 新增 1 个测试:注入 `node: "/nonexistent/node-ENOENT-test"`,依次调用 4 个入口,断言:各自正常返回 `{ pid: null }`;`process.on("uncaughtException")` spy 不被触发;等待 ~300ms 让异步 error 有机会冒泡(进程崩 = 测试天然失败)。
35
- - 现有 4 个测试不动(A2 回归)。
36
-
37
- ### T2:pty-attach.ts openExternalTarget error 兜底(A3)
38
-
39
- **改动**(`src/ui/pty-attach.ts` L1150-1167):三分支统一为:
40
- ```ts
41
- const child = spawn("xdg-open", [sanitized], { detached: true, stdio: "ignore" });
42
- child.on("error", () => {});
43
- child.unref();
44
- ```
45
- (darwin `open`、win32 `cmd /c start` 同理)。参照 L847-848 xclip 既有模式。
46
-
47
- ### T3:全量回归(A4)
48
-
49
- `npm test` + `npm run typecheck`,568+ 全绿。
50
-
51
- ## 验收对账
52
-
53
- - A1 → T1 新测试
54
- - A2 → T1 既有测试回归
55
- - A3 → T2 代码审查 + `rg 'child.on\("error"' src/ui/pty-attach.ts` 断言三分支(A3 static)
56
- - A4 → T3
@@ -1,96 +0,0 @@
1
- # issue #90 plan:defaultModel 失效防护实现
2
-
3
- 日期:2026-09-08 · spec:docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md
4
-
5
- ## 任务拆解
6
-
7
- ### T1:core 匹配函数 + service 注入(A1, A4)
8
-
9
- 1. `src/core/launch-options.mjs` 新增导出(spec D1.1 签名原样):
10
- ```js
11
- export function modelRefAvailable(modelRef, availableModels) {
12
- const ref = String(modelRef ?? "").trim().toLowerCase();
13
- if (!ref) return true; // no constraint
14
- if (!availableModels || availableModels.length === 0) return true; // can't judge → allow
15
- return availableModels.some((m) => `${m.provider}/${m.id}`.toLowerCase() === ref);
16
- }
17
- ```
18
- 2. 单测:若已有 `test/launch-options*.test.mjs` 则加入,否则新建。真值表:精确匹配(大小写混合)→ true;不匹配 → false;null/空串 → true;availableModels undefined/[] → true;部分匹配("glm-5.3" 不配 provider)→ false。
19
- 3. `src/ui/dashboard.ts` L1738 `findLaunchModelByRef` 改为内部调用 core 的匹配逻辑(保持返回 LaunchModel 对象:find 复用 `modelRefAvailable` 不可行——它返回 boolean;改为直接复用 core 里新增的共享比较,或最简单:dashboard 本地函数改为 `models.find(m => modelRefAvailable(ref, [m])) ?? null`。选最简单且单一事实源的写法)。
20
- - 注:若这步让 diff 变脏(import 路径等),可以跳过,dashboard 本地函数保持——在 plan 偏差里说明即可。优先级低。
21
-
22
- ### T2:service 校验接线(A2, A3, A5)
23
-
24
- 1. `createService` opts 新增:`availableModels`(`() => Array<{provider:string,id:string}> | undefined`;默认 undefined → 跳过校验)。JSDoc typedef 更新(文件内 createService opts 注释处)。
25
- 2. service.mjs 模块内新增 helper:
26
- ```js
27
- /** @returns {string|null} error message when the view's defaultModel is provably unavailable. */
28
- function validateViewModelMeta(meta) {
29
- const model = meta.defaultModel ?? null;
30
- if (!model || !optsAvailableModels) return null;
31
- const list = optsAvailableModels() ?? undefined;
32
- if (modelRefAvailable(model, list)) return null;
33
- return `Model "${model}" configured for this session is no longer available — update the view's model (or clear defaultModel) and retry attach.`;
34
- }
35
- ```
36
- (注意 availableModels() 调用本身 try/catch → undefined。)
37
- 3. `startHostUnderLease`(L205):在 canReplaceHost 检查之后、instanceId 生成之前插入:
38
- ```js
39
- const modelError = validateViewModelMeta(meta);
40
- if (modelError) return { ok: false, error: modelError };
41
- ```
42
- (校验失败不留 claim、不 spawn。)
43
- 4. `adoptClaimedHost`(L775 起):config 构建前插入校验;失败时:
44
- ```js
45
- updateOwnedHost(root, viewId, instanceId, (h) => ({ ...h, state: "failed", endedAt: nowImpl(), exitCode: 1, error: modelError, claimPid: null, claimIdentity: null }));
46
- return { ok: true, pending: true, socketPath: null, instanceId };
47
- ```
48
- (对齐该函数既有 spawnError 失败处理模式。)
49
- 5. `src/index.ts` serviceFor 与 `src/commands/agent-board.ts` flag 路径注入:
50
- ```ts
51
- availableModels: () => { try { return ctx.modelRegistry.getAvailable(); } catch { return undefined; } },
52
- ```
53
- (agent-board.ts flag 路径 L115 的 createService 调用同款注入;dashboard 路径 L95 已有 availableModels 变量但那是 UI deps,service 注入仍需单独传。)
54
- 6. 集成测试(`test/host-resolver.test.mjs` 或 `test/service.test.mjs`,看 ensure 测试在哪更顺):
55
- - A2:view meta defaultModel="glm/glm-5.3",注入 availableModels: () => [{provider:"zai-coding-cn",id:"glm-5.3"}],launchHost spy → ensureHost("v1") 断言 ok:false、error 含模型名、spy 零调用、readHost 无 starting 残留;
56
- - A3 对照:availableModels 含 glm/glm-5.3 → 正常 claim+spawn(断言 started/started pending 路径与现状一致);
57
- - A4:不注入 availableModels → 跳过校验正常 launch;
58
- - A5:废弃 starting claim(adopt 路径,参照现有 adoption 测试 fixture)+ 失效模型 → 断言落 failed、不 spawn。
59
-
60
- ### T3:runner exit 归因(A6, A7)
61
-
62
- 1. `src/core/heuristics.mjs` 新增导出:
63
- ```js
64
- /**
65
- * Last non-empty visible line of a raw terminal log chunk (ANSI/OSC stripped,
66
- * per-line carriage-return resolved). @returns {string|null}
67
- */
68
- export function lastVisibleLogLine(text, maxLen = 200)
69
- ```
70
- 实现:strip `/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g`(OSC)→ `/\x1b\[[0-9;?]*[ -/]*[@-~]/g`(CSI)→ 其他 `\x1b.` 单字符转义 → 按 `\n` 切分 → 每行取最后一个 `\r` 之后段 → trim → 过滤空 → 取最后一行 → truncate(maxLen)(复用同文件 truncate)。
71
- 2. 单测(heuristics 测试文件):ANSI 颜色、OSC 链接、`\r` 覆盖行、空行跳过、截断、空输入 → null。
72
- 3. `runner/pty-runner.mjs` child exit 回调(L213-223 区域):
73
- ```js
74
- if (!crashed) {
75
- let exitError = null;
76
- if (exitCode !== 0) {
77
- try {
78
- const tail = readScreenLogTail(screenLog, 8192); // openSync/readSync seek 尾部
79
- exitError = lastVisibleLogLine(tail);
80
- } catch { /* best effort */ }
81
- }
82
- update({ state: "exited", endedAt: Date.now(), exitCode, childPid: null, ...(exitError ? { error: exitError } : {}) });
83
- ...
84
- }
85
- ```
86
- (screenLog 变量在 L62 作用域内可见;确认该回调能访问到。readScreenLogTail 写成 runner 内私有小函数。)
87
- 4. 集成测试(test/pty-runner.integration.test.mjs):用 test-support/ 下 fake pi 模式(看 fake-slow-start-pi.mjs;若无"立即 exit 1 + 输出错误"的 fake 就新增一个 `fake-failing-pi.mjs`:打印 `Error: Model "glm/glm-5.3" not found.` 后 exit 1)→ spawn runner → 等 exit → 断言 readHost 的 error 含 "Model" / "not found";对照 exit 0(现有 fake)→ error 字段保持 null。注意 hasNodePty skip 守卫参照现有测试。
88
-
89
- ### T4:全量回归(A8)
90
-
91
- `npm test` + `npm run typecheck`(基线 608)。
92
-
93
- ## 验收对账
94
-
95
- - A1 → T1.2 · A2/A3/A4/A5 → T2.6 · A6 → T3.2 · A7 → T3.4 · A8 → T4
96
- - U1(用户实测,合并后):view_539a5e9e20 恢复失效 defaultModel → attach 看 notify + diagnostics 不再累积;改回有效模型 attach 成功。
@@ -1,267 +0,0 @@
1
- # claimPid-Blocks-Replace Fix 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:** Fix issue #99 — an exited/failed host whose `claimPid` (the dashboard process) is still alive must be replaceable, so attach no longer pends to `host start timed out`.
6
-
7
- **Architecture:** The fix relaxes one pure decision function (`canReplaceHost` in `src/core/host-coordination.mjs`): the claim role no longer participates in the replacement gate for terminal hosts, because claim protection (a launcher mid-transaction between claim and spawn) only matters while a host is `starting`. The observation helper (`observeHostForReplace` in `src/runtime/service.mjs`) drops its now-unused `claimObservation` field.
8
-
9
- **Tech Stack:** Node.js (node:test, node:assert/strict), plain ESM modules, no new dependencies.
10
-
11
- **Spec:** `docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md`
12
-
13
- ## Global Constraints
14
-
15
- - Change only the claim-role gate semantics; runner/child unknown observations must still block replacement (spec A2), `launchLeaseActive` must still block (spec A3), non-terminal hosts must still refuse (spec A4).
16
- - `canReplaceHost` signature loses `claimObservation`; `observeHostForReplace` stops computing it (one fewer `process.kill(pid, 0)` syscall).
17
- - All existing tests in `test/host-coordination.test.mjs` and `test/host-resolver.test.mjs` must keep passing (no behavioral regressions outside the claim-role gate).
18
- - `npm run typecheck` must pass (service.mjs is .mjs but the repo runs tsc over TS sources; keep JSDoc types consistent).
19
- - No changes to pty-runner.mjs, store.mjs, or host.json structure (spec non-goals).
20
-
21
- ## Acceptance traceability
22
-
23
- | Spec ID | Plan coverage |
24
- |---------|---------------|
25
- | A1 (exited/failed + live claimPid → replaceable) | Task 1 Step 1 test `canReplaceHost allows replacing a terminal host whose claimPid is still alive` |
26
- | A2 (runner/child unknown still blocks) | Task 1 Step 1 test `canReplaceHost still refuses unknown runner/child observations` + legacy case 1 of `canReplaceHost refuses unknown observations` |
27
- | A3 (launchLeaseActive still blocks) | Task 1 Step 1 legacy case 3 of `canReplaceHost refuses unknown observations` |
28
- | A4 (non-terminal hosts refuse; host null) | Task 1 Step 1 test `canReplaceHost refuses non-terminal hosts and null host` |
29
- | A5 (foreign/not_started releasable) | Task 1 Step 1 test `canReplaceHost SAFE_TO_RELEASE boundary regression` |
30
- | A6 (resolver integration: exited + live claimPid → new host) | Task 2 |
31
- | U1 (real dashboard attach) | Post-implementation manual task (Task 3) |
32
-
33
- ---
34
-
35
- ### Task 1: Relax `canReplaceHost` claim-role gate + drop `claimObservation` (A1-A5)
36
-
37
- **Files:**
38
- - Modify: `src/core/host-coordination.mjs:60-80` (canReplaceHost + its JSDoc)
39
- - Modify: `src/runtime/service.mjs:2006-2013` (observeHostForReplace)
40
- - Test: `test/host-coordination.test.mjs:30-35`
41
-
42
- **Interfaces:**
43
- - Consumes: `SAFE_TO_RELEASE` set (already defined at `host-coordination.mjs:57`).
44
- - Produces: `canReplaceHost({ host, runnerObservation, childObservation, launchLeaseActive }) → boolean` — the `claimObservation` parameter is REMOVED. Sole caller `observeHostForReplace` (service.mjs, internal, not exported) stops passing it. Task 2's integration test relies on this behavior only indirectly (through `resolveAttachTarget`), so no signature dependency.
45
-
46
- - [ ] **Step 1: Write the failing tests**
47
-
48
- In `test/host-coordination.test.mjs`, first EDIT the existing `canReplaceHost refuses unknown observations` test (line 30-35) to drop the `claimObservation` argument from all three assertions (the parameter is being removed):
49
-
50
- ```js
51
- test("canReplaceHost refuses unknown observations", () => {
52
- const host = { state: "failed" };
53
- assert.equal(canReplaceHost({ host, runnerObservation: "unknown", childObservation: "dead", launchLeaseActive: false }), false);
54
- assert.equal(canReplaceHost({ host, runnerObservation: "dead", childObservation: "not_started", launchLeaseActive: false }), true);
55
- assert.equal(canReplaceHost({ host, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: true }), false);
56
- });
57
- ```
58
-
59
- Then ADD these four tests right after it:
60
-
61
- ```js
62
- test("canReplaceHost allows replacing a terminal host whose claimPid is still alive (issue #99)", () => {
63
- // The bug: an exited/failed host keeps its claimPid (the dashboard process
64
- // that wrote the claim), and a live pid observed as "unknown" used to block
65
- // replacement forever — attach pended to "host start timed out". Claim
66
- // protection only matters while a claim is mid-transaction (state
67
- // "starting"); a terminal host cannot still be being launched.
68
- assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: false }), true);
69
- assert.equal(canReplaceHost({ host: { state: "failed" }, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: false }), true);
70
- });
71
-
72
- test("canReplaceHost still refuses unknown runner/child observations (issue #99 conservatism)", () => {
73
- assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "unknown", childObservation: "dead", launchLeaseActive: false }), false);
74
- assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "dead", childObservation: "unknown", launchLeaseActive: false }), false);
75
- assert.equal(canReplaceHost({ host: { state: "failed" }, runnerObservation: "unknown", childObservation: "unknown", launchLeaseActive: false }), false);
76
- });
77
-
78
- test("canReplaceHost refuses non-terminal hosts and null host (issue #99)", () => {
79
- for (const state of ["starting", "alive", "stopping"]) {
80
- assert.equal(canReplaceHost({ host: { state }, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: false }), false, `state ${state} must refuse`);
81
- }
82
- assert.equal(canReplaceHost({ host: null, runnerObservation: "dead", childObservation: "dead", launchLeaseActive: false }), false);
83
- });
84
-
85
- test("canReplaceHost SAFE_TO_RELEASE boundary regression (issue #99)", () => {
86
- assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "foreign", childObservation: "dead", launchLeaseActive: false }), true, "foreign runner (pid reuse) is releasable");
87
- assert.equal(canReplaceHost({ host: { state: "exited" }, runnerObservation: "not_started", childObservation: "not_started", launchLeaseActive: false }), true);
88
- });
89
- ```
90
-
91
- - [ ] **Step 2: Run tests to verify they fail**
92
-
93
- Run: `node --test test/host-coordination.test.mjs 2>&1 | tail -30`
94
- Expected: FAIL — the first new test fails (`exited` + dead runner/child returns `false` under the old three-role gate). The edited legacy test fails too: the old implementation ignores the removed `claimObservation` key but still requires `claimObservation` in SAFE_TO_RELEASE via `undefined → not in set → false`… actually with `claimObservation` absent, `SAFE_TO_RELEASE.has(undefined)` is `false`, so case 2 of the legacy test (`"dead"` args → expected `true`) FAILS under the old code. Both failures prove the tests exercise the gate.
95
-
96
- - [ ] **Step 3: Implement the relaxation**
97
-
98
- In `src/core/host-coordination.mjs`, replace the `canReplaceHost` function (lines ~63-80) with:
99
-
100
- ```js
101
- /**
102
- * Whether an exited/failed host can be replaced by a new claim. The runner and
103
- * child roles must be provably gone; any `unknown` observation or an active
104
- * launch lease blocks replacement. The claim role does NOT participate: claim
105
- * protection (a launcher mid-transaction between claim and spawn) only matters
106
- * while the host is `starting`, and this gate only ever sees terminal hosts —
107
- * a terminal host cannot still be being launched (issue #99: a live claimPid —
108
- * the dashboard process that wrote the claim — must not block re-attach).
109
- * @param {{
110
- * host: HostStatus|null|undefined,
111
- * runnerObservation: string,
112
- * childObservation: string,
113
- * launchLeaseActive: boolean,
114
- * }} input
115
- * @returns {boolean}
116
- */
117
- export function canReplaceHost({ host, runnerObservation, childObservation, launchLeaseActive }) {
118
- if (!host || (host.state !== "exited" && host.state !== "failed")) return false;
119
- if (launchLeaseActive) return false;
120
- return (
121
- SAFE_TO_RELEASE.has(runnerObservation) &&
122
- SAFE_TO_RELEASE.has(childObservation)
123
- );
124
- }
125
- ```
126
-
127
- In `src/runtime/service.mjs`, edit `observeHostForReplace` (line ~2006) to drop the `claimObservation` line:
128
-
129
- ```js
130
- /** @param {import("../core/types.mjs").HostStatus|null} host */
131
- function observeHostForReplace(host) {
132
- return {
133
- host,
134
- runnerObservation: conservativeObservation(host?.runnerPid ?? null),
135
- childObservation: conservativeObservation(host?.childPid ?? null),
136
- launchLeaseActive: false,
137
- };
138
- }
139
- ```
140
-
141
- - [ ] **Step 4: Run tests to verify they pass**
142
-
143
- Run: `node --test test/host-coordination.test.mjs 2>&1 | tail -10`
144
- Expected: PASS — all tests in the file pass (4 new + edited legacy + all untouched).
145
-
146
- Run: `node --test test/host-resolver.test.mjs test/host-recovery.test.mjs test/host-crash.test.mjs 2>&1 | tail -10`
147
- Expected: PASS — no regressions in adjacent host suites.
148
-
149
- Run: `npm run typecheck`
150
- Expected: exit 0.
151
-
152
- - [ ] **Step 5: Commit**
153
-
154
- ```bash
155
- git add src/core/host-coordination.mjs src/runtime/service.mjs test/host-coordination.test.mjs
156
- git commit -m "fix(host): claim role no longer blocks terminal host replacement (#99)"
157
- ```
158
-
159
- ---
160
-
161
- ### Task 2: Resolver integration test — exited host + live claimPid attaches via fresh spawn (A6)
162
-
163
- **Files:**
164
- - Test: `test/host-resolver.test.mjs` (add one test after the `resolver finalizes a provably-dead legacy alive host` test, ~line 163)
165
-
166
- **Interfaces:**
167
- - Consumes: existing helpers `freshRoot`, `resolverService`, `healServiceOverrides(probe, spawns)`, `scriptProbe(seq)`, `hostRecord(root, viewId, over)` (defaults `claimPid: process.pid` — exactly the live-claimer shape), `createView` (returns `{ sessionFile, ... }`), and `writeFileSync` (already imported). Task 1's relaxed `canReplaceHost` must be in place — this test verifies the full attach chain (resolver → ensureHost → startHostUnderLease → canReplaceHost → spawn → probe ready).
168
- - Produces: nothing downstream (terminal verification task).
169
-
170
- - [ ] **Step 1: Write the integration test**
171
-
172
- Add to `test/host-resolver.test.mjs` after the issue #87 legacy-alive test (~line 163):
173
-
174
- ```js
175
- test("resolver replaces an exited host whose claimPid is still alive (issue #99)", async () => {
176
- const root = freshRoot();
177
- try {
178
- const meta = createView(root, { id: "v1", name: "a", cwd: "/r" });
179
- writeFileSync(meta.sessionFile, "");
180
- // The bug's exact shape: the host ran to completion (exited, exitCode 0,
181
- // stopReason child_exit) but its claimPid — the dashboard process that
182
- // wrote the claim — is STILL ALIVE (hostRecord defaults claimPid to
183
- // process.pid). Before the fix, canReplaceHost saw the live claim as
184
- // "unknown" and the resolver pended to "host start timed out".
185
- hostRecord(root, "v1", {
186
- instanceId: "i1",
187
- state: "exited",
188
- runnerPid: 999999,
189
- childPid: null,
190
- endedAt: Date.now(),
191
- exitCode: 0,
192
- stopReason: "child_exit",
193
- });
194
- const probe = scriptProbe(["ready"]);
195
- const spawns = [];
196
- const svc = resolverService(root, healServiceOverrides(probe, spawns));
197
- const result = await svc.resolveAttachTarget("v1", { timeoutMs: 2_000 });
198
- assert.equal(result.kind, "pty", `must replace the exited host despite the live claimPid: ${JSON.stringify(result)}`);
199
- assert.equal(spawns.length, 1, "exactly one fresh claim spawn");
200
- assert.notEqual(result.instanceId, "i1", "attaches to the replacement instance");
201
- } finally {
202
- rmSync(root, { recursive: true, force: true });
203
- }
204
- });
205
- ```
206
-
207
- - [ ] **Step 2: Sanity-verify the test fails against the pre-fix gate (optional but recommended)**
208
-
209
- Temporarily `git stash` the Task 1 commit (`git stash` won't work across commits — instead: `git checkout HEAD~1 -- src/core/host-coordination.mjs src/runtime/service.mjs`), then run:
210
-
211
- Run: `node --test --test-name-pattern "issue #99" test/host-resolver.test.mjs 2>&1 | tail -15`
212
- Expected: the new test FAILS or times out (resolver pends — the pre-fix behavior). Then restore: `git checkout HEAD -- src/core/host-coordination.mjs src/runtime/service.mjs`.
213
-
214
- If the timeout makes the run slow, the 2_000 ms timeoutMs bounds it.
215
-
216
- - [ ] **Step 3: Run the test against the fix**
217
-
218
- Run: `node --test --test-name-pattern "issue #99" test/host-resolver.test.mjs 2>&1 | tail -10`
219
- Expected: PASS — `kind: "pty"`, exactly one spawn, replacement instanceId.
220
-
221
- - [ ] **Step 4: Run the full suite**
222
-
223
- Run: `npm test 2>&1 | tail -15`
224
- Expected: PASS — all suites green, no regressions.
225
-
226
- Run: `npm run typecheck`
227
- Expected: exit 0.
228
-
229
- - [ ] **Step 5: Commit**
230
-
231
- ```bash
232
- git add test/host-resolver.test.mjs
233
- git commit -m "test(resolver): exited host with live claimPid attaches via fresh spawn (#99)"
234
- ```
235
-
236
- ---
237
-
238
- ### Task 3: U1 manual verification (post-implementation, user-executed)
239
-
240
- **Files:** none (manual).
241
-
242
- **Interfaces:** none.
243
-
244
- - [ ] **Step 1: Restart dashboard process** (loads new code — the "immediately effective on existing bad records" property requires restart).
245
-
246
- - [ ] **Step 2: Attach view_2472d82627 from the dashboard** — observe: attach enters the session, history renders, no `host start timed out`.
247
-
248
- - [ ] **Step 3: Exit the session, attach again** — confirm repeatability.
249
-
250
- - [ ] **Step 4: Same check on view_4b667ad75d / view_c038badb30** (the other two exited + live-claimPid views).
251
-
252
- - [ ] **Step 5: Record results in the issue** (comment each view's outcome; mark U1 pass/pending in the final report).
253
-
254
- ---
255
-
256
- ## Self-Review
257
-
258
- **1. Spec coverage:**
259
- - A1-A5 → Task 1 Step 1 (three new tests + edited legacy test covers A2/A3 cases) ✓
260
- - A6 → Task 2 ✓
261
- - U1 → Task 3 ✓
262
- - 改动文件清单 (spec) → Task 1 + Task 2 files match exactly (host-coordination.mjs, service.mjs, host-coordination.test.mjs, host-resolver.test.mjs) ✓
263
- - 非目标: no pty-runner/store/host.json changes in any task ✓
264
-
265
- **2. Placeholder scan:** no TBD/TODO; every code step has full code; verification commands concrete. ✓
266
-
267
- **3. Type consistency:** `canReplaceHost` new signature `{host, runnerObservation, childObservation, launchLeaseActive}` used consistently in Task 1 tests, Task 1 implementation, and matches Task 2's indirect usage (no direct call). `healServiceOverrides(probe, spawns)` helper name matches file. ✓