@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.
- package/CHANGELOG.md +13 -4
- package/CONTRIBUTING.md +7 -0
- package/README.md +53 -419
- package/SECURITY.md +19 -0
- package/docs/how-it-works.md +31 -0
- package/docs/reference.md +276 -0
- package/package.json +5 -11
- package/src/index.ts +1 -1
- package/src/runtime/service.mjs +12 -0
- package/src/ui/dashboard.ts +14 -8
- package/IMPLEMENTATION_PLAN.md +0 -920
- package/PRD.md +0 -484
- package/PROGRESS.md +0 -142
- package/VERIFY.md +0 -115
- package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
- package/docs/EXPLORATION.md +0 -187
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
- package/docs/RELEASE.md +0 -148
- package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
- package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
- package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
- package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
- package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
- package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
- package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
- package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
- package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
- package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
- package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
- package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
- package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
- package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
- package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
- package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
- package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
- package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
- package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
- package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
- package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
- package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
- package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
- package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
- package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
- package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
- package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
- package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
- package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
- package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
- package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
- package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
- package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
- package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
- package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
- package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
- package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
- package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
- package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
- package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
- package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
- package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
- package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
- package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
- package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
- package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
- package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
- package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
- package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
- package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
- package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
- package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
- package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
- package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
- package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
- package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
- package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
- package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
- package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
- package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
- package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
- package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
- package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
- package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
- package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
- package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
- package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
- package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
- package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
- package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
- package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
- package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
- package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
- package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
- package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
- package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
- package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
- package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
- package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
- package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
- package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
- package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
- package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
- package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
- package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
- package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
- package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
- package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
- package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
- package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
- package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
- package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
- package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
- package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
- package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
- package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
- package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
- package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
- package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
- package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
- package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
- package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
- package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
- package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
- package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
- package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
- package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
- package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
- package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
- package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
- package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
- package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
- package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
- package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
- package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
- package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
- package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
- package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
- package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
- package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
- package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
- package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
- package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
- package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
- package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
- package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
- package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
- package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
- package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
- package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
- package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
- package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
- package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
- package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
- package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
- package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
- package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
- package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
- package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
- package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
- package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
- package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
- package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
- package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
- package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
- package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
- package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
- package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
- package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
- package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
- package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
- package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
- package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
- package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
- package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
- package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
- package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
- package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
- package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
- package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
- package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
- package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
- package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
- package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
- package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
- package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
- package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
- package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
- package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
- package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
- package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
- package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
- package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
- package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
- package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
- package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
- package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
- package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
- package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
- package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
- package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
- package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
- package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
- package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
- package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
- package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
- package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
- package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
- package/scripts/budget-audit.mjs +0 -135
- package/scripts/release.mjs +0 -446
- package/scripts/release_helper.mjs +0 -309
- package/scripts/run-perf-gate.mjs +0 -40
- package/scripts/run-tests.mjs +0 -375
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
# issue #224 — 设计:用租约心跳超龄回收替代「pid 死活」判定
|
|
2
|
-
|
|
3
|
-
Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T21:35:00+08:00
|
|
4
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T21:58:00+08:00 · 依据 R1 复核(G1 + D1–D5)
|
|
5
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T22:12:00+08:00 · 依据 R2 复核(G1 + G2 + D1–D5)
|
|
6
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T22:31:00+08:00 · 依据 R3 复核(0 真缺口 + D1–D6,收敛轮)
|
|
7
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T23:20:00+08:00 · 规划期回改(监督定时器起点移到 listen 之后、A9b、`reason` 语义)
|
|
8
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-24T00:05:00+08:00 · 实现期回改(D1 伪代码的无 token 语义与既有 A4 契约对齐)
|
|
9
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-24T01:00:00+08:00 · 整支复核后文档精确性修正(A6 表述、D4 常量关系改为由 A9b 单测钉住)
|
|
10
|
-
|
|
11
|
-
仓库:zhuxixi/pi-agent-board · issue:#224 · 起草轮次:R0(待交叉复核)
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 1. 问题与根因(已复现)
|
|
16
|
-
|
|
17
|
-
**现象**:租约 owner 的 pid 被系统复用后,`classifyLeaseOwner` 认为「持有者还活着」,导致
|
|
18
|
-
`state-coordinator.lock` 永久不可回收;新 coordinator 每次抢锁失败即 `exit(0)`(stderr 被丢弃),
|
|
19
|
-
`ensureCoordinator` 永远返回 `coordinator_unavailable`,全部 `state_command` 被拒,面板所有会话停在 `QUEUED`。
|
|
20
|
-
|
|
21
|
-
**根因链**(行号已核实):
|
|
22
|
-
|
|
23
|
-
| 环节 | 位置 | 事实 |
|
|
24
|
-
|---|---|---|
|
|
25
|
-
| 身份捕获 | `src/core/pid.mjs:53` | `captureStartToken()` 仅 Linux 读 `/proc`,Windows 恒 `null` |
|
|
26
|
-
| 判定 | `src/core/locks.mjs:270-286` | identity 分支只问 `isProcessDead(pid)`;identity-less 分支同样只问死活 |
|
|
27
|
-
| 探针 | `locks.mjs:226` | 默认 `(pid) => !isAlive(pid)`,无法表达「pid 活着但不是持有者」 |
|
|
28
|
-
| 心跳 | `locks.mjs:342` + `state-coordinator.mjs:119` | 每 1000ms 写 `heartbeat.<token>`,**`locks.mjs` 从不读取** |
|
|
29
|
-
| 静默 | `state-coordinator.mjs:116` | 抢锁失败只写 stderr 后 `exit(0)` |
|
|
30
|
-
|
|
31
|
-
**复现证据**(Windows 本机、真实模块;脚本在留档目录
|
|
32
|
-
`~/.github-issue-driven/zhuxixi/pi-agent-board/issue-224/research/repro-224.mjs`,**不入库**。
|
|
33
|
-
该脚本的四个用例在 PR 中以单测形态复刻为 A1/A2/A4,因此修复前基线在仓内可回归):
|
|
34
|
-
|
|
35
|
-
| 用例 | 期望 | 实测(修复前) |
|
|
36
|
-
|---|---|---|
|
|
37
|
-
| A. pid 活着(复用)+ 心跳 24h 未更新 | 回收 | **`busy`(bug)** |
|
|
38
|
-
| B. pid 活着 + 心跳新鲜 | busy | `busy` ✓ |
|
|
39
|
-
| C. pid 死 + 锁龄 48h | 回收 | acquired ✓ |
|
|
40
|
-
| D. 无心跳文件 + pid 活着(host-meta 形态) | busy | `busy` ✓ |
|
|
41
|
-
|
|
42
|
-
**关键结论**:记录 Windows 的进程创建时间(issue 建议 2)**修不好本 bug**——判定函数从不比对
|
|
43
|
-
token 值;且该改动会让记录落入 identity 分支,跳过 5 分钟宽限,反而更糟。心跳是唯一平台无关、
|
|
44
|
-
且能区分「持有者活着」与「pid 被复用」的信号。
|
|
45
|
-
|
|
46
|
-
## 2. 范围
|
|
47
|
-
|
|
48
|
-
**做**:心跳超龄回收(租约层)+ 心跳承诺的自描述/显式契约 + coordinator 级可观测性 + 心跳失效时的自终止不变量。
|
|
49
|
-
|
|
50
|
-
**不做**(明确非目标,避免范围蔓延):
|
|
51
|
-
|
|
52
|
-
1. Windows `captureStartToken` 实现与三份拷贝收敛、token 值比对 —— 属「进程身份可验证」共享底座,另行处理。
|
|
53
|
-
2. `replaceStaleCoordinator`(`coordinator-client.mjs:154`)对复用 pid 直接 `SIGTERM` 的隐患 —— 同源但语义独立,另开 issue。
|
|
54
|
-
3. #217 的 `stopping` 顺序缺陷与释放路径 —— 另一层,独立修。
|
|
55
|
-
4. 心跳回收**不**扩展到 host-meta 毫秒级租约(见 D2 否决理由)。
|
|
56
|
-
|
|
57
|
-
## 3. 设计决策
|
|
58
|
-
|
|
59
|
-
### D1 判定输入:把「心跳年龄」作为注入的纯函数参数
|
|
60
|
-
|
|
61
|
-
`classifyLeaseOwner` 保持纯函数(现有单测风格),新增 `opts`:
|
|
62
|
-
|
|
63
|
-
```js
|
|
64
|
-
classifyLeaseOwner(owner, now, isProcessDead, {
|
|
65
|
-
orphanAgeMs, // 既有(默认 5min)
|
|
66
|
-
heartbeatAgeMs, // 新增:number(陈旧)| null(无心跳证据)| undefined(同 null)
|
|
67
|
-
heartbeatMs, // 新增:owner 声明的周期(number>0)| undefined 未声明
|
|
68
|
-
heartbeatStaleMs, // 新增:门限(默认 LEASE_HEARTBEAT_STALE_MS = 60_000)
|
|
69
|
-
}) -> "reclaim" | "busy" | "blocked"
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
判定顺序(伪代码,前置优先级与失败方向都在此固定;实现必须逐条对应):
|
|
73
|
-
|
|
74
|
-
```js
|
|
75
|
-
const hasIdentity = identity.pid > 0 && typeof identity.startToken === "string";
|
|
76
|
-
const pid = Number(owner?.identity?.pid ?? owner?.pid ?? 0);
|
|
77
|
-
const declared = Number(heartbeatMs) > 0; // D2 显式声明
|
|
78
|
-
// 无 token 只剥夺「可回收性」,不影响 busy 判定(既有契约,已被单测钉住)
|
|
79
|
-
const reclaimable = typeof owner?.token === "string";
|
|
80
|
-
if (!owner || typeof owner !== "object") return "blocked";
|
|
81
|
-
if (pid <= 0) return "blocked"; // 没有 pid 无法判定
|
|
82
|
-
|
|
83
|
-
if (declared) {
|
|
84
|
-
// 显式声明:对 identity 与 identity-less 两个分支都生效,且**绕过** age 早退
|
|
85
|
-
const hbAge = heartbeatAgeMs ?? (now - owner.startedAt); // 无文件/破损 → startedAt 回退(D3)
|
|
86
|
-
if (hbAge >= Math.max(heartbeatStaleMs, 3 * heartbeatMs)) return reclaimable ? "reclaim" : "blocked";
|
|
87
|
-
} else if (heartbeatAgeMs != null && (now - owner.startedAt) >= orphanAgeMs) {
|
|
88
|
-
// 存量自愈:只对 identity-less 生效,且要求 age >= orphanAgeMs(D2)
|
|
89
|
-
if (heartbeatAgeMs >= heartbeatStaleMs) return reclaimable ? "reclaim" : "blocked";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (hasIdentity) {
|
|
93
|
-
if (!isProcessDead(pid)) return "busy"; // 既有:identity 分支不跳 age
|
|
94
|
-
return reclaimable ? "reclaim" : "blocked";
|
|
95
|
-
}
|
|
96
|
-
// identity-less
|
|
97
|
-
if (!declared && (now - owner.startedAt) < orphanAgeMs) return "blocked"; // 既有;仅非声明租约受 age 约束
|
|
98
|
-
if (!isProcessDead(pid)) return "busy";
|
|
99
|
-
return reclaimable ? "reclaim" : "blocked";
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
三点必须钉住(否则前向契约形同虚设):
|
|
103
|
-
1. 显式声明分支**先于** `hasIdentity` 的 `busy` 早退 —— 否则 Linux(有 startToken)上永远不进心跳判定。
|
|
104
|
-
2. 显式声明**绕过** identity-less 的 `age < orphanAgeMs -> blocked` —— 否则崩溃后 60s 自愈退化为 5min+。
|
|
105
|
-
3. 负值 `heartbeatAgeMs` 在纯函数与 I/O 壳两层都按 0(新鲜)处理(防御性双保险;壳层见 D3)。
|
|
106
|
-
4. **无 token 不等于 blocked**:它只剥夺「可回收性」(quarantine 需 token 校验),仍可返回 `busy`。
|
|
107
|
-
既有单测 `live-holder path keeps busy even without a token` 钉住了这一点。
|
|
108
|
-
|
|
109
|
-
### D2 心跳承诺如何成立(eligibility)——两条互斥路径
|
|
110
|
-
|
|
111
|
-
| 路径 | 条件 | 覆盖对象 | 理由 |
|
|
112
|
-
|---|---|---|---|
|
|
113
|
-
| 显式声明 | `owner.heartbeatMs` 为正数(新 coordinator 写 1000) | 前向契约 | 声明即承诺;门限 = `max(heartbeatStaleMs, 3 × heartbeatMs)` |
|
|
114
|
-
| 存量自愈 | `heartbeat.<owner.token>` 可解析 **且** 锁龄 ≥ `orphanAgeMs`(5min) | 升级前遗留记录(含 #224 现场) | 心跳文件证明该租约确实承诺心跳;5min 锁龄保证与毫秒级短持有租约完全区隔 |
|
|
115
|
-
|
|
116
|
-
其余一律不eligible,沿用 #112 契约。**否决**「心跳文件存在即可回收」:`pty-runner` 的
|
|
117
|
-
`host-start` 租约也写心跳,但它在 boot 期间持有(`pty-runner.mjs:769-1058`),若仅凭文件存在判定,
|
|
118
|
-
一次 30s 以上的慢启动就会被第二个 runner 抢占 → host.json 双写。以「锁龄 ≥5min」或「显式声明」作门槛
|
|
119
|
-
把该窗口排除;`host-meta`(`store.mjs:191`,从不 `touch()`)两种门槛都不满足,行为完全不变(用例 D)。
|
|
120
|
-
|
|
121
|
-
### D3 心跳年龄的计算与失败方向
|
|
122
|
-
|
|
123
|
-
`reclaimOrBlock`(`locks.mjs:299`,唯一 I/O 壳)在读取 owner.json 后:
|
|
124
|
-
|
|
125
|
-
| 情形 | `heartbeatAgeMs` |
|
|
126
|
-
|---|---|
|
|
127
|
-
| `heartbeat.<token>` 可解析且 `at` 为有限数 | `max(0, now - at)`(未来时间戳按新鲜处理) |
|
|
128
|
-
| 文件不存在 | `null`(未声明租约则永不 eligible;已声明则**回退**为 `now - startedAt`) |
|
|
129
|
-
| 文件不可读 / JSON 破损 / `at` 非数 | 未声明 → `null`(fail-closed);已声明 → `now - startedAt` |
|
|
130
|
-
|
|
131
|
-
「不可读即新鲜」不可取(会让真实孤锁永久卡死);「不可读即陈旧」也不可取(会让健康持有者被抢占)。
|
|
132
|
-
上表是二者之间的最小让步:**只有主动声明了心跳契约的持有者才吃 startedAt 回退**。
|
|
133
|
-
|
|
134
|
-
### D4 心跳写失败必须自终止(防双写不变量)
|
|
135
|
-
|
|
136
|
-
`lease.touch()` 失败时静默返回 false → 一个**活着但写不了心跳**的 coordinator 会在 60s 后被判陈旧并被抢占,
|
|
137
|
-
产生双写。新增不变量:
|
|
138
|
-
|
|
139
|
-
> 持有者连续 3 次**由 interval 监督的** `touch()` 失败(即 3s,远小于 60s 门限)→ **best-effort** 写诊断
|
|
140
|
-
> `coordinator_heartbeat_lost`(包在 try/catch 里,写失败不阻断)→ 走**既有** `shutdown(0)`
|
|
141
|
-
> (`state-coordinator.mjs:199-215`:`clearInterval(startTouchTimer)` + `server.close()` +
|
|
142
|
-
> POSIX socket unlink + `lease.release()` + exit)。
|
|
143
|
-
|
|
144
|
-
顺序固定为 **计数 → 诊断 → shutdown**。`shutdown(0)` 直接 exit,所以诊断必须先落盘;反之
|
|
145
|
-
`appendDiagnostic` 内部的 `appendFileSync` 会抛(仓库 host-meta spec 已记录此风险),而 D4 的目标场景
|
|
146
|
-
恰恰是磁盘故障,因此诊断必须 try/catch 包住,**写失败不得阻断 shutdown**。
|
|
147
|
-
|
|
148
|
-
即:**不能在门限耗尽前证明自己活着,就主动让位**,而不是被别人抢占。门限与失败上限的关系由 **A9b 单测**钉住
|
|
149
|
-
(`MAX_CONSECUTIVE_TOUCH_FAILURES × heartbeatMs < LEASE_HEARTBEAT_STALE_MS`,比注释更难被绕过):
|
|
150
|
-
`LEASE_HEARTBEAT_STALE_MS (60s) > MAX_CONSECUTIVE_TOUCH_FAILURES (3) × heartbeatMs (1s)`。
|
|
151
|
-
|
|
152
|
-
**计数范围(与 D5 协调)**:监督心跳定时器在 **`server.listen` 成功后**启动,不在取锁后启动。理由:若在 boot 期间就监督,`3 × 50ms` 的强制失败测试会在 `server.listen` 之前触发让位,socket 从未绑定,A14 的端点清理断言退化为空断言;语义上也应「服务期间才需要心跳」。boot 期间由 D5 的边界 touch 保活。
|
|
153
|
-
只有监督定时器的 touch 计入连续失败;boot 阶段边界 touch(取锁后、回放后、listen 前)直接调 `lease.touch()`
|
|
154
|
-
并忽略返回值,**不计入**。boot 阶段的磁盘故障会由启动后的第一个 tick 在一个周期内捕获。
|
|
155
|
-
|
|
156
|
-
**适用范围(明确边界)**:D4 只覆盖 coordinator 租约。`pty-runner` 的 `host-start` 租约虽然也写心跳,
|
|
157
|
-
但其磁盘故障兜底是 instance 校验(`shouldYieldRunner` / `owner_lost`,见 R2),本 issue **不**给
|
|
158
|
-
host-start 加自终止(非目标,避免范围扩张)。
|
|
159
|
-
|
|
160
|
-
**可注入 seam**(供 A14 自动验证,沿用 `src/core/test-knobs.mjs` 与 `AGENT_BOARD_TEST_*` 既有模式,
|
|
161
|
-
未设置时生产行为字节一致):`AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS`(压缩心跳周期)与
|
|
162
|
-
`AGENT_BOARD_TEST_FORCE_TOUCH_FAILURE=1`(强制**被监督的** interval touch 返回 false)。
|
|
163
|
-
唯一失败注入点就在 interval 回调里;boot 边界 touch 不经过它。
|
|
164
|
-
|
|
165
|
-
### D5 门限取 60s(而非 30s)
|
|
166
|
-
|
|
167
|
-
租约持有者在拿到锁到绑定端点之间有一段同步 boot(`repairJournalTail` + `readJournal` 回放,
|
|
168
|
-
`state-coordinator.mjs:127-189`);回放被 `CHECKPOINT_THRESHOLD_BYTES`(256KB)约束,正常情况下 << 1s,
|
|
169
|
-
但慢盘/杀软扫描不可假设。60s 给出 60× 余量,代价是最坏 60s 才自愈(远优于「永不」)。
|
|
170
|
-
另外在 boot 阶段边界各补一次同步 `touch()`(取锁后、回放后、listen 前),使心跳反映真实进度;
|
|
171
|
-
**监督定时器在 `server.listen` 成功后才启动**(见 D4),因此 boot 的心跳间隙 = boot 耗时,由 60s 门限覆盖
|
|
172
|
-
(回放受 `CHECKPOINT_THRESHOLD_BYTES` 约束,正常 << 1s)。
|
|
173
|
-
|
|
174
|
-
### D6 可观测性:coordinator 级诊断 + 回收留痕
|
|
175
|
-
|
|
176
|
-
- 新文件 `views/_coordinator/diagnostics.jsonl`(`appendDiagnostic(root, "_coordinator", …)`;`viewDir` 只是
|
|
177
|
-
路径拼接,无 viewId 校验;`pruneScreenLogs` 对无 `meta.json` 的外来目录只统计不删除,已核实)。
|
|
178
|
-
- 事件:`lease_reclaimed`(info,source `lock`,details: ownerPid/heartbeatAgeMs/lockAgeMs/eligibility)、
|
|
179
|
-
`coordinator_lease_unavailable`(error,source `coordinator`,details **= `describeLeaseConflict` 的六个键,无增删**)、
|
|
180
|
-
`coordinator_heartbeat_lost`(error,source `coordinator`,details: `{ consecutiveFailures }`)。
|
|
181
|
-
- 回收留痕不引入依赖倒置:`attemptAcquireLease` 在回收成功时把 `reclaimed` 元数据挂在返回的 Lease 上
|
|
182
|
-
(`lease.reclaimed: {ownerPid, heartbeatAgeMs, lockAgeMs, eligibility} | null`),由调用方决定是否落盘。
|
|
183
|
-
- 抢锁失败细节由新导出 `describeLeaseConflict(root, viewId, name, opts)` 提供(读 owner.json + 心跳),
|
|
184
|
-
返回**固定六字段**:`{ reason, ownerPid, hasIdentity, lockAgeMs, heartbeatAgeMs, heartbeatMs }`(读不到锁时返回 `null`)。
|
|
185
|
-
`reason` 的语义固定为**调用时刻 `classifyLeaseOwner` 的原始判定**:`"reclaim" | "busy" | "blocked"`(诊断路径上实际只会看到后两者)。
|
|
186
|
-
该六字段**即** `coordinator_lease_unavailable` 事件 `details` 的全部键(`reason` 取代上文的 `code`),
|
|
187
|
-
A8 断言与实现字段**一一对应**,不得增删。
|
|
188
|
-
|
|
189
|
-
## 4. 契约与边界
|
|
190
|
-
|
|
191
|
-
| 契约 | 现状 | 本改动后 |
|
|
192
|
-
|---|---|---|
|
|
193
|
-
| quarantine 回收前校验 token | 有(`locks.mjs:313-325`) | **不变**,新增路径同样走该校验 |
|
|
194
|
-
| 过期 token 不可删后继锁 | 有(`releaseWithToken`) | **不变** |
|
|
195
|
-
| 全新 identity-less 锁 → blocked | 有(#112) | **不变**(用例 A4) |
|
|
196
|
-
| identity-less + 超龄 + pid 死 → reclaim | 有 | **不变**(用例 A4) |
|
|
197
|
-
| host-meta(无心跳声明) | busy/blocked | **不变**(用例 D/A4) |
|
|
198
|
-
| `pty-runner` host-start(心跳但短持有,锁龄<5min) | busy | **不变**;锁龄≥5min 且心跳陈旧时可回收(性质变化,见风险 R2) |
|
|
199
|
-
| `owner.json` 结构 | `{token,pid,identity,startedAt}` | 追加可选 `heartbeatMs`(仅 coordinator 写);旧记录完全兼容 |
|
|
200
|
-
|
|
201
|
-
## 5. 验收矩阵
|
|
202
|
-
|
|
203
|
-
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
204
|
-
|---|---|---|---|---|
|
|
205
|
-
| A1 | 存量记录(identity.startToken=null、pid 活着、心跳 24h 陈旧、`startedAt` 48h 前即锁龄≥5min、无 `heartbeatMs`)可回收 | 自动化(unit) | `node --test test/locks.test.mjs` 中新增用例:构造 #224 原样 owner.json(含 `startedAt = now - 48h`)+ `heartbeat.<token>`,用真实 fs 调 `tryAcquireOwnedViewLock` | `acquired: true`,锁目录被替换,`lease.reclaimed.heartbeatAgeMs ≥ 24h` |
|
|
206
|
-
| A1b | **D2 否决项的失败分支**:有心跳文件但锁龄 <5min(pid 活着、未声明 `heartbeatMs`)→ 不得回收 | 自动化(unit) | 同上:`startedAt = now - 30s`、`heartbeat.at = now - 29s`、pid 活着 | `acquired: false, reason: "blocked"`(与既有「fresh identity-less lock → blocked」一致:超龄门槛先于 liveness 判定,故不是 busy) |
|
|
207
|
-
| A1c | 存量路径的反向边界:锁龄超龄但心跳**新鲜** → 仍不得回收 | 自动化(unit) | `startedAt = now - 10min`、`heartbeat.at = now`、pid 活着 | `acquired: false, reason: "busy"` |
|
|
208
|
-
| A2 | 显式声明租约:心跳新鲜时仍 busy,且 `heartbeatMs` 真的被写入 `owner.json` | 自动化(unit) | (a) `tryAcquireOwnedViewLock(..., { heartbeatMs: 1000 })` → 读 `owner.json`;(b) 预置声明租约 `startedAt = now-30s` + `heartbeat.at = now` → 再抢 | (a) `owner.heartbeatMs === 1000`;(b) `acquired: false, reason: "busy"` |
|
|
209
|
-
| A3 | 声明 `heartbeatMs` 的租约:陈旧 → 回收;门限 = `max(heartbeatStaleMs, 3×heartbeatMs)` 两侧都能被穷举 | 自动化(unit) | 纯函数直接调用,**同时注入** `heartbeatMs`、`heartbeatStaleMs`、`now`、`heartbeatAgeMs`;分两组:(a) `heartbeatStaleMs > 3×heartbeatMs`(max 取 staleMs)(b) `heartbeatStaleMs < 3×heartbeatMs`(max 取 3×周期)。各在门限 `-1ms` / `+0ms` 处断言 | 两组均:`门限-1ms → busy`、`门限 → reclaim` |
|
|
210
|
-
| A3b | 显式声明**绕过** identity-less 的 age 早退 | 自动化(unit) | 纯函数:`startedAt = now - 30s`(锁龄<5min)+ `heartbeatMs: 1000` + `heartbeatAgeMs = 120_000`(陈旧)| `reclaim`(若实现保留 age 早退则得 `blocked`,测试必挂) |
|
|
211
|
-
| A4 | 无心跳证据的租约行为完全不变 | 自动化(unit) | 三条:全新 identity-less→blocked;超龄+pid 死→reclaim;超龄+pid 活→busy | 与现状一致 |
|
|
212
|
-
| A5 | 负值 `heartbeatAgeMs`(未来时间戳/时钟回拨)按新鲜处理——纯函数与 I/O 壳两层都钉住 | 自动化(unit) | (a) 纯函数:`heartbeatAgeMs = -5000` + 已声明 → `busy`(不得 reclaim);(b) `readLeaseHeartbeat` 注入假 fs 返回 `at = now + 5000` → 返回 `0` | 两层均按新鲜处理,无 reclaim |
|
|
213
|
-
| A6 | 心跳文件破损/不可读的失败方向 | 自动化(unit) | 未声明→无资格(`heartbeatAgeMs=null`):超龄+pid 活→`busy`,锁龄<5min→`blocked`;已声明→按 startedAt 回退→超门限 reclaim | 与 D3 表一致 |
|
|
214
|
-
| A7 | 回收路径仍受 quarantine token 校验保护(不得删除后继的锁) | 自动化(unit) | 沿用既有并发模式:回收过程中插入新 owner,断言恢复而非删除 | 后继锁存活,verdict=busy |
|
|
215
|
-
| A8 | `describeLeaseConflict` 摘要正确 | 自动化(unit) | 临时目录构造 owner+heartbeat,调用该函数 | 恰好返回 `{reason, ownerPid, hasIdentity, lockAgeMs, heartbeatAgeMs, heartbeatMs}` 六字段且值正确;无锁时返回 `null` |
|
|
216
|
-
| A9 | 心跳连续失败 → 让位(纯判定) | 自动化(unit) | 新增 `heartbeatFailureAction(n, max)` 纯函数(导出位置见 §9:`locks.mjs`) | `n<max → "continue"`,`n>=max → "abandon"`,边界包含 |
|
|
217
|
-
| A9b | 让位界限严格早于陈旧门限(常量关系) | 自动化(unit) | 断言 `MAX_CONSECUTIVE_TOUCH_FAILURES × 1000 < LEASE_HEARTBEAT_STALE_MS` | 断言成立(防未来有人把两者调反) |
|
|
218
|
-
| A14 | 让位的**接线**:连续心跳失败 → coordinator 释放租约、清理端点、退出并留诊断 | 自动化(integration) | 在 `test/state-coordinator.integration.test.mjs` 内先写 `process.env.AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS="50"` 与 `AGENT_BOARD_TEST_FORCE_TOUCH_FAILURE="1"`(`startCoordinator` 经 `env:{...process.env}` 继承,**无需改签名**),`t.after` 里 `delete` 清理;等待退出 | 进程在 ≤ 1s 内以 0 退出;`views/_coordinator/state-coordinator.lock/` 已删除(租约释放);`views/_coordinator/diagnostics.jsonl` 有 `coordinator_heartbeat_lost`(details 含 consecutiveFailures);**端点清理**:POSIX 断言 socket 文件已 unlink(`boundSocketIdentity` 已置位,非空断言),win32 断言命名管道不再可连接 |
|
|
219
|
-
| A14b | **诊断写失败的失败分支**:诊断写抛错不得阻断 shutdown | 自动化(integration) | 同 A14,额外加 `AGENT_BOARD_TEST_FAIL_DIAGNOSTIC="1"`(只包住 coordinator 的 diag 包装,使 `appendDiagnostic` 抛) | 进程仍以 0 退出、租约仍释放、端点仍清理;`diagnostics.jsonl` 无该事件(写失败)——断言的是**不阻断**,不是落盘 |
|
|
220
|
-
| A10 | 端到端:陈旧租约存在时新 coordinator 接管并恢复写入 | 自动化(integration) | `test/state-coordinator.integration.test.mjs` 新增用例:预置陈旧锁 + `createView`,起 coordinator,发 `state_command` | 命令 `applied`、`state-journal.jsonl` 增长、`views/_coordinator/diagnostics.jsonl` 含 `lease_reclaimed` |
|
|
221
|
-
| A11 | 健康租约仍独占(第二个实例立即退出),且**抢锁失败会落诊断** | 自动化(integration) | 既有用例「second coordinator instance exits immediately」+ 新增断言 | 保持通过;且 `views/_coordinator/diagnostics.jsonl` 出现 `coordinator_lease_unavailable`,details 键集 = `{reason, ownerPid, hasIdentity, lockAgeMs, heartbeatAgeMs, heartbeatMs}` |
|
|
222
|
-
| A12 | 类型与静态检查 | 自动化(static) | `npm run typecheck` | 无错误 |
|
|
223
|
-
| A13 | 全量回归 + 覆盖率门槛 | 自动化(build) | `npm run verify` | 全绿,coverage 门槛不降 |
|
|
224
|
-
| U1 | Windows 真实环境:复用 pid 的陈旧租约无需人工 `mv` 即自愈 | 用户实测 | **前置**:PR 已合并并发布,本机 `git pull` 到含本次 fix 的版本,重载 Pi(确认 `runner/state-coordinator.mjs` 含 `lease_reclaimed`)且 `jfox`-无关的旧 coordinator 进程已退出。步骤:① 停掉 coordinator;② 把 `views/_coordinator/state-coordinator.lock/owner.json` 改写为 `{"token":"x","pid":<任一存活无关进程pid>,"identity":{"pid":…,"startToken":null},"startedAt":<48h前>}`,并写 `heartbeat.x={"at":<24h前>}`;③ 在面板触发任一状态操作;④ 等 ≤60s 再触发一次 | 面板不再报 `coordinator_unavailable`;`state.json` 的 `materializedRevision` 变为非 null;`views/_coordinator/diagnostics.jsonl` 出现 `lease_reclaimed` |
|
|
225
|
-
| U2 | 心跳失效自终止不误伤正常路径 | 用户实测 | **前置**:同 U1(含 fix 的构建 + Pi 重载)。步骤:正常使用 10 分钟,期间观察 `views/_coordinator/diagnostics.jsonl` 与 coordinator pid | 无 `coordinator_heartbeat_lost`;coordinator pid 不变 |
|
|
226
|
-
|
|
227
|
-
## 6. 可测性拆分设计
|
|
228
|
-
|
|
229
|
-
| 单元 | 形态 | 测试边界 |
|
|
230
|
-
|---|---|---|
|
|
231
|
-
| `classifyLeaseOwner(owner, now, isProcessDead, opts)` | 纯函数,无 I/O | 新增 opts 字段全部以字面量注入;A3/A4/A5 在此层验证 |
|
|
232
|
-
| `readLeaseHeartbeat(lockPath, token, fs)` | 薄 I/O 壳(`fs` 可注入,沿用 `defaultLeaseFs` 模式) | A6 用假 fs 验证三种读失败;真实 fs 仅在 A1 由 `tryAcquireOwnedViewLock` 覆盖 |
|
|
233
|
-
| `reclaimOrBlock` → `attemptAcquireLease` | 内部,经 `tryAcquireOwnedViewLock` 观察 | A1/A2/A7 用真实临时目录(自包含 fixture,不碰宿主仓) |
|
|
234
|
-
| `describeLeaseConflict(root, viewId, name, opts)` | 读文件的摘要函数(`fs`/`clock` 可注入) | A8 |
|
|
235
|
-
| `heartbeatFailureAction(consecutive, max)` | 纯函数(`locks.mjs` 导出,与 `classifyLeaseOwner` 同层) | A9 |
|
|
236
|
-
| coordinator 让位接线(计数 → 诊断(try/catch) → shutdown(0)) | 脚本 shell;seam = 三个 `AGENT_BOARD_TEST_*` 旋钮(经 `process.env` 注入,`startCoordinator` 继承) | A14(成功路径)、A14b(诊断写失败的失败分支) |
|
|
237
|
-
|
|
238
|
-
原则:能满足验收的最低层级优先(`unit` > `integration`);跨进程/跨文件交互(真实的
|
|
239
|
-
"新 coordinator 抢占并恢复写入")必须走 `integration`;Windows 专属语义不进 CI 覆盖范围
|
|
240
|
-
(CI 仅 `ubuntu-latest`),Windows 行为的正确性由「平台无关的纯判定 + 夹具形态」承载,实机由 U1 兜底。
|
|
241
|
-
|
|
242
|
-
## 7. 部署与执行路径真实性
|
|
243
|
-
|
|
244
|
-
- 改动位于 `src/core/locks.mjs`(被 dashboard 与两个 runner 共同引用)与 `runner/state-coordinator.mjs`。
|
|
245
|
-
- 生效路径:扩展更新 → Pi 重载 → 下次 `ensureCoordinator` 拉起的新 coordinator 进程即带新代码。
|
|
246
|
-
**已卡死的实例无需重启面板**:卡死的是锁文件,不是 dashboard;新 spawn 的 coordinator 会自行回收。
|
|
247
|
-
- 存量锁(无 `heartbeatMs`)由 D2 的「存量自愈」路径覆盖,无需人工 `mv`(U1 验证)。
|
|
248
|
-
- 打包面无新增**运行时**产物;`npm run pack:dry` 会多出一个 `docs/superpowers/specs/…-design.md`(`package.json` 的 `files` 含 `docs/`,文档非运行时文件,不算回归)。
|
|
249
|
-
- **不新增生产行为开关**(门限仅经 opts 注入)。新增三个**测试专用** `AGENT_BOARD_TEST_*` 旋钮
|
|
250
|
-
(`AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS`、`AGENT_BOARD_TEST_FORCE_TOUCH_FAILURE`、
|
|
251
|
-
`AGENT_BOARD_TEST_FAIL_DIAGNOSTIC`),沿用 `src/core/test-knobs.mjs` 既有模式;未设置时生产行为字节一致。
|
|
252
|
-
|
|
253
|
-
## 8. 风险
|
|
254
|
-
|
|
255
|
-
| ID | 风险 | 缓解 |
|
|
256
|
-
|---|---|---|
|
|
257
|
-
| R1 | 磁盘故障使健康持有者心跳停滞 → 被抢占 → 双写 | **coordinator**:D4 连续 3 次失败即自终止并让位,门限 60s 远大于 3s;**host-start 租约**:无自终止(非目标),其兜底是 R2 的 instance 校验(`shouldYieldRunner` / `owner_lost`)。两者边界不得混用 |
|
|
258
|
-
| R2 | `host-start` 租约锁龄≥5min 且心跳停滞时被回收(boot 卡死场景) | boot 事务正常为秒级;卡死 5min 时回收优于永久占锁;`host.json` 写入前有 instance 校验(`shouldYieldRunner` / `owner_lost`) |
|
|
259
|
-
| R3 | 并发两个 dashboard 各 spawn 一个 coordinator,在 boot 窗口互相判定陈旧 | D5 门限 60s + boot 阶段边界 touch;且 `ensureCoordinator` 先探端点,端点未绑定时窗口 << 60s;回收仍需通过 quarantine token 校验 |
|
|
260
|
-
| R4 | 时钟回拨导致 `heartbeatAgeMs` 异常大 | 门限判定用 `max(0, now-at)`;回拨幅度远小于 60s 且不影响「pid 确死」路径 |
|
|
261
|
-
| R5 | 诊断目录新增文件影响既有读取 | `_coordinator` 不被 `pruneScreenLogs` 删除(无 meta.json → foreign skip),不被视图扫描当普通 view(`readState` 按 viewId 定点读) |
|
|
262
|
-
|
|
263
|
-
## 9. 变更文件清单(预估)
|
|
264
|
-
|
|
265
|
-
| 文件 | 变更 |
|
|
266
|
-
|---|---|
|
|
267
|
-
| `src/core/locks.mjs` | `LEASE_HEARTBEAT_STALE_MS`、`MAX_CONSECUTIVE_TOUCH_FAILURES`、`readLeaseHeartbeat`、`classifyLeaseOwner` opts 扩展 + **两个分支都先判心跳**(显式声明绕过 age 早退,见 D1 伪代码)、回收判定、`lease.reclaimed`、`describeLeaseConflict`、`heartbeatFailureAction`;**`attemptAcquireLease` 接受 `opts.heartbeatMs` 并写入 `owner.json`**(新 seam,D2 显式声明路径的前提) |
|
|
268
|
-
| `runner/state-coordinator.mjs` | 传 `heartbeatMs: HEARTBEAT_MS`(心跳周期经 `resolveTestMs(env, "AGENT_BOARD_TEST_COORDINATOR_HEARTBEAT_MS", HEARTBEAT_MS)`);boot 边界 touch(不计数);**监督定时器在 `server.listen` 成功后启动**;interval touch 失败 → `heartbeatFailureAction` → **先** try/catch 写诊断 → **后** `shutdown(0)`;`AGENT_BOARD_TEST_FORCE_TOUCH_FAILURE` / `AGENT_BOARD_TEST_FAIL_DIAGNOSTIC` seam;抢锁失败写 `coordinator_lease_unavailable`(details = `describeLeaseConflict` 六字段);成功后 `lease.reclaimed` → `lease_reclaimed` |
|
|
269
|
-
| `test/locks.test.mjs` | A1、A1b、A1c、A2–A9(含 A3/A3b/A5 的两组与两层断言、A9b) |
|
|
270
|
-
| `test/state-coordinator.integration.test.mjs` | A10、A11(含新增诊断断言)、A14、A14b |
|
|
271
|
-
| `docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md` | 本 spec(进 worktree,首个 commit) |
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
# Issue #231 — full-row highlight for the selected board row
|
|
2
|
-
|
|
3
|
-
Drafted: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T19:10:00+08:00
|
|
4
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T19:24:00+08:00 — cross-review round 1 (R1-G1/G2/G3, R1-D1..D5 accepted)
|
|
5
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T19:40:00+08:00 — cross-review round 2 (R2-G1, R2-D1, R2-D2 accepted)
|
|
6
|
-
Revised: deepseek/deepseek-flash (selected) · deepseek/deepseek-flash (physical) @ 2026-10-23T20:00:00+08:00 — cross-review round 3 details applied (R3-D1 stale width, R3-D2 abridged-code-block label)
|
|
7
|
-
|
|
8
|
-
Repo: `zhuxixi/pi-agent-board` · base `main` @ `d41a16b` · issue #231 (enhancement)
|
|
9
|
-
|
|
10
|
-
## Problem & intent
|
|
11
|
-
|
|
12
|
-
The board list marks the selected row with a 1-cell `›` and `bold`, both of which sit in the
|
|
13
|
-
same stage colour as the neighbouring glyph. At rest the cursor is not locatable; the user has
|
|
14
|
-
to tap ↑/↓ and watch which line changes.
|
|
15
|
-
|
|
16
|
-
Intent: the selected row must be identifiable **without moving the selection**, on both dark
|
|
17
|
-
and light themes, for CJK session names, and for every stage colour.
|
|
18
|
-
|
|
19
|
-
## Decisions (confirmed with the user before drafting)
|
|
20
|
-
|
|
21
|
-
| # | Decision | Choice | Why |
|
|
22
|
-
| --- | --- | --- | --- |
|
|
23
|
-
| D1 | Background colour source | `theme.bg("selectedBg", …)` | Theme-aware by construction: `dark` → `okhsl(233 41% 24%)`, `light` → `okhsl(235 19% 91%)`. Also degrades through `backgroundAnsi(mode)` for truecolor / 256 / 16-colour terminals. A hardcoded RGB is unreadable on light themes (`text` resolves dark there). |
|
|
24
|
-
| D2 | Extra cue | keep the `›` marker, **no** `▌` rail | Background answers "which row", `›` still answers "where is the cursor". A rail would collide semantically with the folder-header `▌` (`:1450-1451`, meaning "selection is inside this folder"). |
|
|
25
|
-
| D3 | Highlight extent | full row, **covering `indent`** | lazygit/fzf convention; the indent still conveys hierarchy because the glyphs stay in place. |
|
|
26
|
-
| D4 | Unselected contrast | keep `muted` | Minimal change; do not trade unselected readability for relative contrast. Revisit only if the bg proves insufficient. |
|
|
27
|
-
|
|
28
|
-
Explicitly **not** doing: per-stage desaturated backgrounds (8 states × 2 appearances to
|
|
29
|
-
validate, and a selected `working` bar would compete with the flash banner's bg bar), and the
|
|
30
|
-
`muted → dim` demotion.
|
|
31
|
-
|
|
32
|
-
## Current behaviour (measured, `src/ui/dashboard.ts` @ d41a16b) — **abridged**: the real body also composes a `badge` (batch marker / pin / worktree / status / refs) into `nameText` (:1468, :1473) and joins the marker with a literal space (:1478); neither changes the padding/colour conclusion
|
|
33
|
-
|
|
34
|
-
`renderRow` (:1459-1482):
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
marker = stageFg(state, selected ? `›${glyph}` : ` ${glyph}`)
|
|
38
|
-
name = selected ? fg("text", bold(nameText)) : fg("muted", nameText)
|
|
39
|
-
summary = selected ? fg("text", summary) : fg("muted", summary)
|
|
40
|
-
line = padTo(marker + name + folder + summary, contentWidth - width(age)) + fg("dim", age)
|
|
41
|
-
return clip(" ".repeat(indent) + clip(line, contentWidth), width)
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
No `bg` call anywhere in the row path. `ThemeLike.bg` is already declared at `:52` but unused
|
|
45
|
-
by `renderRow`.
|
|
46
|
-
|
|
47
|
-
## Design
|
|
48
|
-
|
|
49
|
-
### Component contract
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
// module-scope pure helper in src/ui/dashboard.ts (exported for the harness)
|
|
53
|
-
export function applyRowHighlight(
|
|
54
|
-
theme: Pick<ThemeLike, "bg">,
|
|
55
|
-
selected: boolean,
|
|
56
|
-
text: string,
|
|
57
|
-
): string {
|
|
58
|
-
return selected ? theme.bg("selectedBg", text) : text;
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
`renderRow` composes the row body exactly as today, then wraps **`indent + clipped body`**
|
|
63
|
-
(not the body alone) so the highlight starts at column 0:
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
const body = clip(line, contentWidth);
|
|
67
|
-
const paint = applyRowHighlight(t, selected, `${" ".repeat(indent)}${body}`);
|
|
68
|
-
return clip(paint, width);
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Ordering constraints (verified against `pi-tui` `utils.js`):
|
|
72
|
-
|
|
73
|
-
1. `theme.bg(...)` emits `opener + text + "\x1b[49m"`, so the closing sequence is always inside
|
|
74
|
-
the clipped string; nothing bleeds into the next row.
|
|
75
|
-
2. Nested `fg` codes (`\x1b[38;2;…m…\x1b[39m` from `stageFg`/`ansiFg`, and the theme's
|
|
76
|
-
`\x1b[39m`) reset the foreground only — the background survives.
|
|
77
|
-
3. `clip` = `truncateToWidth(..., "")`; when a line fits, it returns the string byte-identical
|
|
78
|
-
(the `\x1b[49m` survives). On overflow it appends a full `\x1b[0m`, which also guarantees no
|
|
79
|
-
leak — the only visible cost is that an ultra-narrow row's highlight stops at the clip point.
|
|
80
|
-
4. The body is already padded to `contentWidth` by `padTo` (:1479) and `age` is appended after
|
|
81
|
-
the pad, so the bg span necessarily includes the trailing pad **and** the age column.
|
|
82
|
-
|
|
83
|
-
### Data flow
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
renderRows → renderRow(rv, width, {indent}) → applyRowHighlight(theme, rv.id === selectedId, indent + body) → clip
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
No state, persistence, or protocol changes. `selectedId` remains the single source of truth.
|
|
90
|
-
|
|
91
|
-
### Non-goals
|
|
92
|
-
|
|
93
|
-
- No change to `select` mode's `◉/○`, `peek`/`reply`/`session`/`evidence`/`launch` layouts,
|
|
94
|
-
`help`, the flash banner, the folder-header rail, or scroll-marker lines.
|
|
95
|
-
- No theme/config option. No per-stage colours. No new dependency.
|
|
96
|
-
- No viewport changes — that is open PR #226's territory (`windowBody()` only).
|
|
97
|
-
|
|
98
|
-
### Failure behaviour / degradation
|
|
99
|
-
|
|
100
|
-
`ThemeLike` declares `bg` as required, and pi's `Theme` always implements it; a theme missing
|
|
101
|
-
`selectedBg` throws inside `bg()` rather than silently dropping the highlight. There is therefore
|
|
102
|
-
**no defensive fallback** — a host without `bg` must fail loudly, and the test harnesses are
|
|
103
|
-
updated to supply it. (Deliberate: a silent identity fallback would hide a real theme-contract
|
|
104
|
-
break and make the highlight mysteriously vanish.)
|
|
105
|
-
|
|
106
|
-
## Acceptance matrix
|
|
107
|
-
|
|
108
|
-
| ID | Feature point | Mode | Concrete verification | Pass criteria |
|
|
109
|
-
| --- | --- | --- | --- | --- |
|
|
110
|
-
| A1 | Selected row emits a bg span that opens at column 0 and closes with `\x1b[49m` | automated — integration (`DashboardComponent.render` harness) | `node --experimental-transform-types test-support/dashboard-selection-render.ts` (invoked from `test/dashboard-selection.test.mjs`) | selected line starts with the stub's bg opener, contains exactly one closer, and `visibleWidth(line) === width` |
|
|
111
|
-
| A2 | Highlight covers the trailing pad and the age column | automated — integration (same harness) | same script: strip ANSI, assert the visible text before `padTo` (name/summary/folder, i.e. the un-padded body) is shorter than `width`, while the painted line's `visibleWidth === width`; assert the age column's cells are inside the bg span by checking the bg closer is the last visible token | bg span covers columns `[0, width)` — the pre-pad text is shorter than `width` and the painted line is exactly `width` wide |
|
|
112
|
-
| A3 | Folder-group indent is inside the highlight | automated — integration (same harness, two rows under one folder header) | same script, `indent = 4` case | selected line's bg opener is at index 0, i.e. before the 4-space indent |
|
|
113
|
-
| A4 | Unselected rows carry no bg | automated — integration | same script | zero bg openers in unselected lines |
|
|
114
|
-
| A5 | `bg` is called exactly once per rendered list frame | automated — unit (spy) | same script: `bgCalls` counter in the stub theme | `bgCalls === 1` with one selected row; `0` when `selectedId` matches no visible row |
|
|
115
|
-
| A6 | `select` mode batch marker survives | automated — integration | same script: press `m` to enter select mode (`dashboard.ts:371`), press space to toggle the current row's batch marker (`dashboard.ts:403-405`), render | the selected row contains `◉`, the other row contains `○`, and the selected row's bg span is unchanged |
|
|
116
|
-
| A7 | Peek header unchanged | automated — integration | same script: open peek, assert the header line | header has no bg opener and keeps `accent + bold` name |
|
|
117
|
-
| A8 | Ultra-narrow width does not leak bg into following lines | automated — integration | same script at `width = 23` with `indent = 4` (`contentWidth = max(20, 23-4) = 20`, so `indent + contentWidth = 24 > 23` and `clip` really truncates) | every returned line's `visibleWidth <= 23`; no unselected line contains an opener; the selected line may lose its closer (documented degradation) but must not leak |
|
|
118
|
-
| A9 | Type + full suite still pass | automated — static / suite | `npm run typecheck`; `npm test`; `npm run test:perf`; `npm run test:coverage`; `npm run pack:dry` (all via `npm run verify`) | exit 0, existing thresholds 85/80/70 hold |
|
|
119
|
-
| A10 | D2: the `›` cursor marker is kept and only on the selected row | automated — integration (same harness) | same script: render list, compare marker positions | selected line contains `›` immediately before the state glyph; unselected lines contain a space in that cell |
|
|
120
|
-
| A11 | D4: unselected rows keep `muted`, not `dim` | automated — unit (spy) | same script: stub theme records `(token, text)` pairs | unselected name/summary calls use `fg("muted", …)`; no `fg("dim", …)` call wraps a name/summary (the age column's `dim` is expected) |
|
|
121
|
-
| U1 | Selected row is locatable at rest on a dark theme | user test | **prerequisite:** `git fetch && git checkout <PR branch> && npm install && pi install "$(pwd)"` (README.md:53-54; dev alternative: `ln -s "$(pwd)" ~/.pi/agent/extensions/agent-board`, README.md:68 — `npm link` does **not** load a pi extension). The extension loads from the installed path / working tree, so a stale checkout validates old code. Then restart pi, open `/agent-board` on a board with ≥5 rows, don't touch the keyboard | selected row identifiable within ~1 s without moving the cursor |
|
|
122
|
-
| U2 | Same on a light theme | user test | switch pi theme to `light`, repeat U1 | row identifiable and the name/summary stay readable (no dark-on-dark) |
|
|
123
|
-
| U3 | CJK names stay readable on the highlight | user test | rename a session to a Chinese name (e.g. `修复面板高亮`), repeat U1 | name legible at 100 % zoom, no clipped glyphs or colour fringe |
|
|
124
|
-
| U4 | All stage colours remain legible on the highlight | user test | bring one row to each of `working` / `needs answer` / `idle` / `failed` / `stopped` (dispatch or stop a session), repeat U1 | state glyph and row text readable on top of the bg for every stage |
|
|
125
|
-
| U5 | Highlight is distinguishable from existing strong signals | user test | trigger a flash banner with a real notice-producing action (dispatch a session, or stop one — the list `r`/`ctrl+r` are reply/rename, not refresh) and put the selection inside a folder group | flash bar, folder `▌`, and row highlight read as three different things |
|
|
126
|
-
|
|
127
|
-
Failure branches are covered by A4/A5 (bg must not leak to unselected or absent selections) and
|
|
128
|
-
A8 (overflow). The "theme lacks `bg`" branch is intentionally a hard failure, exercised by the
|
|
129
|
-
harness itself.
|
|
130
|
-
|
|
131
|
-
D1 (colour source) is owned by A1 + A2 for the mechanics and U1 + U2 for the visual outcome;
|
|
132
|
-
there is no separate automated check on the resolved RGB because the harness stub deliberately
|
|
133
|
-
substitutes its own colour (the theme token itself is pi's contract, pinned by
|
|
134
|
-
`theme.d.ts`).
|
|
135
|
-
|
|
136
|
-
### Guarantee boundaries
|
|
137
|
-
|
|
138
|
-
- **Guaranteed:** the built-in `dark` and `light` themes resolve `selectedBg` to a low-saturation
|
|
139
|
-
bg with adequate contrast against `text`/`muted`; verified visually by U1/U2.
|
|
140
|
-
- **Not guaranteed:** chroma fidelity on 16-colour terminals — `backgroundAnsi(mode)` downgrades
|
|
141
|
-
to the nearest palette entry, so the hue may shift. The highlight stays visible; the exact
|
|
142
|
-
shade does not.
|
|
143
|
-
- **Not guaranteed:** user-authored themes that override `selectedBg` with a low-contrast colour.
|
|
144
|
-
That is the theme author's contract, not this change's.
|
|
145
|
-
- **Not guaranteed:** terminals that ignore 24-bit SGR backgrounds entirely (extremely old
|
|
146
|
-
emulators). `backgroundAnsi` handles the standard colour modes; a terminal outside them is out
|
|
147
|
-
of scope.
|
|
148
|
-
|
|
149
|
-
## Testability split (design constraint for the implementation)
|
|
150
|
-
|
|
151
|
-
| Boundary | Artifact | How it is tested | Why this split |
|
|
152
|
-
| --- | --- | --- | --- |
|
|
153
|
-
| Highlight application (pure) | `applyRowHighlight(theme, selected, text)` — module scope, exported | unit assertions inside the harness: unselected returns input unchanged (identity/`===`), selected wraps once | Pure function, no `this`, no I/O; a regression in the wrap/close logic fails without rendering a whole frame |
|
|
154
|
-
| Row composition (impure: theme + view model) | `renderRow` | integration via `DashboardComponent.render`: A1–A4, A6–A8 | Needs the real row view model (`rows.mjs`), grouping, and `clip`/`padTo` interaction; only a rendered frame can prove pad coverage |
|
|
155
|
-
| Theme call accounting | stub theme with `bgCalls` counter | A5 | Proves the highlight is applied once, not per nested fragment |
|
|
156
|
-
| Asset/build | none | A9 | Repo gate |
|
|
157
|
-
|
|
158
|
-
The pure helper must stay free of `this`, `selectedId`, and row-view imports so the unit
|
|
159
|
-
assertions stay meaningful; `renderRow` keeps the single `selected` comparison and delegates the
|
|
160
|
-
painting. Implementation must not re-inline the ternary back into `renderRow` (that would break
|
|
161
|
-
the split and reintroduce an untested branch).
|
|
162
|
-
|
|
163
|
-
Harness file: `test-support/dashboard-selection-render.ts`, invoked by
|
|
164
|
-
`test/dashboard-selection.test.mjs` with `node --experimental-transform-types` (same pattern as
|
|
165
|
-
`dashboard-refs-render.ts`). Test file name follows the existing `dashboard-*.test.mjs`
|
|
166
|
-
convention.
|
|
167
|
-
|
|
168
|
-
### Harness stub contract (two distinct stubs, on purpose)
|
|
169
|
-
|
|
170
|
-
| Harness | Stub shape | Its assertions |
|
|
171
|
-
| --- | --- | --- |
|
|
172
|
-
| `dashboard-selection-render.ts` (new) | `bg: (_c, t) => "\x1b[48;2;1;2;3m" + t + "\x1b[49m"` with `bgCalls++`; **recording** `fg: (c, t) => { fgCalls.push([c, t]); return t; }`; identity `bold` | A1–A11 (the recording `fg` is what makes A11's `muted`-vs-`dim` assertion possible; `bgCalls` serves A5) |
|
|
173
|
-
| the 8 pre-existing `DashboardComponent` harnesses listed in the risk table | `bg: (_c, t) => t` (identity) | none — they keep rendering and their existing assertions unchanged |
|
|
174
|
-
|
|
175
|
-
An identity stub in the **new** harness would make A1–A4/A8 vacuous (no opener, no closer), which
|
|
176
|
-
is exactly what review round 1 flagged; the two-stub split keeps old harnesses untouched while
|
|
177
|
-
the new one asserts real ANSI.
|
|
178
|
-
|
|
179
|
-
## Risks & mitigations
|
|
180
|
-
|
|
181
|
-
| Risk | Mitigation |
|
|
182
|
-
| --- | --- |
|
|
183
|
-
| Existing harnesses stub `{ fg, bold }` only, so adding a `bg` call breaks them with `theme.bg is not a function` | The **new** harness uses a real stub (`bg: (c, t) => "\x1b[48;2;1;2;3m" + t + "\x1b[49m"`, plus a `bgCalls` counter) so the ANSI assertions are meaningful. **8 harnesses construct `DashboardComponent` directly** and all get the identity stub `bg: (_c, t) => t`: `dashboard-delete-key-probe.ts`, `cwd-browse-tab-probe.ts`, `dashboard-shrink-render.ts`, `dashboard-refs-render.ts`, `dashboard-prewarm-smoke.ts`, `navigation-wrap.ts`, `hold-key-wrap.ts`, `ui-smoke.ts`. Of these, only **5 actually call `.render(`** and therefore reach `renderRow` → `theme.bg`: `cwd-browse-tab-probe.ts`, `dashboard-delete-key-probe.ts`, `dashboard-refs-render.ts`, `dashboard-shrink-render.ts`, `ui-smoke.ts`; the other 3 are construct-only. `attach-delete-flow-probe.ts` constructs a real `DashboardComponent` through a factory (it never imports the class directly), so it also gets the identity stub defensively. `npm run typecheck` does **not** cover any of them (`tsconfig.json` excludes `test` and `test-support`), so a missing method surfaces as a runtime failure under `npm test` — not at typecheck. |
|
|
184
|
-
| `selectedBg` may be too subtle on some custom themes | U1/U2 cover the two built-in themes; custom themes can override the token themselves (that is a feature of D1, not a bug). If a built-in theme reads too weak, the follow-up is a token change in pi, not a hardcoded colour here. |
|
|
185
|
-
| Conflict with open PR #226 (same file, `windowBody`) | Orthogonal regions. If #226 merges first, rebase; if this merges first, #226's harness asserts line counts/presence only and is unaffected. |
|
|
186
|
-
| `\x1b[0m` from an overflowing `clip` could kill the bg on very narrow terminals | A8 pins the behaviour at `width = 23`; visual degradation is acceptable and leak-free. |
|
|
187
|
-
|
|
188
|
-
## Rollout
|
|
189
|
-
|
|
190
|
-
Single PR against `main`, no release action required in this issue (release is a separate
|
|
191
|
-
manual step per `docs/RELEASE.md`).
|