@huseynsnmz/pi-board 0.15.3 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -4
- package/CONTRIBUTING.md +7 -0
- package/README.md +55 -419
- package/SECURITY.md +19 -0
- package/docs/how-it-works.md +31 -0
- package/docs/reference.md +277 -0
- package/package.json +5 -11
- package/src/commands/agent-board.ts +26 -2
- package/src/commands/attach-flow.ts +4 -0
- package/src/core/double-press.mjs +6 -2
- package/src/core/slash-commands.mjs +52 -0
- package/src/index.ts +1 -1
- package/src/runtime/service.mjs +12 -0
- package/src/ui/dashboard.ts +59 -25
- package/src/ui/pty-attach.ts +8 -3
- 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,86 +0,0 @@
|
|
|
1
|
-
# Spec: board 删除键改为 `x` → `y` 确认(issue #150)
|
|
2
|
-
|
|
3
|
-
状态:draft(等用户确认)
|
|
4
|
-
日期:2026-09-30
|
|
5
|
-
仓库:pi-agent-board @ main `c93307d`
|
|
6
|
-
|
|
7
|
-
## 问题
|
|
8
|
-
|
|
9
|
-
board 上删除一个 session 现在是 `Ctrl+X` 连按两次(500ms 窗口,超时作废),第二次直接 archive、**无确认**。同一面板里 `d`(标 Done)、`X`(按状态批量删)、多选 `Ctrl+X`(批量删 Done)都走 `(y/N)` 确认——list 模式删除是唯一例外(`d5b2b84` 引入,当时替换掉的正是 y/N 确认)。用户反馈双击"有点麻烦",要求改为 `x` → `y` 确认,与 `d` 的心智一致。
|
|
10
|
-
|
|
11
|
-
键位前提已审计通过:list 模式小写 `x` 空闲(调研见 `research/key-occupancy-audit.md`)。
|
|
12
|
-
|
|
13
|
-
## 设计决策
|
|
14
|
-
|
|
15
|
-
| # | 决策 | 结论 | 理由 |
|
|
16
|
-
| --- | --- | --- | --- |
|
|
17
|
-
| D1 | 触发键 | list 模式小写 `x` | 空闲;与 `v`/`e`/`m`/`i`/`d` 的单字母风格一致;避开大写 `X`(已占用)与 evidence 的 `x`(已占用,不同模式) |
|
|
18
|
-
| D2 | 确认方式 | 复用 `pending` + `mode = "confirm"` + `handleConfirmKey` | 与 `d`/`X`/多选删除同构,零新增机制 |
|
|
19
|
-
| D3 | 旧 `Ctrl+X` ×2 | **保留为便捷别名**(用户 2026-09-30 拍板) | 老用户肌肉记忆不受影响;`x` → `y` 成为主要入口,`ctrl+x` ×2 仍是"免确认快捷路" |
|
|
20
|
-
| D4 | 多选态(`m`)批量删 Done | **不动**(保持 `ctrl+x`;用户 2026-09-30 拍板) | 缩小改动面;多选态与主列表的删除路径不要求同键 |
|
|
21
|
-
| D5 | 删除语义 | 不变,仍 `service.archive()` | 软删除、busy 先停 host/run、session 文件保留 |
|
|
22
|
-
| D6 | peek / session / evidence | 不动 | 这些模式本无删除入口;evidence 的 `x` 语义不同 |
|
|
23
|
-
| D7 | 与 #147 的关系 | 不动 attach 键位 | 独立议题 |
|
|
24
|
-
|
|
25
|
-
### 非目标
|
|
26
|
-
|
|
27
|
-
- 不改 `service.archive()` / 状态协调器(语义与实现均不动)。
|
|
28
|
-
- 不给 attach 会话加删除入口(#147)。
|
|
29
|
-
- 不改 `X`(大写,按状态批量删)与 evidence 的 `x`(清诊断)。
|
|
30
|
-
- 不做键位可配置化(YAGNI)。
|
|
31
|
-
|
|
32
|
-
## 组件契约
|
|
33
|
-
|
|
34
|
-
改动集中在 `src/ui/dashboard.ts`:
|
|
35
|
-
|
|
36
|
-
1. 新增 `private confirmDelete(): void`(样板照 `confirmClearDiagnostics` `:1101` / `confirmDeleteState` `:1035`):
|
|
37
|
-
- `const row = this.selectedRow(); if (!row) return;`
|
|
38
|
-
- `this.pending = { prompt: \`Delete "${row.meta.name}"?${isAgentBusy(row) ? " Stops the active run." : ""} Session file is preserved. (y/N)\`, onYes: () => { void Promise.resolve(this.deps.service.archive(row.meta.id)).then((res) => { res.ok ? this.notice(\`Deleted "${row.meta.name}"\`, "info") : this.notice(res.error ?? "Delete failed", "error"); this.refresh(); }); } }`
|
|
39
|
-
- `this.mode = "confirm"`(`returnMode` 省略即默认 `list`,与 `confirmDeleteState` 一致)
|
|
40
|
-
2. `handleListKey`(`:355-390`):新增 `if (data === "x") return this.confirmDelete();`(放在 `d`/`X` 分支旁)。`handleListKey` 首行的 arm 重置(`:356`)保持不变——按 `x` 会清掉 `ctrl+x` 的待确认态,语义正确。
|
|
41
|
-
3. **保留** `handleDeleteKey`(`:1016-1032`)、`deleteArm`(`:135`)、`DELETE_DOUBLE_PRESS_MS`(`:1803`)、`handleSelectKey` 的多选 `ctrl+x`(`:405`)——D3/D4 不变。
|
|
42
|
-
4. 文案同步(两条路径都要能查到,主入口 `x` 在前):
|
|
43
|
-
- list hints(`:1316`):`ctrl+x x2 delete` → 两个 token:`x delete (y/N)` 与 `ctrl+x x2 quick`
|
|
44
|
-
- help overlay(`:1667-1668`):`d` 行下一并列出 `["x", "Delete selected session (y/N confirm)"]` 与既有 `["ctrl+x x2", "Delete selected session (quick double-press, no confirm)"]`
|
|
45
|
-
- `README.md:117`(bullet)与 `README.md:142`(表格):`x` 确认式为主、`Ctrl+X` ×2 标注为免确认快捷方式
|
|
46
|
-
- select hints(`:1311`)与 help `:1671` **不动**(多选态保持 `ctrl+x`)
|
|
47
|
-
|
|
48
|
-
不新增 export、不改 `PtyAttachResult`/协议、不改 `service` 层。
|
|
49
|
-
|
|
50
|
-
## 验收矩阵
|
|
51
|
-
|
|
52
|
-
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
53
|
-
| --- | --- | --- | --- | --- |
|
|
54
|
-
| A1 | `x` 打开确认提示(idle 行) | 自动化(unit,组件探针) | 新增 `test-support/dashboard-delete-key-probe.ts`(真 service 工厂);`handleInput("x")` | `mode === "confirm"`;prompt 为 `Delete "name"? Session file is preserved. (y/N)`;`archive` 未被调用 |
|
|
55
|
-
| A1b | 空列表 / 无选中行按 `x` | 自动化(unit) | probe:空 root(无 view)构造组件 → `handleInput("x")` | 无崩溃、`mode === "list"`、不进入 confirm |
|
|
56
|
-
| A1c | busy 行提示会停 run | 自动化(unit) | probe:`setRowPatch` 置 `alive:true, state:{semanticState:"working"}` → `x` | prompt 含 `Stops the active run.`(idle 行不含) |
|
|
57
|
-
| A2 | `y` 执行软删除并回列表 | 自动化(unit,组件探针 + 真 temp-root service) | 同 probe 第二场景:`createView` ×2(navigation-wrap 模式)→ `x` → `y` | `service.rows()` 不含被删 id;notice `Deleted "name"`;`mode === "list"`;选中落相邻行 |
|
|
58
|
-
| A3 | 取消路径不删除 | 自动化(unit) | probe:`x` 后分别注入 `n`、`\x1b`(esc)、再次 `x`、`q` | 无 archive 调用、无 `Deleted` notice、`mode === "list"` |
|
|
59
|
-
| A4 | 旧双击别名仍可用(D3 回归) | 自动化(unit) | probe:注入 `\x18` ×2(间隔 0ms) | `archive` 被调用一次、无 confirm 步骤;间隔 700ms 的组合不删除(窗口语义不变;700ms > 500ms 窗口留余量) |
|
|
60
|
-
| A5 | insert 模式 `x` 是普通字符 | 自动化(unit) | probe:`i` 进 dispatch → 输入 `x` | `dash.input` 含 `x`;无 confirm/archive |
|
|
61
|
-
| A6 | 多选态路径不变(D4 回归) | 自动化(unit) | probe:`m` → 选中 Done 行 → `x`(应无副作用)→ `ctrl+x` → `y` | `x` 在多选态无任何副作用;`ctrl+x` 仍打开批量确认(`returnMode === "select"`);`y` 经 `service.archiveMany` 归档且行从列表消失。注意:`archiveMany` 内部直调 `archiveView`、绕过 `service.archive`,须单独包记录器 |
|
|
62
|
-
| A7 | 文案同步(两条路径都可发现) | 自动化(unit + static) | probe 断言 list hints 与 help overlay 同时含 `x delete` 与 `ctrl+x x2`;`rg -n "Ctrl\+X twice quickly|Twice quickly" README.md` 命中且周围提到 `x` + `(y/N)` | 断言通过;README 两处均说明两条路径 |
|
|
63
|
-
| U1 | 真机交互确认 | 用户实测 | 起 board:选中一行 → `x` → 看提示 → `y`;对 Working 行重做;再验 `ctrl+x` ×2 老路径;删后查 `views/<id>/meta.json` 与 session 文件 | 行消失、host 停止、session 文件仍在;Working 行先停 run 再 archive;`x`→`n`/`esc` 无副作用;老双击仍按原语义删除 |
|
|
64
|
-
|
|
65
|
-
## 可测性拆分设计
|
|
66
|
-
|
|
67
|
-
- **不新增纯函数**:本次改动是"两个按键分支 + 一个复用既有 confirm 引擎的方法",逻辑已天然分离——副作用边界 = `deps.service.archive()`,确认会话 = `handleConfirmKey()`。再抽纯函数只会为测试增加表面,无行为收益。
|
|
68
|
-
- **单工厂探针**:所有场景共用 `makeEnv()` —— 真 temp-root service(复用 `test-support/navigation-wrap.ts` 的 `mkdtempSync + createView + createService` 模式)+ 两个记录 wrapper(`service.archive` 与 `service.archiveMany`——后者内部直调 `archiveView`、绕过前者,必须单独包)+ `setRowPatch(fn)` 行补丁(busy 行 `alive:true, semanticState:"working"`;Done 行 `semanticState:"completed"`)。A2/A6 借真 service 验证"归档后行真的从列表消失"这条端到端组件路径;A1/A1b/A1c/A3/A4/A4b/A5 只断言提示与调用记录。
|
|
69
|
-
- **唯一真实计时点**:A4b 用 700ms sleep 验证 500ms 双击窗口过期——`setTimeout` 回调触发时墙钟必 ≥700ms > 窗口,不受机器负载影响,确定性成立。
|
|
70
|
-
- **驱动方式**:只走公开 `handleInput()`;断言可白盒读 `mode` / `pending` / `flash` / `input`(`navigation-wrap.ts` 已有先例)。按键编码用裸字节(`x`、`y`、`\x18`、`\x1b`)——dashboard 侧无终端编码依赖。
|
|
71
|
-
- **测试边界(不得越界)**:不测 `service.archive` 内部(`test/service.test.mjs` 已覆盖);不测 attach / pty 路径(#147);不测 pi-tui `matchesKey`(上游职责);不依赖真实 host 进程。
|
|
72
|
-
- **新增文件**:`test-support/dashboard-delete-key-probe.ts`(tsconfig 已排除 test-support,不参与类型检查)+ `test/dashboard-delete-key.test.mjs`(`execFileSync` 跑探针,模式同 `dashboard-navigation.test.mjs`)。
|
|
73
|
-
|
|
74
|
-
## 风险
|
|
75
|
-
|
|
76
|
-
- **两套删除路径并存(D3)**:`deleteArm` 状态机保留,`x` 与 `ctrl+x` 的交互需明确——按 `x` 会清掉待确认的 arm(`:356` 现有逻辑),不会出现"x 确认中又触发双击"的叠加;A1/A4 用例分别钉住两条路径。
|
|
77
|
-
- **误按 `x` 后随手 `y`**:确认提示带行名,且有 `(y/N)` 兜底;代价是比双击多一次按键,属用户自选的主入口。
|
|
78
|
-
- **文案漂移**:同一事实现在散在 hints / help / README 三处且涉及两条路径,A7 用断言 + rg 钉住。
|
|
79
|
-
- **双入口的文档负担**:README 需同时解释"确认式 `x`"与"免确认 `Ctrl+X` ×2",措辞要避免让用户以为两者等价。
|
|
80
|
-
|
|
81
|
-
## 决策记录(2026-09-30 用户拍板)
|
|
82
|
-
|
|
83
|
-
1. D3 = **保留** `Ctrl+X` ×2 作为免确认快捷别名(不移除状态机)。
|
|
84
|
-
2. D4 = 多选态批量删**保持** `ctrl+x`,本次不改。
|
|
85
|
-
|
|
86
|
-
scope 收敛为:**只在 list 模式新增 `x` → `(y/N)` 确认入口**;其余键位与语义全部保持。下一步:worktree `issue-150-board-delete-key-x-y-confirm` + writing-plans。
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# Issue #153 Design — job-runner boot-window stop loss (stop latch)
|
|
2
|
-
|
|
3
|
-
Status: **approved 2026-09-30 (D2 = (a), split out of #95 per D3)** · Base: `main@444199d`
|
|
4
|
-
Parent: issue #95 spec (F1) — this doc is self-contained; executors do not need #95's spec.
|
|
5
|
-
|
|
6
|
-
## 1. Problem (experimentally proven)
|
|
7
|
-
|
|
8
|
-
`runner/job-runner.mjs` publishes an observable `working` state (the coordinator's
|
|
9
|
-
`run_started` creates `status.json`, `:95-112`) **before** it spawns the worker and **before**
|
|
10
|
-
it registers `process.on("SIGTERM", stop)` (`:356-371`). A stop signal landing in that window
|
|
11
|
-
dies with Node's default action: the runner exits silently — no `finalizeRun`, no
|
|
12
|
-
`run_finalized`, no `endedAt`, not even the `worker_pid` diagnostic. The row stays
|
|
13
|
-
`working/alive` forever.
|
|
14
|
-
|
|
15
|
-
Production reach: `archiveView()` (`src/runtime/service.mjs:726-735`) sends `killProcess(pid)`
|
|
16
|
-
(SIGTERM → 4 s → SIGKILL) to a just-started run's pid — exactly the tested path. The CI flake
|
|
17
|
-
`stopping the runner finalizes the run as stopped` (burns exactly its 15 s wait; neighbour test
|
|
18
|
-
passes in 260 ms; red on 09-25 and 09-30, both re-run green) is this race under slow machines.
|
|
19
|
-
|
|
20
|
-
Proof (2026-09-30, local, controlled): injecting an 800 ms window between `run_started` and the
|
|
21
|
-
spawn → 3/3 deterministic failures with the CI TAP fingerprint; the same window plus a
|
|
22
|
-
module-scope SIGTERM latch → 3/3 passes; unmodified runner under load → 2/2 failures. Full
|
|
23
|
-
method + preserved failure state in #95's research notes.
|
|
24
|
-
|
|
25
|
-
## 2. Invariant
|
|
26
|
-
|
|
27
|
-
**No non-terminal state may be observable before the process can handle a stop, and a
|
|
28
|
-
SIGTERM/SIGINT delivered at any time after the module is loaded must produce a terminal
|
|
29
|
-
state.**
|
|
30
|
-
|
|
31
|
-
## 3. Design (decided: module-scope latch, replay into the existing `stop()`)
|
|
32
|
-
|
|
33
|
-
| Layer | Artifact | Kind |
|
|
34
|
-
|---|---|---|
|
|
35
|
-
| Pure state machine | `src/core/stop-latch.mjs` — `createStopLatch()` → `{ note(signal), pending(), take() }`; `note` is idempotent (first signal wins, SIGINT/SIGTERM collapse), `take` reads-and-clears once | pure |
|
|
36
|
-
| I/O shell | `runner/job-runner.mjs` — latch handlers installed at module scope; `stop_latch_armed` emitted as the run's **first** diagnostic (before `runner_start`); after the real handlers are wired, `if (stopLatch.take() != null) stop()` replays an early stop | side-effectful |
|
|
37
|
-
| Test-only window | `AGENT_BOARD_TEST_BOOT_WINDOW_MS` delays the runner between `run_started` and the spawn so the window is hit deterministically, not by timing luck | side-effectful, test-only |
|
|
38
|
-
|
|
39
|
-
Replay semantics (accepted trade-off, #95 D2=(a)): a stop landing in the window is remembered,
|
|
40
|
-
the runner continues, spawns the worker, and the replayed `stop()` kills it immediately after
|
|
41
|
-
handler wiring — a doomed worker really is spawned once. Signals after wiring run both the
|
|
42
|
-
latch note (harmless bookkeeping) and `stop()`; `take()` fires at most once, and `stop()`
|
|
43
|
-
itself is guarded by `worker.killed`, so no double-fire.
|
|
44
|
-
|
|
45
|
-
Residual window (documented, harmless): a signal during module *import* — before the latch
|
|
46
|
-
handlers exist — still exits by default, but at that point nothing has been published by the
|
|
47
|
-
runner, so no runner-written state is lost. (The parent's `mark_queued`/`pid.json` predate the
|
|
48
|
-
child; that pre-module shape is converged by `service.reconcile()` — A10.) A3 pins "nothing
|
|
49
|
-
published by the runner before the latch" via the first-diagnostic ordering.
|
|
50
|
-
|
|
51
|
-
Windows platform scope: Node on Windows ignores the signal name and hard-kills, so no handler
|
|
52
|
-
can run — this fix is Unix-only and Windows behaviour is unchanged (Non-goal; the durable
|
|
53
|
-
stop-intent alternative is parked in #95 spec §7).
|
|
54
|
-
|
|
55
|
-
## 4. Acceptance (IDs沿用 #95 spec 矩阵)
|
|
56
|
-
|
|
57
|
-
| ID | Item | Verification type | Concrete check | Pass criterion |
|
|
58
|
-
|---|---|---|---|---|
|
|
59
|
-
| A1 | A stop inside the boot window still finalizes | integration (deterministic) | `test/runner-stop-window.integration.test.mjs`: leg 1 sets `AGENT_BOARD_TEST_BOOT_WINDOW_MS=800`, SIGTERMs as soon as `working` is observable; leg 2 runs the same flow without the knob | both legs: `status.json.endedAt != null` and `semanticState === "stopped"`; leg 2 also proves the knob is inert when unset |
|
|
60
|
-
| A2 | Stop-latch state machine | unit | `test/stop-latch.test.mjs` | note/take/repeat/collapse/empty/independent-instance paths; no double-fire |
|
|
61
|
-
| A3 | Nothing published by the runner before the latch | integration | assert the run's first `diagnostics.jsonl` entry is `stop_latch_armed` (single-file observable, ahead of `runner_start`) | any other first entry fails |
|
|
62
|
-
| A4 | The regression case survives load | integration (repeat, **one-time procedure — not a recurring CI test**) | 20 consecutive local runs of `test/runner.integration.test.mjs` while the full suite runs in parallel | 20/20 green (pre-fix baseline: failures within the first 2–3 runs under load) |
|
|
63
|
-
| A10 | A dead-runner row converges | integration (deterministic) | SIGKILL the runner after `run_started`, then call `service.reconcile()` | `state.json` converges to `semanticState: "failed"`, `processState: "exited"`, summary `Failed (runner exited)` within the same pass (path exists: `service.mjs:2118` reconcile → `reconcile_finalize`) |
|
|
64
|
-
|
|
65
|
-
`npm run verify` must exit 0 at branch head (A8-level gate inherited from repo convention).
|
|
66
|
-
U1/U2 (user acceptance against an installed build) stay with #95.
|
|
67
|
-
|
|
68
|
-
## 5. Risks / Non-goals
|
|
69
|
-
|
|
70
|
-
- Windows: unfixable at the signal layer (above); recorded, not addressed here.
|
|
71
|
-
- The test knob lives in a production file: `AGENT_BOARD_TEST_*` naming, inert-when-unset
|
|
72
|
-
proven by A1 leg 2, behaviour proven by leg 1.
|
|
73
|
-
- Budget-nesting fixes for `runner.integration.test.mjs:282` belong to #95 F2 and **must land
|
|
74
|
-
after this issue** — widening that budget here would paper over the race.
|
|
75
|
-
- `pty-runner` / `state-coordinator` boot windows are unaudited (follow-up, #95 §7).
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# Spec — issue #95: real-process tests must not flake (runner stop-safety + budget/diagnosis discipline)
|
|
2
|
-
|
|
3
|
-
Status: **branch spec (refreshed 2026-09-30 late): tonight's scope = F3 → F2 → F4 only** · Base: `main@43dca95` (2026-09-30) · Route: bug → systematic-debugging
|
|
4
|
-
|
|
5
|
-
## Delivery status (2026-09-30)
|
|
6
|
-
|
|
7
|
-
- **F1 delivered by #153 / PR #155** (squash `43dca95`, merged 23:34): stop latch + `stop_latch_replayed`; A1–A4 and A10 all green there, CR two-stream Round-1 PASS.
|
|
8
|
-
- **A10's convergence fix also landed in #153** (`loadRow` recorded-pid-wins-over-mirror).
|
|
9
|
-
- Acceptance matrix below is narrowed to what remains on THIS branch: **A5, A6, A7, A8** (+ A9 stays open as the 14-day CI observation; A11 N/A while D4 = defer; A12 N/A while D1 = O3).
|
|
10
|
-
- F5 (serialization) stays deferred per D1 = O3 — the observation window is running; tonight's three flake observations are its first baseline samples.
|
|
11
|
-
- F7 (issue trail) is complete: research conclusions + falsified-claim corrections are on the issue thread.
|
|
12
|
-
Research: `research/2026-09-30-runner-stop-window-race.md`, `research/2026-09-30-flake-inventory-and-cost.md`
|
|
13
|
-
|
|
14
|
-
## 1. Root-cause revision (this round's main result)
|
|
15
|
-
|
|
16
|
-
The issue's previous framing — "CI machine is slow, the tests need bigger wall-clock budgets"
|
|
17
|
-
(comment 1 §三) — is **falsified for the highest-frequency failure**
|
|
18
|
-
(`stopping the runner finalizes the run as stopped`).
|
|
19
|
-
|
|
20
|
-
`runner/job-runner.mjs` publishes an observable `working` state (via the coordinator's
|
|
21
|
-
`run_started`, which creates `status.json`) **before** it spawns the worker and **before** it
|
|
22
|
-
installs `process.on("SIGTERM", stop)` (`:95-112` vs `:356-371`). A stop request that lands in
|
|
23
|
-
that window kills the runner with Node's default action: no `finalizeRun`, no `run_finalized`,
|
|
24
|
-
no `endedAt`, no diagnostics. Proven by a controlled experiment (inject the window → 3/3
|
|
25
|
-
deterministic failures; add an early stop latch → 3/3 passes; unmodified runner under load →
|
|
26
|
-
2/2 failures locally, and the same signature appears in CI on 09-25 and 09-30 with
|
|
27
|
-
`duration_ms ≈ 15200` against a 15 s internal wait).
|
|
28
|
-
|
|
29
|
-
Sample-size honesty: each experiment arm is n = 2–3. The claim rests on the mechanism closing
|
|
30
|
-
the loop — widen the window and the failure becomes deterministic, arm the latch and it
|
|
31
|
-
disappears — not on the counts themselves.
|
|
32
|
-
|
|
33
|
-
This is reachable in production: `archiveView()` kills the current run pid with SIGTERM
|
|
34
|
-
(`src/runtime/service.mjs:726-735` → `killProcess`), so archiving/stopping a **just-started**
|
|
35
|
-
run can leave the row pinned at `working/alive`. Whether anything then converges such a row
|
|
36
|
-
(reconcile / pid-liveness watching) is **unverified** — promoted to acceptance item A10.
|
|
37
|
-
|
|
38
|
-
The other three known signatures (the host-concurrency `A10` resolver burn,
|
|
39
|
-
`subscribe_terminal` 15 s, `A5 burst` 30 s) were **not** re-proven; they stay in scope as "unexplained until the failure can name its
|
|
40
|
-
own blocked step" (→ F3).
|
|
41
|
-
|
|
42
|
-
## 2. Deliverables
|
|
43
|
-
|
|
44
|
-
### F1 — Runner stop-safety (production fix) — *primary*
|
|
45
|
-
|
|
46
|
-
Invariant: **no non-terminal state may be observable before the process can handle a stop, and
|
|
47
|
-
a SIGTERM/SIGINT delivered at any time after the module is loaded must produce a terminal
|
|
48
|
-
state.**
|
|
49
|
-
|
|
50
|
-
Platform scope: the latch/handler mechanism is **Unix-only**. On Windows, Node's `process.kill`
|
|
51
|
-
ignores the signal name and hard-kills the target — no handler can ever run — so the
|
|
52
|
-
boot-window loss is unfixable at the signal layer there. Either Windows stays out of scope
|
|
53
|
-
(current behaviour unchanged, recorded in Non-goals) or D2 option (d) is chosen, which does not
|
|
54
|
-
depend on signals being catchable.
|
|
55
|
-
|
|
56
|
-
Design (testability split):
|
|
57
|
-
|
|
58
|
-
| Layer | Artifact | Kind |
|
|
59
|
-
|---|---|---|
|
|
60
|
-
| Pure state machine | `src/core/stop-latch.mjs` — `createStopLatch()` → `{ note(signal), pending(), take() }` (idempotent note, single take, SIGINT/SIGTERM collapse) | pure |
|
|
61
|
-
| I/O shell | `runner/job-runner.mjs` — handlers installed at module scope; `take()` replayed into the existing `stop()` once it exists; emits a `stop_latch_armed` diagnostic as the run's **first** diagnostic, before the coordinator bootstrap | side-effectful |
|
|
62
|
-
| Test-only window | `AGENT_BOARD_TEST_BOOT_WINDOW_MS` delaying the runner between `run_started` and the spawn, so the window can be hit deterministically instead of by luck | side-effectful, test-only |
|
|
63
|
-
|
|
64
|
-
The test knob lives in a production file, so it carries the same guard as F4's knobs:
|
|
65
|
-
`AGENT_BOARD_TEST_*` naming, a unit test proving it is inert when unset, and a behaviour test
|
|
66
|
-
proving it actually widens the window (it must not be able to rot into a no-op).
|
|
67
|
-
|
|
68
|
-
Replay semantics (this is what D2 weighs): with the latch alone, a stop landing in the window
|
|
69
|
-
is *remembered*, the runner continues, spawns the worker, and the replayed `stop()` kills it
|
|
70
|
-
immediately after the handlers are wired — i.e. a doomed worker really is spawned once.
|
|
71
|
-
D2 (b) avoids that wasted spawn by publishing `run_started` only after spawn + handler wiring,
|
|
72
|
-
at the cost of reordering the bootstrap (the coordinator's `run_progress` needs a materialized
|
|
73
|
-
status).
|
|
74
|
-
|
|
75
|
-
### F2 — Budget audit gate (mechanical discipline)
|
|
76
|
-
|
|
77
|
-
`scripts/budget-audit.mjs` with pure exports `parseTestBudgets(source)` / `auditBudgets(files)`:
|
|
78
|
-
- rule (a): a test declaring `{ timeout: T }` must satisfy `T ≥ max(single explicit wait in the
|
|
79
|
-
body) + margin`, where **`margin = max(5 000 ms, wait / 3)`** (a named constant in the audit
|
|
80
|
-
script, unit-tested). Summing all waits would demand 120 s+ timeouts for multi-wait files
|
|
81
|
-
(`terminal-snapshot` alone has four 30 s waits) — re-inflating the very budgets this issue
|
|
82
|
-
retires;
|
|
83
|
-
- rule (b): a test that drives an app-level deadline ≥ 30 s must declare a timeout ≥ that
|
|
84
|
-
deadline **unless** it goes through an F4 knob, in which case `// budget: knob` exempts it
|
|
85
|
-
(static analysis cannot know a runtime knob compressed the deadline);
|
|
86
|
-
- escape hatch: an inline `// budget: <reason>` comment exempts the line (audited by the unit test).
|
|
87
|
-
|
|
88
|
-
Known tension, stated openly: derived budgets are still wall-clock numbers. The audit buys
|
|
89
|
-
*accountability* — no unsatisfiable nesting, every number carries a recorded reason — not
|
|
90
|
-
immunity to slow machines; that part is F1/F3/F4's job.
|
|
91
|
-
|
|
92
|
-
Amends the two inversions the spec predicted rather than fixing them as written:
|
|
93
|
-
`test/runner.integration.test.mjs:282` already satisfies rule (a) under max+margin
|
|
94
|
-
(20000 ≥ 15000 + 5000) and needed no change; the delivered fixes are the two rule-derived
|
|
95
|
-
widenings (`test/pty-attach-cold-start-e2e.test.mjs`'s slow-boot test 30000 → 35000 and
|
|
96
|
-
`test/runner.integration.test.mjs:489` 30000 → 35000) plus `host-concurrency A10`'s explicit
|
|
97
|
-
`// budget:` escape. Wired as `test/budget-audit.test.mjs` so `npm test` enforces it, plus
|
|
98
|
-
unit fixtures that must fail.
|
|
99
|
-
|
|
100
|
-
### F3 — Failure self-diagnosis (the next red names its own blocked step)
|
|
101
|
-
|
|
102
|
-
`test-support/flake-postmortem.mjs`: pure `formatPostmortem({ status, state, host, diagnosticsTail, journalTail })`
|
|
103
|
-
→ human-readable "last observed" block; I/O shell `capturePostmortem(root, viewId, runId)`; the
|
|
104
|
-
four flaky helpers' `waitFor` wrappers attach it to the assertion error on timeout.
|
|
105
|
-
Unit tested with fixtures (missing file, truncated tail, absent diagnostics).
|
|
106
|
-
|
|
107
|
-
### F4 — Test-only ladder knobs (stop paying production grace)
|
|
108
|
-
|
|
109
|
-
Expose test-scoped overrides for the wall-clock ladders that tests currently have to wait out:
|
|
110
|
-
`HOST_START_GRACE_MS`, `HOST_RECOVERY_GRACE_MS`, `HOST_RECOVERY_POLL_MS`,
|
|
111
|
-
`ATTACH_RESOLVE_TIMEOUT_MS` (`src/runtime/service.mjs:61-69`), `HOST_PROBE_TIMEOUT_MS`,
|
|
112
|
-
`ATTACH_RECONNECT_TIMEOUT_MS`. Names are `AGENT_BOARD_TEST_*`-scoped; production defaults stay
|
|
113
|
-
byte-identical when unset (unit-tested), and a test asserts the knob actually changes behaviour
|
|
114
|
-
(so it can never rot into a no-op).
|
|
115
|
-
|
|
116
|
-
Testability split: each constant gains a pure reader beside its definition — e.g.
|
|
117
|
-
`resolveHostStartGraceMs(env = process.env)` returning override-or-default — and the I/O shell
|
|
118
|
-
is reduced to passing `process.env` at the call site. Unit tests: unset ⇒ default byte-identical;
|
|
119
|
-
set ⇒ override used; invalid value ⇒ default plus one warn diagnostic. The integration proof
|
|
120
|
-
(A7) drives the host-concurrency `A10` recovery chain under the knob with a shortened budget.
|
|
121
|
-
|
|
122
|
-
### F5 — Serialization (only if D1 says so)
|
|
123
|
-
|
|
124
|
-
- O1: two-phase runner (`scripts/run-tests.mjs`) used by `test` / `test:coverage`: phase 1 =
|
|
125
|
-
real-process files (concurrency 1 or 2), phase 2 = the rest. No `ci.yml` change.
|
|
126
|
-
- O2: split the CI step into two steps (`ci.yml` change).
|
|
127
|
-
- O3: defer; re-measure after F1–F4 (the issue's own "若再红则升级串行方案" clause).
|
|
128
|
-
Measurement plan so O3 is falsifiable: count in-scope CI reds (failures matching the research
|
|
129
|
-
note's §3 signatures, or any new real-process red) per week, recorded in the issue thread;
|
|
130
|
-
observation window 14 days. Zero in-scope reds ⇒ close the issue; any in-scope red ⇒
|
|
131
|
-
revisit D1 with the accumulated evidence.
|
|
132
|
-
Cost, recomputed from the same TAP model with honest makespan arithmetic (today's 67.7 s ≈
|
|
133
|
-
max(45 s largest file, 321 s ÷ 3); the two phases are **sequential, they do not overlap**):
|
|
134
|
-
two-lane ≈ 110 s (phase 1 at c=2) + 35–50 s (phase 2 at c=3) = 150–175 s → **+80–105 s
|
|
135
|
-
(≈ +1.5 min) per CI job**; fully serial ≈ 260 s → **+3–3.5 min per job**. The same delta lands
|
|
136
|
-
on every local `npm test` run. CI re-measurement stays mandatory before these numbers are
|
|
137
|
-
treated as final.
|
|
138
|
-
|
|
139
|
-
### F6 (optional) — Test hygiene
|
|
140
|
-
|
|
141
|
-
924 leaked `/tmp/agentview-*` roots (9.7 MB) and one observed leaked detached coordinator.
|
|
142
|
-
Scope if chosen: remove the roots in the leaking suites, add a static "no unremoved mkdtemp root"
|
|
143
|
-
check, and a post-run assertion that no detached coordinator survives a test file.
|
|
144
|
-
|
|
145
|
-
### F7 — Issue trail & fact correction (documentation, no code)
|
|
146
|
-
|
|
147
|
-
Two actions inside the plan (not commit content): (1) post this round's research conclusions —
|
|
148
|
-
the root-cause experiment table and the revised framing — as comments on issue #95; (2) correct
|
|
149
|
-
the now-falsified claims in the thread, chiefly comment 1 §三's "环境问题而不是生产缺陷" and the
|
|
150
|
-
budget-widening direction, so a future reader cannot re-derive the wrong design from the
|
|
151
|
-
thread. This recovers the "comment-fact correction" decision point referenced in issue comment 3
|
|
152
|
-
(the 09-25 spec draft it pointed to never landed on disk and was lost).
|
|
153
|
-
|
|
154
|
-
## 3. Acceptance matrix
|
|
155
|
-
|
|
156
|
-
| ID | Item | Verification type | Concrete check | Pass criterion |
|
|
157
|
-
|---|---|---|---|---|
|
|
158
|
-
| A1 | A stop signal inside the boot window still finalizes the run | integration (deterministic) | new `test/runner-stop-window.integration.test.mjs`: inject `AGENT_BOARD_TEST_BOOT_WINDOW_MS`, SIGTERM inside the window, and a second leg without injection | both legs: `status.json.endedAt != null` and `semanticState === "stopped"` |
|
|
159
|
-
| A2 | Stop latch state machine | unit | `test/stop-latch.test.mjs` | note/take/repeat/no-signal paths covered; no double-fire |
|
|
160
|
-
| A3 | "Nothing observable before stop is handleable" ordering | integration | assert `stop_latch_armed` is the **first entry** of the run's `diagnostics.jsonl` (ahead of the existing `runner_start`) — a single-file observable, no cross-file timestamp comparison | any other first entry fails the test |
|
|
161
|
-
| A4 | The regression case survives load | integration (repeat) | 20 consecutive local runs of `test/runner.integration.test.mjs` while the full suite runs in parallel — **one-time acceptance procedure, not a recurring CI test** | 20/20 green (baseline: 2–3 failures in the first few runs before the fix) |
|
|
162
|
-
| A5 | Budget audit gate | unit + static | `test/budget-audit.test.mjs` over fixtures **and** the real `test/` tree | fixtures with known inversions are reported; real tree reports zero violations |
|
|
163
|
-
| A6 | Failure self-diagnosis | unit + integration | `formatPostmortem` fixtures (missing file, truncated tail) + the wrapped `waitFor` exercised with a **stubbed always-false predicate and a tiny real timeout** — no 15 s burn inside `npm test` | the failure message contains status/state/host/diagnostics excerpts and a "last observed state" line |
|
|
164
|
-
| A7 | Ladder knobs | unit + integration | knob set/unset comparison, then the host-concurrency `A10` chain re-driven under the knob | unset ⇒ production defaults unchanged; set ⇒ same assertions pass with a shorter budget |
|
|
165
|
-
| A8 | Full gate | static + build | `npm run verify` (typecheck, perf gate, tests, coverage thresholds, pack:dry) | exit 0 |
|
|
166
|
-
| A9 | CI observation | automated E2E (CI) | ≥5 consecutive CI runs **or ≥2 days**, whichever comes first, after merge (matches the issue's original acceptance direction) | no in-scope red (other issues excluded explicitly) |
|
|
167
|
-
| A10 | A dead-runner row is converged | integration | construct the boot-window death (runner killed after `run_started`, no `endedAt`); **locate** whichever convergence path actually exists (reconcile / refresh / sweeper) and drive it | a located path converges the row within 15 s; no path found, or no convergence ⇒ **blocking finding** and F1's scope extends with the convergence fix — U1 depends on it |
|
|
168
|
-
| A11 | No leaked roots / processes (enforced only if D4 = yes) | static + integration | leak audit over `test/` + post-run process check | zero unremoved temp roots; zero surviving detached coordinators |
|
|
169
|
-
| A12 | Two-phase runner is faithful (enforced only if D1 ≠ O3) | static + integration | one local run of the two-phase wrapper: aggregated test count vs the single-phase run; `npm run test:coverage`; record the wall-clock delta | identical test count (no double-run, no skip); coverage thresholds still pass; measured wall-clock delta within the bound declared in F5 |
|
|
170
|
-
| U1 | User-observed: stop/delete a just-started run | user | on an installed build, archive/stop a row seconds after its run starts | the row reaches a terminal state (stopped/archived), never stuck at `working` |
|
|
171
|
-
| U2 | User-observed: two days of normal use | user | daily board use | no row stuck at `working` without an error and without a converge/reconcile step |
|
|
172
|
-
|
|
173
|
-
Automated items must be executed and recorded with their real commands in the PR; U1/U2 stay
|
|
174
|
-
`pending` until the user runs them (they cannot be replaced by A-items).
|
|
175
|
-
|
|
176
|
-
## 4. Decisions requested
|
|
177
|
-
|
|
178
|
-
- **D1 (scope of serialization)**: O1 two-phase local-runner (no `ci.yml`) / O2 CI split /
|
|
179
|
-
O3 defer until F1–F4 are measured. Cost: see F5. **Recommendation: O3** — the proven defect
|
|
180
|
-
is F1, and the measurement plan makes revisiting D1 cheap and evidence-based.
|
|
181
|
-
- **D2 (F1 shape)**: (a) module-scope latch replaying into `stop()` — a stop in the window
|
|
182
|
-
results in spawn-then-immediate-kill of a doomed worker (cheap, experimentally proven);
|
|
183
|
-
(b) publish `run_started` only after spawn + handler wiring — no wasted spawn, but reorders
|
|
184
|
-
the bootstrap and must re-prove the progress-beat path; (c) both; (d) durable stop-intent
|
|
185
|
-
marker (file / coordinator command) instead of a signal — the only option that also fixes
|
|
186
|
-
Windows, at materially higher design cost. **Recommendation: (a) now** — cheap and
|
|
187
|
-
experimentally proven; (d) stays parked as the long-term open question (§7).
|
|
188
|
-
- **D3 (packaging)**: does F1 ship as **its own issue/PR** (it is a production defect found via
|
|
189
|
-
the test) with #95 keeping only the test-discipline work F2–F5, or does everything ride one
|
|
190
|
-
branch? Recommendation: split — F1's review question ("can a stop be lost?") is unrelated to
|
|
191
|
-
#95's review question ("is the suite stable?"), and F1 can merge first. If split: A1–A4 and
|
|
192
|
-
A10 travel with the F1 issue (they verify F1 alone); A5–A8, A11 stay here; ordering
|
|
193
|
-
constraint — **F2's budget fix for `runner.integration.test.mjs:282` must land after F1**, or
|
|
194
|
-
the budget change papers over the race again; and F7's comments cover both issues.
|
|
195
|
-
- **D4 (optional F6 hygiene)**: include or defer. **Recommendation: defer** — cheap but
|
|
196
|
-
orthogonal; fold it into whichever branch lands last.
|
|
197
|
-
|
|
198
|
-
## 5. Non-goals
|
|
199
|
-
|
|
200
|
-
- No further wall-clock widening (40 → 90 → 150 s is the pattern being retired).
|
|
201
|
-
- `ci.yml` untouched unless D1 chooses O2.
|
|
202
|
-
- No reopening of #121 / #132 / #140; no changes to the attach subsystem's behaviour.
|
|
203
|
-
- If D2 ≠ (d), Windows stop-safety is explicitly out of scope: signals are uncatchable there,
|
|
204
|
-
current Windows behaviour is unchanged, and that limitation is documented.
|
|
205
|
-
- Serialization is not allowed to double-run tests or to break coverage aggregation.
|
|
206
|
-
|
|
207
|
-
## 6. Risks
|
|
208
|
-
|
|
209
|
-
| Risk | Mitigation |
|
|
210
|
-
|---|---|
|
|
211
|
-
| Residuum: a SIGTERM during module *import* (before the latch exists) still exits by default — but nothing has been published yet, so no observable state is lost | Documented as a known window; A3 pins "no observable state before the latch" |
|
|
212
|
-
| F2's source regex mis-parses an exotic test shape | Minimal rules + explicit `// budget:` escape hatch + fixtures; audit is unit-tested |
|
|
213
|
-
| F4 knobs leaking into production semantics | `AGENT_BOARD_TEST_*` naming, unit test asserting defaults are unchanged when unset, and a behaviour-change test |
|
|
214
|
-
| F1 changes runner startup order (D2 b/c) and breaks the progress beat | Only considered if D2 = b/c; the coordinator's `run_progress` requires a materialized status, so the bootstrap order must be re-verified by the integration suite before landing |
|
|
215
|
-
| Windows: signals cannot be caught at all (Node hard-kills on `process.kill`), so F1's mechanism cannot restore stop-safety there | D2 (d) is the only complete fix; with D2 = a/b/c the Windows behaviour is unchanged and recorded as a Non-goal |
|
|
216
|
-
| The boot-window knob is a test hook inside a production file | Same guard as F4: `AGENT_BOARD_TEST_*` naming + inert-when-unset unit test + behaviour test (see F1) |
|
|
217
|
-
| "Reconcile converges the stuck row" turns out to be false | A10 verifies it explicitly and is a blocking gate for U1; a negative result extends F1 rather than shipping a silent gap |
|
|
218
|
-
|
|
219
|
-
## 7. Open questions (explicitly not scope)
|
|
220
|
-
|
|
221
|
-
- Do `runner/pty-runner.mjs` (handlers at `:530`/`:754`) and `runner/state-coordinator.mjs`
|
|
222
|
-
(`:199`) have the same boot window — an observable state published before their handlers
|
|
223
|
-
exist? Not audited this round; F1 covers `job-runner` only. If the audit finds the same
|
|
224
|
-
pattern, that is a follow-up issue, not scope growth here.
|
|
225
|
-
- Should the stop path migrate to a durable stop-intent marker for all platforms long-term
|
|
226
|
-
(supersedes D2 (d))? Parked until F1 lands and the residual failure modes are known.
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# Spec:publish.yml 手动恢复后门的 tag 来源修复(issue #154)
|
|
2
|
-
|
|
3
|
-
日期:2026-09-30 · 状态:**approved(用户确认方案 A,范围 A1–A4 + U1 全量)· 勘误(同日双审后):A1 拆分为 A1a/A1b、A2 重写为探针 tag 法、A3 断言加严为计数 ≥2,改动点在各条目内注明**
|
|
4
|
-
基线:`main @ 444199d`
|
|
5
|
-
调研:issue #154 评论「调研:根因三源交叉验证」/ `~/.claude/github-issue-driven/zhuxixi/pi-agent-board/issue-154/research/root-cause.md`
|
|
6
|
-
|
|
7
|
-
## 1. 问题陈述
|
|
8
|
-
|
|
9
|
-
`docs/RELEASE.md:117-118` 记载的恢复路径——「Actions → **Publish to npm** → **Run workflow** → enter the tag」——按字面操作 100% 失败:从 `main` 触发手动恢复时,workflow 在第一个自检步骤退出 1,typecheck / 测试 / 发布全部未执行。
|
|
10
|
-
|
|
11
|
-
后果是「文档承诺的自救路径是死的」:必须知道文档外的 `--ref <tag>` 绕法才能恢复一次发布,而失败信息(`tag: main`)看起来像发布内容出错,会把操作者引向错误方向。
|
|
12
|
-
|
|
13
|
-
## 2. 根因(systematic-debugging 结论)
|
|
14
|
-
|
|
15
|
-
### 2.1 一句话根因
|
|
16
|
-
|
|
17
|
-
`.github/workflows/publish.yml:57` 把 **ref 名**当成 **本次要发布的 tag** 使用:
|
|
18
|
-
|
|
19
|
-
```yaml
|
|
20
|
-
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`GITHUB_REF_NAME` 的含义随触发方式变化,而该步骤按 release 事件的形状写死:
|
|
24
|
-
|
|
25
|
-
| 事件 | ref | `GITHUB_REF_NAME` | `${GITHUB_REF_NAME#v}` | 与 `package.json` 版本比对 |
|
|
26
|
-
| --- | --- | --- | --- | --- |
|
|
27
|
-
| `release: published` | `refs/tags/v0.9.0` | `v0.9.0` | `0.9.0` | 通过 |
|
|
28
|
-
| `workflow_dispatch`(从 main 触发) | `refs/heads/main` | `main` | `main` | **恒失败** |
|
|
29
|
-
|
|
30
|
-
### 2.2 同文件内的正确范例(决定修复形态)
|
|
31
|
-
|
|
32
|
-
checkout 步骤(`:32`)用事件感知表达式同时覆盖两种触发——手动触发时真正要发布的 tag 在 `inputs.tag` 里:
|
|
33
|
-
|
|
34
|
-
```yaml
|
|
35
|
-
ref: ${{ github.event.release.tag_name || inputs.tag }}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
即:同一概念("当前发布的 tag")在本文件有两处实现,checkout 处对、自检处错。修复应让二者同源,而不是引入第三种解析机制。
|
|
39
|
-
|
|
40
|
-
### 2.3 溯源:为什么潜伏了 5 天
|
|
41
|
-
|
|
42
|
-
- `git log -S 'GITHUB_REF_NAME'` 只命中 `d982724`(#144,2026-09-25)——该自检步骤自引入起**从未被改动**,不是回归而是「引入即存在」。
|
|
43
|
-
- #144 的 commit body 明确把手动恢复列为设计目标("a `workflow_dispatch` input re-runs a tag whose publish failed"),但自检只按 release 事件形状写。
|
|
44
|
-
- #144 的 PR body 把 npm 侧 trusted-publisher 配置列为 "Required follow-up (web action, not code)"。该配置滞后 → 09-25 发 0.9.0 时 release 事件发布 404 失败 → 0.9.0 卡在 npm 之外 4 天 → 09-30 配好配置后必须用恢复后门补发 → **首次使用后门,bug 当场暴露**。
|
|
45
|
-
- #144 的 comments/reviews 均为 0 条,无 CR 讨论可复用。
|
|
46
|
-
|
|
47
|
-
### 2.4 复现性(确定性,不依赖时序)
|
|
48
|
-
|
|
49
|
-
9-30 两次真实 run 构成单变量对照,唯一变量是 dispatch 的 ref:
|
|
50
|
-
|
|
51
|
-
| 实验 | 触发命令 | 自检步骤结果 |
|
|
52
|
-
| --- | --- | --- |
|
|
53
|
-
| 失败腿 | `gh workflow run publish.yml -f tag=v0.9.0 --ref main` | `package.json: 0.9.0, tag: main` → exit 1([run 36726074913](https://github.com/zhuxixi/pi-agent-board/actions/runs/36726074913)) |
|
|
54
|
-
| 通过腿 | `gh workflow run publish.yml -f tag=v0.9.0 --ref v0.9.0` | 自检通过,一路 publish 成功([run 36726192171](https://github.com/zhuxixi/pi-agent-board/actions/runs/36726192171)) |
|
|
55
|
-
|
|
56
|
-
### 2.5 影响面
|
|
57
|
-
|
|
58
|
-
- 只影响手动恢复路径;release 事件路径不受影响,故潜伏至今。
|
|
59
|
-
- 失败 fail-closed 且发生在最早一步:不会误发布、不污染注册表,代价是一次约 20 秒的无效 run。
|
|
60
|
-
- 人因代价是主要危害:文档写的操作失败、文档外的操作可行。
|
|
61
|
-
|
|
62
|
-
### 2.6 测试覆盖现状(决定可测性边界)
|
|
63
|
-
|
|
64
|
-
- `test/release-flow.test.mjs` 的 13 个测试全部针对 `scripts/release.mjs`。
|
|
65
|
-
- 全仓 grep `publish.yml|GITHUB_REF_NAME`:`test/` 下零命中——**本仓目前没有任何测试触碰 workflow YAML**。
|
|
66
|
-
|
|
67
|
-
## 3. 方案决策
|
|
68
|
-
|
|
69
|
-
| 候选 | 做法 | 评价 | 结论 |
|
|
70
|
-
| --- | --- | --- | --- |
|
|
71
|
-
| **A(采用)** | 自检步骤经 `env:` 接收 `github.event.release.tag_name \|\| inputs.tag`,与 checkout 同源 | 3 行改动;与既有正确写法一致;两种事件都覆盖;不引入新机制 | **采用** |
|
|
72
|
-
| B | 改为 `git describe --tags --exact-match`(对已 checkout 的提交取 tag) | 也能对,但引入新失败面(轻量/附注 tag、一提交多 tag),且与 checkout 已解析的 ref 重复表达同一事实 | 否 |
|
|
73
|
-
| C | 保留 `GITHUB_REF_NAME`,改为要求操作者必须用 `--ref <tag>` 触发 | 等于把 bug 固化进文档(文档已写 "enter the tag"),恢复路径继续反直觉 | 否 |
|
|
74
|
-
| D | 自检改为只校验 `inputs.tag` 存在性、不比对版本 | 削弱护栏(版本错配护栏是 #144 的明确设计目标之一) | 否 |
|
|
75
|
-
|
|
76
|
-
**改动内容**(`.github/workflows/publish.yml:54-59`):
|
|
77
|
-
|
|
78
|
-
```yaml
|
|
79
|
-
- name: Check the tag matches package.json
|
|
80
|
-
env:
|
|
81
|
-
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
|
82
|
-
run: |
|
|
83
|
-
PKG_VERSION=$(node -p "require('./package.json').version")
|
|
84
|
-
TAG_VERSION="${RELEASE_TAG#v}"
|
|
85
|
-
echo "package.json: ${PKG_VERSION}, tag: ${TAG_VERSION}"
|
|
86
|
-
[ "${PKG_VERSION}" = "${TAG_VERSION}" ]
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
用 `env:` 中转而非把 `${{ }}` 直接写进 `run` 脚本,遵循 GitHub Actions 惯例(避免表达式拼接进 shell)。
|
|
90
|
-
|
|
91
|
-
## 4. 契约与边界
|
|
92
|
-
|
|
93
|
-
- **不变量**:`publish.yml` 中任何需要「本次要发布的 tag」的位置,都必须从 `github.event.release.tag_name || inputs.tag` 派生,**不得**从 ref 名派生。静态测试钉死 `GITHUB_REF_NAME`(见 A3);`GITHUB_REF` 在语义上同属错误的派生源,但它存在合法用途(如日志),故不做文本级封禁。
|
|
94
|
-
- **数据流**:`release.tag_name`(release 事件)或 `inputs.tag`(手动触发)→ `env.RELEASE_TAG` → 去 `v` 前缀 → 与 `package.json.version` 比对。
|
|
95
|
-
- **降级**:若未来新增第三种触发方式(如 `push: tags`),该表达式需同步扩展;checkout 与自检必须一起改,二者共用同一表达式是这条降级的唯一保障。
|
|
96
|
-
- **非目标**:不改 release 事件路径行为;不改 checkout 表达式;不改 `scripts/release.mjs`;不增加「workflow 是否已重跑成功」类护栏;不引入 workflow 级 lint 基础设施。
|
|
97
|
-
|
|
98
|
-
## 5. 验收矩阵
|
|
99
|
-
|
|
100
|
-
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
101
|
-
| --- | --- | --- | --- | --- |
|
|
102
|
-
| A1a | 手动触发恢复不再红(合并前验证修复本身) | 自动化验证(automated E2E,真实 dispatch) | `gh workflow run publish.yml -f tag=v0.9.0 --ref issue-154-publish-workflow-dispatch-tag-check`——**ref 必须指向修复分支**:`--ref` 决定运行哪份 workflow 定义,合并前从 main 触发跑的是未修复版,自检必红,会把正确的修复误判为失败。随后 `gh run view <id> --log` 取自检步骤输出 | 自检步骤打出 `package.json: 0.9.0, tag: 0.9.0` 并通过;命中「already published — skipping」分支;整 run `success`。选 v0.9.0 因为它已在 npm 上,**零重复发布风险**。注意:本条只验证到 skip 分支,恢复路径的 `npm publish` 环节本次不执行(与 release 路径共用、未改动) |
|
|
103
|
-
| A1b | 文档承诺的路径按字面成立(**合并后**补验) | 自动化验证(automated E2E,真实 dispatch,合并后执行) | `docs/RELEASE.md:117-118` 的字面操作:Actions → Publish to npm → Run workflow → 填 `v0.9.0`(等价命令 `gh workflow run publish.yml -f tag=v0.9.0 --ref main`) | 同 A1a 的日志与结论。这一步验证的不是修复本身,而是**文档承诺的路径从用户视角成立**(ref=main、只填 tag) |
|
|
104
|
-
| A2 | 版本错配护栏仍然有效(负向) | 自动化验证(automated E2E,负向) | **勘误**:原设计 `-f tag=v0.8.0 --ref main` 不成立——checkout 跟随 `inputs.tag`(`publish.yml:32`),工作区 `package.json` 也随之变成 0.8.0,比对恒等、无法触发失败;更糟的是若该版本不在 npm 上,run 会一路走到 `npm publish` **真实发布旧版本**。错配必须人为制造:在分支 HEAD 打探针 tag `issue-154-mismatch-probe`(**不带 `v` 前缀**,避开 `release.mjs:207` / `release_helper.mjs:182` 的 `v*` 基线通配)并 push(需用户许可),随后 `gh workflow run publish.yml -f tag=issue-154-mismatch-probe --ref issue-154-publish-workflow-dispatch-tag-check`;验后删除本地与远端 tag,`git ls-remote --tags origin` 确认远端无残留 | 自检步骤 exit 1,日志显示 `package.json: 0.9.0, tag: issue-154-mismatch-probe`,run `failure`(证明护栏没被改成「永远通过」) |
|
|
105
|
-
| A3 | `GITHUB_REF_NAME` 不再出现在 publish.yml,且 tag 来源表达式在 checkout 与自检两处同源 | 自动化验证(static) | 新增 `test/publish-workflow.test.mjs`:读 `.github/workflows/publish.yml` 文本(**剥掉整行注释**,避免文档性注释误伤),断言不含 `GITHUB_REF_NAME`,且 `github.event.release.tag_name \|\| inputs.tag`(`\|\|` 两侧空白归一)出现 **≥2 次**(checkout + 自检;删掉自检的 `env:` 块必须红) | `npm test` 通过;修复前两条断言都红(`GITHUB_REF_NAME` 存在、表达式只出现 1 次),先写红再修 |
|
|
106
|
-
| A4 | 既有代码不被破坏 | 自动化验证(unit + build + static) | `npm run verify`(typecheck + perf gate + 全量测试 + 覆盖率阈值 + pack dry-run) | 全绿,覆盖率不低于阈值(lines 85 / funcs 80 / branches 70) |
|
|
107
|
-
| U1 | release 事件自动路径回归 | 用户实测(下个真实发版自然覆盖) | 发布 0.10.0 时走 `prepare → open-pr → merge → finish`,观察 publish.yml 由 release 事件触发的那次 run | 自检步骤通过(`tag: 0.10.0`),publish 步骤成功。本次修复不改变该路径的输入来源,属回归观察项而非新增验证 |
|
|
108
|
-
|
|
109
|
-
### 5.1 可测性拆分设计
|
|
110
|
-
|
|
111
|
-
本改动是 YAML 流程编排,逻辑量近零,可测性设计主要落在「把不变量钉死在可断言的形式上」:
|
|
112
|
-
|
|
113
|
-
- 修复逻辑本身(表达式替换)无法在本地执行,**其唯一有效验证是真实 dispatch(A1/A2)**——这是本 issue 的核心验收,不设更廉价的替代品,也不因此降级为「用户实测」。
|
|
114
|
-
- 唯一可静态化的产物是**不变量 A3**:纯文本断言,无副作用、无外部依赖。做成最小纯函数 `readWorkflowSource()`(读文件)+ 两条字符串断言;副作用(读文件)隔离在单个 helper 内,断言部分保持纯函数式,符合本仓「纯函数 + 薄 I/O 壳」的既有约定(参照 `scripts/release_helper.mjs` 的拆分方式)。
|
|
115
|
-
- 该静态测试断言**否定式不变量**(剥整行注释后不含 `GITHUB_REF_NAME`)+ **正向表达式计数 ≥2**(钉住「checkout 与自检同源」——删掉自检的 `env:` 块必须红),不断言 YAML 结构或步骤名。剥注释与归一空白是为了让文档性注释、无害格式调整不触发假红;而计数断言的有意严苛(把表达式上提成 job 级 `env:` 会红)正是守卫测试的期望行为——改架构就必须重审本不变量。
|
|
116
|
-
|
|
117
|
-
### 5.2 测试边界
|
|
118
|
-
|
|
119
|
-
| 边界 | 覆盖 | 不覆盖(明确) |
|
|
120
|
-
| --- | --- | --- |
|
|
121
|
-
| workflow YAML 文本不变量 | A3(static,新增) | YAML 语法正确性(由 GitHub 解析器在 run 时校验,A1 覆盖) |
|
|
122
|
-
| workflow 运行时行为 | A1a、A2(真实 dispatch);A1b(合并后) | release 事件路径(U1,下个真实发版覆盖) |
|
|
123
|
-
| 发布脚本逻辑 | A4(既有 13 个 release-flow 测试) | 本次不改 `scripts/release.mjs`,无新增 |
|
|
124
|
-
|
|
125
|
-
## 6. 风险
|
|
126
|
-
|
|
127
|
-
- **A1a/A1b 各消耗一次真实 run**:约 1 分钟、无副作用、不产生发布(版本已在注册表 → skip 分支)。可接受。
|
|
128
|
-
- **A2 的错配必须人为制造**:checkout 跟随 `inputs.tag`,dispatch 路径下 tag 与工作区 `package.json` 恒同源,用既有 tag 造不出错配;更危险的是若误选未发布过的版本号,run 会一路走到 `npm publish` 真实发布。因此探针 tag 指向分支 HEAD(其 `package.json` 与 tag 名必然不一致),且**不带 `v` 前缀**——`release.mjs` / `release_helper.mjs` 以 `v*` 通配 tag 定 changelog 基线,`v*` 探针泄漏会污染下一次发版。
|
|
129
|
-
- **静态测试(A3)的假红风险**:剥整行注释 + 空白归一已规避文档性假红;保留的假红面(计数 ≥2 对上提重构敏感)是守卫测试的期望行为,见 §5.1。
|