@huseynsnmz/pi-board 0.15.3 → 0.16.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 (211) hide show
  1. package/CHANGELOG.md +13 -4
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +53 -419
  4. package/SECURITY.md +19 -0
  5. package/docs/how-it-works.md +31 -0
  6. package/docs/reference.md +276 -0
  7. package/package.json +5 -11
  8. package/src/index.ts +1 -1
  9. package/src/runtime/service.mjs +12 -0
  10. package/src/ui/dashboard.ts +14 -8
  11. package/IMPLEMENTATION_PLAN.md +0 -920
  12. package/PRD.md +0 -484
  13. package/PROGRESS.md +0 -142
  14. package/VERIFY.md +0 -115
  15. package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
  16. package/docs/EXPLORATION.md +0 -187
  17. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
  18. package/docs/RELEASE.md +0 -148
  19. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
  20. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
  21. package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
  22. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
  23. package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
  24. package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
  25. package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
  26. package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
  27. package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
  28. package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
  29. package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
  30. package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
  31. package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
  32. package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
  33. package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
  34. package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
  35. package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
  36. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
  37. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
  38. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
  39. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
  40. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
  41. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
  42. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
  43. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
  44. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
  45. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
  46. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
  47. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
  48. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
  49. package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
  50. package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
  51. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
  52. package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
  53. package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
  54. package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
  55. package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
  56. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
  57. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
  58. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
  59. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
  60. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
  61. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
  62. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
  63. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
  64. package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
  65. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
  66. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
  67. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
  68. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
  69. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
  70. package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
  71. package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
  72. package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
  73. package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
  74. package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
  75. package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
  76. package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
  77. package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
  78. package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
  79. package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
  80. package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
  81. package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
  82. package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
  83. package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
  84. package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
  85. package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
  86. package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
  87. package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
  88. package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
  89. package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
  90. package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
  91. package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
  92. package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
  93. package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
  94. package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
  95. package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
  96. package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
  97. package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
  98. package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
  99. package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
  100. package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
  101. package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
  102. package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
  103. package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
  104. package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
  105. package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
  106. package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
  107. package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
  108. package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
  109. package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
  110. package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
  111. package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
  112. package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
  113. package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
  114. package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
  115. package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
  116. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
  117. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
  118. package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
  119. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
  120. package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
  121. package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
  122. package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
  123. package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
  124. package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
  125. package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
  126. package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
  127. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
  128. package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
  129. package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
  130. package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
  131. package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
  132. package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
  133. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
  134. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
  135. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
  136. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
  137. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
  138. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
  139. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
  140. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
  141. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
  142. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
  143. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
  144. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
  145. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
  146. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
  147. package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
  148. package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
  149. package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
  150. package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
  151. package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
  152. package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
  153. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
  154. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
  155. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
  156. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
  157. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
  158. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
  159. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
  160. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
  161. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
  162. package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
  163. package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
  164. package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
  165. package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
  166. package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
  167. package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
  168. package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
  169. package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
  170. package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
  171. package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
  172. package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
  173. package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
  174. package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
  175. package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
  176. package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
  177. package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
  178. package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
  179. package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
  180. package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
  181. package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
  182. package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
  183. package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
  184. package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
  185. package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
  186. package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
  187. package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
  188. package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
  189. package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
  190. package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
  191. package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
  192. package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
  193. package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
  194. package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
  195. package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
  196. package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
  197. package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
  198. package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
  199. package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
  200. package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
  201. package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
  202. package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
  203. package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
  204. package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
  205. package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
  206. package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
  207. package/scripts/budget-audit.mjs +0 -135
  208. package/scripts/release.mjs +0 -446
  209. package/scripts/release_helper.mjs +0 -309
  210. package/scripts/run-perf-gate.mjs +0 -40
  211. 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. ✓