@zhuxixi/pi-agent-board 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +6 -3
  3. package/VERIFY.md +2 -1
  4. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +3 -1
  5. package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +917 -0
  6. package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +603 -0
  7. package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +252 -0
  8. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +115 -0
  9. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +469 -0
  10. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +92 -0
  11. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +771 -0
  12. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +299 -0
  13. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +609 -0
  14. package/docs/superpowers/plans/2026-09-14-terminal-model.md +145 -0
  15. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +224 -0
  16. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +341 -0
  17. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +875 -0
  18. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +116 -0
  19. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +517 -0
  20. package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +130 -0
  21. package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +298 -0
  22. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +114 -0
  23. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +120 -0
  24. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +146 -0
  25. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +116 -0
  26. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +84 -0
  27. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +92 -0
  28. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +123 -0
  29. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +204 -0
  30. package/package.json +3 -2
  31. package/runner/job-runner-legacy.mjs +68 -0
  32. package/runner/job-runner.mjs +371 -67
  33. package/runner/pty-runner-legacy.mjs +50 -0
  34. package/runner/pty-runner.mjs +685 -58
  35. package/runner/state-coordinator.mjs +429 -0
  36. package/runner/state-runner.mjs +90 -15
  37. package/scripts/run-perf-gate.mjs +40 -0
  38. package/src/commands/agent-board.ts +8 -8
  39. package/src/commands/attach-flow.ts +5 -5
  40. package/src/commands/bg.ts +2 -1
  41. package/src/core/control-protocol.mjs +482 -0
  42. package/src/core/coordinator-client.mjs +313 -0
  43. package/src/core/coordinator-journal.mjs +282 -0
  44. package/src/core/coordinator-protocol.mjs +12 -0
  45. package/src/core/editor-state-reporter.mjs +11 -1
  46. package/src/core/foreground-preview-cache.mjs +117 -0
  47. package/src/core/host-protocol.mjs +24 -0
  48. package/src/core/launch.mjs +15 -0
  49. package/src/core/locks.mjs +68 -14
  50. package/src/core/paths.mjs +48 -0
  51. package/src/core/pid.mjs +32 -1
  52. package/src/core/pty-attach-jiggle-controller.mjs +83 -6
  53. package/src/core/pty-attach-reconnect.mjs +13 -6
  54. package/src/core/pty-attach-render.mjs +50 -0
  55. package/src/core/state-commands.mjs +699 -0
  56. package/src/core/status-consistency.mjs +98 -0
  57. package/src/core/store.mjs +59 -13
  58. package/src/core/terminal-attach-client.mjs +803 -0
  59. package/src/core/terminal-attach-protocol.mjs +252 -0
  60. package/src/core/terminal-model.mjs +222 -0
  61. package/src/core/terminal-snapshot.mjs +440 -0
  62. package/src/core/types.mjs +2 -0
  63. package/src/index.ts +12 -4
  64. package/src/runtime/service.mjs +694 -121
  65. package/src/ui/dashboard.ts +67 -92
  66. package/src/ui/pty-attach.ts +298 -72
  67. package/src/core/pty-input.mjs +0 -47
@@ -0,0 +1,252 @@
1
+ # Single-Writer Completion (PR #2, issue #91 Phase 2b) Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** 迁移 PR #1 白名单中的全部剩余 writeState/writeStatus 直写点到 View State Coordinator,使架构边界白名单清零到仅剩「设计内豁免」,A7 完全闭合;同时关闭 PR #1 遗留的 pty-runner `markRowFailed` 无 manual-fence 残留风险。
6
+
7
+ **Architecture:** 扩展 `src/core/state-commands.mjs` 的命令种类(lifecycle 命令显式建类 + 元数据/镜像合并为 `patch_fields` 通用命令),coordinator 支持「非 journal 的瞬时命令」(run_progress 热路径),各 runner/service/dashboard 的剩余站点逐个迁移。读侧 revision 一致性 enforcement 明确留给 PR #3(需要所有写者先打戳)。
8
+
9
+ **Tech Stack:** Node.js ESM、node:net JSONL、现有 coordinator/journal/client(PR #1 已建)。
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md`(D3 + 验收 A7 + 根治条件 1/2/5 写侧);本 plan 不覆盖读侧 enforcement(PR #3)。
12
+
13
+ ## 设计决策(本 plan 锁定)
14
+
15
+ 1. **新命令种类**(加入 `STATE_COMMAND_KINDS`,决策层每个有显式分支):
16
+ - lifecycle(改 semanticState/processState/currentRunId):`mark_queued`、`run_started`、`run_progress`、`reconcile_finalize`、`host_run_failed`、`archive_view`、`adopt_session`、`sync_foreground`、`plan_ready`、`followup_started`
17
+ - 元数据/镜像合并:`patch_fields`,带 per-source 字段白名单 + 通用守卫
18
+ 2. **`run_progress` 是瞬时命令,不进 journal**:进度写是周期性快照(250ms 节流),被下一拍自愈;journal 化会让 journal 无界膨胀(4 条/秒/run),违背 GC 设计。模糊失败(timeout/reset)→ 跳过 + debug 级 diagnostic,等下一拍。落地仍打 materializedRevision(单调性由 boot counter 的 views 扫描保证)。
19
+ 3. **`host_run_failed` 必须带 manual fence**(通用守卫已覆盖非 user 来源)——这关闭 PR #1 遗留的 pty-runner markRowFailed 残留风险(手动完成后宿主迟到崩溃不再能把行翻成 failed)。
20
+ 4. **永久豁免**(边界测试白名单的终态):(a) 各 runner 的 `coordinator_disabled` 遗留分支(显式 debug 逃生门,文档化);(b) `store.mjs` createView bootstrap(不可能竞争:新 viewId 是随机生成的,不存在其他知情的写者)。豁免理由写进白名单条目;A7 的「清零」含义 = 无未治理写点,不是字面零条目。
21
+ 5. **dashboard.ts 热重载兼容回退直接删除**(旧 service 对象不跨重启存活,窗口是瞬时的)。
22
+ 6. **M2 summary 分歧**:coordinator 的 mark_completed 保持「保留现有 summary」(PR #1 裁决),本 PR 不改;如用户反馈再议。
23
+
24
+ ## Global Constraints
25
+
26
+ - 所有 runner 进程 plain ESM `.mjs`,不依赖 jiti。
27
+ - Commit message 英文 conventional commits;`git add <file>` 显式 stage,禁止 `-A`。
28
+ - 测试 `node --test`;隔离必须同时设 `AGENT_BOARD_ROOT` + `PI_CODING_AGENT_DIR`;coordinator 一律用 tracked fixture(test-support/ensure-coordinator-helper.mjs,带 readiness wait + finally kill)。
29
+ - 可测性硬约束:决策层保持纯函数;新命令的守卫逻辑在 state-commands.mjs 内,不碰 I/O。
30
+ - 模糊结果(timeout/connection_reset)不回退直写;只有 `coordinator_disabled` 走 legacy 分支。
31
+ - 工作区:`$WT = /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-91-single-writer-completion`,全部用绝对路径 + `git -C $WT`。
32
+
33
+ ## 验收映射
34
+
35
+ | Task | 验收 ID | 说明 |
36
+ |---|---|---|
37
+ | Task 1 | A7 基础 | 决策层新命令种类 + 守卫(纯函数) |
38
+ | Task 2 | A7/A7b | coordinator 支持瞬时命令 + patch_fields + host_run_failed |
39
+ | Task 3 | A7 | job-runner 全量迁移(含热路径 run_progress) |
40
+ | Task 4 | A7 | state-runner 镜像迁移 |
41
+ | Task 5 | A7 + PR#1 残留风险 #2 | pty-runner markRowFailed → host_run_failed(带 fence) |
42
+ | Task 6 | A7 | service.mjs 全量迁移 |
43
+ | Task 7 | A7 | dashboard 回退删除 |
44
+ | Task 8 | A7 闭合 | 边界测试白名单 → 设计内豁免;justification 更新 |
45
+ | Task 9 | 对账 | 全量回归 + 验收对账 |
46
+
47
+ A8 已在 PR #1 闭合(本 PR 不破坏其测试);根治条件 5 的读侧在 PR #3。
48
+
49
+ ---
50
+
51
+ ### Task 1: 决策层扩展——新命令种类与守卫(纯函数)
52
+
53
+ **Files:**
54
+ - Modify: `src/core/state-commands.mjs`
55
+ - Test: `test/state-commands.test.mjs`
56
+
57
+ **Interfaces:**
58
+ - Consumes: 现有 `validateCommand`/`decideStateTransition` 结构、守卫顺序(unknown_view → revision_conflict → stale_run → manual_fence → kind 分支)。
59
+ - Produces(后续 task 依赖的精确契约):
60
+ - `STATE_COMMAND_KINDS` 扩展为 13 种(现有 3 种 + 新增 10 种)
61
+ - `TRANSIENT_KINDS` = `["run_progress"]`(coordinator 据此跳过 journal)
62
+ - `PATCHABLE_FIELDS` per source 白名单:
63
+ - `job-runner`/`state-runner`: `["review","evidenceSummary"]`(镜像)+ status 侧 `evidenceSummary`
64
+ - `service`: `["lastVisitedAt"]`(markVisited 经此)
65
+ - 每个新 kind 的守卫(决策层分支,全部纯函数):
66
+
67
+ | kind | 额外守卫(通用守卫之外) | mutate 语义 |
68
+ |---|---|---|
69
+ | `mark_queued` | 无 | state: {currentRunId: payload.runId, semanticState:"queued", processState:"alive", summary:"Queued", needsInput:false, hasError:false, question:null, pendingQuestions:[], error:null, autoState:null} |
70
+ | `run_started` | runId 必须等于 payload.runId | status 新建字段补丁(createRunStatus 形状由调用方算好放在 payload.status);state: {processState:"alive", semanticState:"working", currentRunId} |
71
+ | `run_progress` | `state.currentRunId === command.runId && state.processState === "alive"`,否则 reject "stale_run"(活跃性语义) | payload.statusPatch 稀疏合并 status;state 侧由 coordinator 用 projectViewState(statusClone) 重算(委托,不复制规则) |
72
+ | `reconcile_finalize` | `state.processState === "alive"`(否则 no_change) | payload: {semanticState: "failed"\|"idle", reason};state: {semanticState, processState:"exited", error?};currentRunId 对应 status 存在则同步 finalize 字段 |
73
+ | `host_run_failed` | 无(通用 manual_fence 即 PR#1 残留风险 #2 的关闭点) | state: {semanticState:"failed", processState:"exited", error: payload.error ?? null};status 同理(currentRunId 匹配时) |
74
+ | `archive_view` | busy 时 allow(archive 的 busy 分支原本就写 stopped) | state: {semanticState:"stopped", processState:"exited", needsInput:false, hasError:false, question:null, pendingQuestions:[], error:null, autoState:null, summary:"Stopped"} |
75
+ | `adopt_session` | `processState !== "alive"`(否则 reject "busy") | state: {semanticState:"idle", processState:"exited", summary:"Backgrounded session"} |
76
+ | `sync_foreground` | 无 | payload.projection(service 侧 projectViewState 已算好,currentRunId 强制 null 由本分支保证)稀疏合并 state |
77
+ | `plan_ready` | `processState === "alive"`(否则 no_change) | state: {needsInput:true, question: payload.question ?? null};status 侧 recordPlanReady 对应字段 |
78
+ | `followup_started` | 无 | payload.statusPatch → status;state 侧 projectViewState 重算 |
79
+ | `patch_fields` | 字段白名单(`PATCHABLE_FIELDS[command.source]`,越界字段 reject "field_not_allowed");payload.runId 存在时 stale_run 通用守卫生效 | payload.state/payload.status 稀疏合并(仅限白名单字段) |
80
+
81
+ - [ ] **Step 1: 写失败测试**——每种新 kind 至少 1 个 happy-path + 1 个守卫测试;`patch_fields` 越界字段拒绝测试;`run_progress` 的 stale/alive 守卫测试;`host_run_failed` 的 manual_fence 测试(复用现有 manualCompletedState fixture 模式)。
82
+ - [ ] **Step 2: 运行确认失败** → **Step 3: 实现** → **Step 4: PASS** → **Step 5: Commit**
83
+
84
+ ```bash
85
+ git -C $WT add src/core/state-commands.mjs test/state-commands.test.mjs
86
+ git -C $WT commit -m "feat(core): extend state commands with lifecycle kinds and patch_fields (issue #91)"
87
+ ```
88
+
89
+ ---
90
+
91
+ ### Task 2: Coordinator 支持瞬时命令与新 kinds
92
+
93
+ **Files:**
94
+ - Modify: `runner/state-coordinator.mjs`
95
+ - Test: `test/state-coordinator.integration.test.mjs`
96
+
97
+ **Interfaces:**
98
+ - Consumes: Task 1 的 `TRANSIENT_KINDS` 与新 kinds。
99
+ - Produces:
100
+ - 瞬时命令(`TRANSIENT_KINDS`)路径:validate → dedupe 不需要(无 commandId 幂等语义——run_progress 无需 commandId 去重,client 可省略 commandId)→ decide → 直接物化(仍打 materializedRevision、仍 under view lock)→ 返回结果。**不写 journal、不进 processed 集合**。
101
+ - `run_progress` 的 revision bump 照常(单调性由全局 counter 保证;boot 时 views 扫描已覆盖)。
102
+ - 其余新 kinds 走正常 journaled 路径。
103
+
104
+ - [ ] **Step 1: 失败测试**:run_progress 瞬时命令 applied 且 journal 行数不增长;host_run_failed applied;mark_queued applied;并发 run_progress 不交错(同一 view 两客户端连发)。
105
+ - [ ] **Step 2–5: 失败 → 实现 → PASS → Commit**
106
+
107
+ ```bash
108
+ git -C $WT add runner/state-coordinator.mjs test/state-coordinator.integration.test.mjs
109
+ git -C $WT commit -m "feat(runner): transient run_progress and new lifecycle kinds in coordinator (issue #91)"
110
+ ```
111
+
112
+ ---
113
+
114
+ ### Task 3: job-runner 全量迁移(含热路径)
115
+
116
+ **Files:**
117
+ - Modify: `runner/job-runner.mjs`
118
+ - Test: `test/runner.integration.test.mjs`
119
+
120
+ **Interfaces:**
121
+ - Consumes: Task 1-2 的命令;`sendStateCommand`。
122
+ - Produces:
123
+ - boot bootstrap(L65/69、L95)→ `run_started` 命令
124
+ - `persist()` 热路径(L103-112 及 125/201/217 调用点)→ `run_progress` 瞬时命令(fire-and-forget:不等结果、失败 debug 级忽略——下一拍自愈;**这是本 PR 唯一允许 fire-and-forget 的命令**,理由:周期性快照 + 下一拍覆盖)
125
+ - `refreshEvidenceMirrors`(L152/157)→ `patch_fields`(review/evidenceSummary)
126
+ - plan-ready(L382)→ `plan_ready`;follow-up bootstrap(L408-409)→ `followup_started`
127
+ - post-exit summary persist(maybeModelSummary 尾部的 persistUnlessManual(true))→ `patch_fields`(summary/latestAssistantPreview;manual fence 由通用守卫保证)
128
+ - `coordinator_disabled` 分支保留为设计内豁免(不迁移,但保持现状可用)
129
+ - **迁移完成后 runner/job-runner.mjs 不再 import writeState/writeStatus**(coordinator_disabled 分支改为调 legacy helper——见下)
130
+
131
+ **关键设计点(防回归):** coordinator_disabled 分支需要直写能力。方案:把 PR #1 之前的直写逻辑收进 `runner/job-runner-legacy.mjs`(新文件,只有 disabled 分支 import 它),使 job-runner.mjs 本身不再 import write 函数——边界测试白名单条目从 job-runner.mjs 移到 job-runner-legacy.mjs(justification:disabled 逃生门)。
132
+
133
+ - [ ] **Step 1: 失败测试**:run_progress 经 coordinator 的集成测试(runner 活跃期间 state.json 由 coordinator 物化、journal 不增长);#46 回归与既有 runner.integration 用例保持绿。
134
+ - [ ] **Step 2–5: 失败 → 实现 → PASS → Commit**
135
+
136
+ ```bash
137
+ git -C $WT add runner/job-runner.mjs runner/job-runner-legacy.mjs test/runner.integration.test.mjs
138
+ git -C $WT commit -m "refactor(runner): migrate job-runner writes to coordinator incl. transient run_progress (issue #91)"
139
+ ```
140
+
141
+ ---
142
+
143
+ ### Task 4: state-runner 镜像迁移
144
+
145
+ **Files:**
146
+ - Modify: `runner/state-runner.mjs`(镜像写 → `patch_fields`);disabled 分支同样收进 legacy helper 或直接保留(该文件小,保留 disabled 分支并更新白名单 justification 即可,不必拆文件)
147
+ - Test: `test/state-coordinator.integration.test.mjs`(state-runner 真实进程用例更新断言)
148
+
149
+ - [ ] **Step 1–5: TDD 循环 → Commit**
150
+
151
+ ```bash
152
+ git -C $WT add runner/state-runner.mjs test/state-coordinator.integration.test.mjs
153
+ git -C $WT commit -m "refactor(runner): migrate state-runner evidence mirrors to patch_fields (issue #91)"
154
+ ```
155
+
156
+ ---
157
+
158
+ ### Task 5: pty-runner markRowFailed → host_run_failed(关闭残留风险 #2)
159
+
160
+ **Files:**
161
+ - Modify: `runner/pty-runner.mjs`(markRowFailed,~L1021-1048)
162
+ - Test: `test/pty-runner.integration.test.mjs`
163
+
164
+ **Interfaces:**
165
+ - markRowFailed 改为发送 `host_run_failed` 命令(source "pty-runner"——需加入 COMMAND_SOURCES);payload `{ error, exitCode? }`。
166
+ - 手动完成的行不再能被迟到崩溃翻成 failed(通用 manual_fence 守卫);这是本 task 的核心回归测试。
167
+ - pty-runner 是 detached 进程,coordinator-client 是 plain ESM 可 import。
168
+ - 模糊结果处理:warn diagnostic + 继续(崩溃路径不能因此卡住退出);coordinator_disabled → 保留 legacy 直写(收进 legacy helper 或保留原位并更新白名单 justification)。
169
+
170
+ - [ ] **Step 1: 失败测试**(核心回归):手动 completed 的行 + 宿主迟到崩溃 → state.json 保持 completed(fence 拒绝 host_run_failed);无 fence 时正常 failed。
171
+ - [ ] **Step 2–5: TDD → Commit**
172
+
173
+ ```bash
174
+ git -C $WT add runner/pty-runner.mjs test/pty-runner.integration.test.mjs
175
+ git -C $WT commit -m "fix(runner): route host crash finalization through fenced host_run_failed command (issue #91)"
176
+ ```
177
+
178
+ ---
179
+
180
+ ### Task 6: service.mjs 全量迁移
181
+
182
+ **Files:**
183
+ - Modify: `src/runtime/service.mjs`
184
+ - Test: `test/service.test.mjs`
185
+
186
+ **迁移映射(站点 → 命令):**
187
+ - L423 markQueued → `mark_queued`
188
+ - L433 markVisited → `patch_fields`(lastVisitedAt;metadata,但统一走 coordinator 保持边界干净)
189
+ - L524 archiveView 的 state 部分 → `archive_view`(meta.archived 直写保留——meta.json 是文档化例外)
190
+ - L606 writeForegroundState → `sync_foreground`
191
+ - L1602/1629 adoptSession → `adopt_session`
192
+ - L1855/1864/1873 reconcile 终态 → `reconcile_finalize`
193
+ - `completeViewDirect`(coordinator_disabled 分支)保留为设计内豁免——收进 `src/runtime/service-legacy.mjs` 或原位保留并更新白名单 justification(择实现复杂度低者;service.mjs 很大,原位保留 + justification 更新更简单)
194
+ - 全部迁移后 service.mjs 的直接 writeState/writeStatus 调用点只剩 disabled 分支
195
+
196
+ - [ ] **Step 1–5: TDD → Commit**(现有 service 测试全部保持绿;reconcile 相关用例更新为 coordinator 断言)
197
+
198
+ ```bash
199
+ git -C $WT add src/runtime/service.mjs test/service.test.mjs
200
+ git -C $WT commit -m "refactor(service): migrate remaining state writes to coordinator commands (issue #91)"
201
+ ```
202
+
203
+ ---
204
+
205
+ ### Task 7: dashboard.ts 兼容回退删除
206
+
207
+ **Files:**
208
+ - Modify: `src/ui/dashboard.ts`(删除 ~L981-1020 的 stale-service-object 回退;markCompleted 调用统一走新 async 路径)
209
+ - Test: `test/service.test.mjs` / dashboard 相关测试保持绿
210
+
211
+ - [ ] **Step 1–5: TDD → Commit**
212
+
213
+ ```bash
214
+ git -C $WT add src/ui/dashboard.ts
215
+ git -C $WT commit -m "refactor(dashboard): drop pre-coordinator markCompleted compat fallback (issue #91)"
216
+ ```
217
+
218
+ ---
219
+
220
+ ### Task 8: 边界测试白名单 → 设计内豁免(A7 闭合)
221
+
222
+ **Files:**
223
+ - Modify: `test/architecture-writer-boundary.test.mjs`
224
+
225
+ **终态白名单(每项带永久 justification):**
226
+ - `runner/job-runner-legacy.mjs`(或 state-runner/pty-runner 的 disabled 原位分支)——「coordinator_disabled 逃生门,设计内豁免」
227
+ - `src/runtime/service.mjs`——仅当 disabled 分支原位保留时;若收进 service-legacy.mjs 则换成该文件
228
+ - `src/core/store.mjs`——「createView bootstrap 不可能竞争(新 viewId 随机生成,无其他写者知情),永久豁免」
229
+ - 其余条目全部删除;测试断言白名单外零 importer。
230
+ - 更新文件头注释:从「PR #2 迁移并删除条目」改为「设计内豁免清单」。
231
+ - 顺手修 PR #1 的 deferred minor:non-rotting 检查对 importer 条目改用 import-level 正则(mention-level 只留给 store.mjs)。
232
+
233
+ - [ ] **Step 1–5: TDD → Commit**
234
+
235
+ ```bash
236
+ git -C $WT add test/architecture-writer-boundary.test.mjs
237
+ git -C $WT commit -m "test(arch): shrink writer allowlist to designed exceptions only (issue #91)"
238
+ ```
239
+
240
+ ---
241
+
242
+ ### Task 9: 全量回归 + 验收对账
243
+
244
+ - [ ] `node --test test/*.test.mjs` 全绿 + `npm run typecheck` + 零泄漏进程检查
245
+ - [ ] 验收对账写入 PR 描述:A7 ✅(豁免清单版);A8 不回归;根治条件 1/2 写侧闭合;根治条件 5 读侧 → PR #3
246
+ - [ ] PR 描述诚实声明:读侧 revision 一致性未启用;coordinator 不可用时 mutation fail-closed(mark-done 显示原始 reason)
247
+
248
+ ## Self-Review 记录
249
+
250
+ - Spec 覆盖:D3 写侧全覆盖;读侧 enforcement 明确 PR #3(spec §8 分阶段允许)。
251
+ - 与 PR #1 的接口一致性:`TRANSIENT_KINDS`/`PATCHABLE_FIELDS`/新 kind 名在 Task 1 定义、Task 2-6 消费,名称在 Global Constraints 与各 task Interface 块一致。
252
+ - 占位符:无 TBD;Task 6 的「原位保留 vs 收进 legacy 文件」给了明确的取舍指引(实现复杂度低者)。
@@ -0,0 +1,115 @@
1
+ # Reader Consistency & Residual Convergence (PR #3, issue #91) Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** 关闭 spec 根治条件 5 的读侧(revision 一致性 enforcement)+ 收敛 PR #2 已知残留(coordinator 硬宕窗口的无 status 文件行)+ 合并后卫生波。完成后 **D3「状态所有权」弧线全部闭合**(写侧 PR #1/#2、读侧本 PR)。
6
+
7
+ **Architecture:** 三个小面:(1) 决策层让 `run_progress` 在「行存活 + runId 匹配 + status 文件缺失」时从 beat 的全量 patch 自举(部分反转 F2 守卫——F2 防的是稀疏 patch 物化 undefined,而 beat 的 patch 是全量的,且硬宕残留正是需要自愈的场景);(2) reconcile 作为唯一组合读者,读侧做 revision mismatch 检测——不一致则丢弃组合、踢 ensureCoordinator(boot replay 修复)、记 diagnostic;(3) 卫生波(.catch、双击窗口、JSDoc、warn 措辞、void+catch pass)。
8
+
9
+ **Tech Stack:** 既有 coordinator/journal/client/决策层(PR #1/#2 已建并稳定)。
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md`(根治条件 5;§5 降级条款)。
12
+
13
+ ## 设计决策(本 plan 锁定)
14
+
15
+ 1. **run_progress bootstrap 是 F2 的部分反转,有边界**:F2 的 `!currentStatus → stale_run` 改为——行 `processState === "alive" && currentRunId === command.runId` 时从 patch 自举(决策层校验 patch 携带 `processState`/`semanticState`/`runId` 一致性后才自举;稀疏或身份不符仍 stale_run)。coordinator 的 `STATUS_BOOTSTRAP_KINDS` 增加 `run_progress`。理由:beat 是 runner 内存中权威状态的全量快照;硬宕残留(mark_queued applied 但 run_started 丢失)下,coordinator 恢复后第一条 beat 即自愈。
16
+ 2. **读侧 mismatch 的语义**:reconcile 读 `s.currentRunId` 对应 status 时,若 `state.materializedRevision != null && status?.materializedRevision != null && 二者不等` → 视为不一致:跳过本行投影(不拼)、`appendDiagnostic(code: "state_status_revision_desync")`、异步 `ensureCoordinator(root)`(boot replay 从 journal 修复半物化对)、不计入 fixed。**legacy 行(任一侧无 revision)不检查**——与 spec 的 legacy 迁移条款一致。
17
+ 3. **mismatch 只可能来自 coordinator 崩溃窗口**(写侧已全部在 view lock 内配对打戳);活着的 coordinator 不会产生 mismatch。所以修复路径 = ensureCoordinator spawn → boot replay,无需新命令 kind。
18
+ 4. **双击窗口修复选「同步清理」**:`submitDispatch` 在发起异步 dispatch 前**同步**清 `this.launch`/input/mode(不等 .then),消除 coordinator 冷启动 round-trip 期间的重复提交窗口;.catch 兜底 fs 类异常。
19
+ 5. **decided-rejection warn 措辞**:job-runner/state-runner 中对 `manual_fence`/`stale_run`/`no_change` 等**确定性拒绝**的分支不再使用「outcome unknown … replay will recover」句式——改为 info 级 `*_skipped` 或措辞明确的 warn(确定性拒绝已 journal,无恢复语义)。
20
+
21
+ ## Global Constraints
22
+
23
+ - 所有 runner 进程 plain ESM `.mjs`。
24
+ - Commit message 英文 conventional commits;`git add <file>` 显式 stage,禁止 `-A`。
25
+ - 测试 `node --test`;隔离必须同时设 `AGENT_BOARD_ROOT` + `PI_CODING_AGENT_DIR`;coordinator 一律 tracked fixture(`test-support/ensure-coordinator-helper.mjs`,readiness wait + finally kill)。
26
+ - 决策层保持纯函数。
27
+ - 模糊结果(timeout/connection_reset)不回退直写;只有 `coordinator_disabled` 走 legacy 分支。
28
+ - **大 task 时间纪律**:分阶段提交工作增量,保持树常绿(吸取 PR #2 三次超时教训)。
29
+ - `$WT = /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-91-reader-consistency`;全部绝对路径 + `git -C $WT`。
30
+
31
+ ## 验收映射
32
+
33
+ | Task | 验收目标 | 说明 |
34
+ |---|---|---|
35
+ | Task 1 | 根治条件 5 残留收敛(决策层) | run_progress bootstrap 守卫 + 纯函数测试 |
36
+ | Task 2 | 残留收敛(端到端) | 硬宕场景:beat 自举 → finalize 收敛 |
37
+ | Task 3 | **根治条件 5 读侧闭合** | reconcile mismatch 检测 + 修复踢 + diagnostic |
38
+ | Task 4 | 卫生波 | .catch / 双击窗口 / JSDoc / warn 措辞 / void+catch |
39
+ | Task 5 | 对账 | 全量回归 + 根治条件 1/2/5 全闭合声明 |
40
+
41
+ ---
42
+
43
+ ### Task 1: 决策层——run_progress 缺失 status 时的自举分支
44
+
45
+ **Files:**
46
+ - Modify: `src/core/state-commands.mjs`
47
+ - Test: `test/state-commands.test.mjs`
48
+
49
+ **Interfaces:**
50
+ - Consumes: 现有 `run_progress` 分支(~L358:`if (!currentStatus) return reject("stale_run")`)、`applyStatusProjection`。
51
+ - Produces(Task 2 依赖):
52
+ - run_progress 分支新语义:
53
+ - `currentStatus == null` 且 `currentState.processState === "alive" && currentState.currentRunId === command.runId`:校验 `payload.statusPatch` 为自举合格(含 `processState`、`semanticState` 字段且 `statusPatch.runId === command.runId`)→ 以 patch 自身为基座走 applyStatusProjection(基座 = patch 本身,非 `{}`),mutate.status 为全量 patch;
54
+ - `currentStatus == null` 且行不存活或 runId 不匹配:维持 `stale_run` 拒绝(F2 原语义);
55
+ - `currentStatus != null`:原逻辑不变(liveness 守卫 + 投影)。
56
+ - 更新 F2 的既有测试(`run_progress` 缺 status → stale_run)为分场景:行不匹配仍拒;行匹配 → 自举 applied。
57
+ - [ ] **Step 1: 失败测试**——三个场景(自举 applied + 全量 status patch;行 exited → stale_run;patch 稀疏缺 processState → stale_run)
58
+ - [ ] **Step 2–5: 失败 → 实现 → PASS → Commit**(`feat(core): run_progress bootstraps a missing status from its full patch (issue #91)`)
59
+
60
+ ---
61
+
62
+ ### Task 2: Coordinator——bootstrap kinds 扩展 + 硬宕残留端到端
63
+
64
+ **Files:**
65
+ - Modify: `runner/state-coordinator.mjs`(`STATUS_BOOTSTRAP_KINDS` 增加 `"run_progress"`)
66
+ - Test: `test/state-coordinator.integration.test.mjs`
67
+
68
+ **Interfaces:**
69
+ - 端到端回归测试(残留关闭证明):`mark_queued` applied → **不发 run_started**(模拟硬宕窗口错过)→ 发一条 `run_progress` beat(全量 patch)→ 断言 status 文件自举创建(含 revision 戳)+ state 投影 applied → 再发 `run_finalized` → 断言 applied(此前会 stale_run 拒绝)。全程 journal 只有 mark_queued + run_finalized(beat 不落 journal)。
70
+ - 既有 F2 集成测试同步更新。
71
+ - [ ] **Step 1–5: TDD → Commit**(`feat(runner): beats bootstrap missing status, closing the hard-down window (issue #91)`)
72
+
73
+ ---
74
+
75
+ ### Task 3: 读侧 revision 一致性(reconcile enforcement)
76
+
77
+ **Files:**
78
+ - Create: `src/core/status-consistency.mjs`(纯函数 helper)
79
+ - Modify: `src/runtime/service.mjs`(reconcile 组合读处)
80
+ - Test: `test/status-consistency.test.mjs` + `test/service.test.mjs`
81
+
82
+ **Interfaces:**
83
+ - Produces:
84
+ - `statusRevisionDesynced(state, status)` → boolean:`state?.materializedRevision != null && status?.materializedRevision != null && state.materializedRevision !== status.materializedRevision`(legacy 任一侧缺失 → false,不检查)。
85
+ - reconcile(~L2022 读 status 处)集成:desynced → `appendDiagnostic(code: "state_status_revision_desync", level: "warn")` + `void ensureCoordinator(root).catch(() => {})`(异步踢修复;boot replay 修复半物化对)+ `continue`(跳过本行,不计 fixed,不拼状态)。
86
+ - `ensureCoordinator` 需从 `src/core/coordinator-client.mjs` 导出(检查是否已导出;PR #1 Task 5 建过,确认签名)。
87
+ - 测试:纯函数表驱动(desync/一致/legacy 侧缺失/双侧缺失);service 集成(手动构造半物化对——state 戳 N+1、status 戳 N——reconcile 跳过该行 + diagnostic 落盘 + 不计入 fixed;coordinator spawn 后可另行验证修复,不强求同测试内)。
88
+ - [ ] **Step 1–5: TDD → Commit**(`feat(core): reader-side revision consistency enforcement in reconcile (issue #91)`)
89
+
90
+ ---
91
+
92
+ ### Task 4: 卫生波
93
+
94
+ **Files:**
95
+ - Modify: `src/ui/dashboard.ts`、`src/runtime/service.mjs`、`runner/job-runner.mjs`、`runner/state-runner.mjs`(措辞)+ 相关调用点(void+catch pass)
96
+
97
+ **清单(每项独立可验证):**
98
+ 1. `submitDispatch`(dashboard.ts ~L844+):发起异步 dispatch 前**同步**清 `this.launch`/输入/mode;`.then` 链尾加 `.catch`(notice "Dispatch failed")。
99
+ 2. JSDoc:`launchForView` `@returns` → Promise 形状;`startHostUnderLease` `@returns` 修正。
100
+ 3. decided-rejection 措辞:job-runner 的 run_started/finalize 处 `manual_fence`/`stale_run` 分支改 info 级 skipped diagnostic(或明确措辞 warn,不再用 "outcome unknown … replay will recover");state-runner 同类分支对齐。
101
+ 4. void+catch pass:13 处 `markVisited?.()`、7 处 `service.reconcile()` 调用点加 `void …catch(() => {})`(仅吞 fs 类异常;调用点文件:dashboard.ts、attach-flow.ts、agent-board.ts、index.ts)。
102
+ - [ ] **验证**:受影响套件 + typecheck → Commit(`chore: post-merge hygiene wave — catch guards, dispatch double-submit window, honest diagnostics (issue #91)`)
103
+
104
+ ---
105
+
106
+ ### Task 5: 全量回归 + 验收对账
107
+
108
+ - [ ] `node --test test/*.test.mjs` 全绿 + `npm run typecheck` + 零测试进程泄漏(按 root 路径区分生产 coordinator)
109
+ - [ ] 对账写入 PR 描述:**根治条件 1/2/5 全闭合**(D3 弧线完成);3/4/6 属 Phase 3-6;已知残留清单更新(硬宕窗口已关,剩「coordinator 永不回来」= 既有 fail-closed 语义)
110
+
111
+ ## Self-Review 记录
112
+
113
+ - Spec 覆盖:根治条件 5 读侧 + §5 降级;不越界到 Phase 3-6。
114
+ - 接口一致性:`statusRevisionDesynced`/bootstrap 分支语义/`STATUS_BOOTSTRAP_KINDS` 在 Task 1/2/3 间一致。
115
+ - 无占位符;每个 hygiene 项独立可验证。