@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,299 @@
1
+ # issue #106 实施计划:终态链仍须学习帧认知
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:** 让 `feed()` 在链的**两个终态**(`clearDetected` 与 `stopped`)下仍从后续输出学习 TUI 帧认知(`tuiFrameSeen`),使运行期失配自愈(#11)的 gate 2 在「链先停、TUI 后启动」的连接里不再永久关闭;学习本身零副作用(不发 resize、不设 timer、不改 retry state)。
6
+
7
+ **Architecture:** 改动落在唯一一处——`src/core/pty-attach-jiggle-controller.mjs` 的 `feed()`:把原本的两条终态早退(`:220`/`:221`)合并为一个终态分支,在分支内用既有纯函数 `feedOutput()` 扫本块数据、推进 `carry`、命中 `\x1b[?2026h` 即锁存 `tuiFrameSeen`,然后返回。纯函数层 `pty-attach-jiggle-retry.mjs` 不动;重打开协议仍归 `heal()`(自带 10s 限速与 5 次终身上限)。
8
+
9
+ **Tech Stack:** Node ESM `.mjs` 控制器 + 注入式副作用(`sendResize`/`setTimeoutFn`/`clearTimeoutFn`);测试 `node --test`,组件级用 `test-support/desync-heal-smoke.ts`(`node --experimental-transform-types`)。
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md`(根因、两个终态可达性、契约表、备选否决、验收矩阵 A1–A9/U1)。
12
+
13
+ ## 设计决策(本 plan 锁定)
14
+
15
+ 1. **两个终态合并为一个分支**(用户已确认范围 B):两者对重试协议含义相同(链已结束),对学习需求相同。
16
+ 2. **`if (tuiFrameSeen) return;` 前置短路**:锁存后终态下的每次扫描是无用功(惰性 shell session 长期输出);短路后稳态成本为零。
17
+ 3. **`carry` 仅在未锁存时推进**:帧标记 8 字节可跨 chunk,不推进会「只学一半」。
18
+ 4. **终态分支不改 `state`**:终态是「协议已结束」的判定,不得被后续输出改写(否则 G2 预算语义与既有 `no re-arm probe` 契约被推翻)。
19
+ 5. **不新增抽象**:`feedOutput()` 已返回 `frameStartFound` + `carry`,测试边界维持现状(controller = 状态机 + 注入副作用;观测点 `getState()` 与记录的 `resizes`)。
20
+ 6. **红证用「teeth check」而非提交红测试**:Task 1 先写测试跑出红再实现;Task 2 用「临时 checkout 修复前源码 → 跑红 → 恢复」证明组件级断言有牙齿。每个 commit 结束时树常绿。
21
+
22
+ ## Global Constraints
23
+
24
+ - `$WT = /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-106-terminal-frame-cognition`;全部操作用 `$WT` 绝对路径(工具 `cwd` 也用 `$WT`),git 用 `git -C $WT`;**禁碰 main**。
25
+ - commit message 英文 conventional commits;`git add <file>` 显式 stage,**禁止 `git add -A`**。
26
+ - 本次改动不涉及 coordinator/PTY 进程,**不需要** `AGENT_BOARD_ROOT`/`PI_CODING_AGENT_DIR` 隔离(纯单测 + 组件 smoke)。
27
+ - 测试层纪律:E2E(真子进程)不做——G2 需真等 56s、G4 需精确卡首帧前 resize(spec「可测性拆分设计」已给理由)。
28
+ - 分阶段提交,保持树常绿(每个 task 结束前必须跑绿)。
29
+ - `$WT/node_modules` 是指向主 checkout 的符号链接(281M 不做拷贝),**不要在 worktree 里跑 `npm ci`/`npm install`**。
30
+ - spec 提交哈希:`1529555`(本分支首个 commit,Task 2 的红证要用它)。
31
+
32
+ ## 验收映射
33
+
34
+ | Task | 验收 ID | 说明 |
35
+ |---|---|---|
36
+ | Task 1 | A1 A2 A3 A4 A6 A9(unit 段) | 单测:两个终态锁存、跨 chunk、heal 可达、零副作用、既有 27 条不回归 |
37
+ | Task 2 | A5 A9(integration 段) | 组件级 smoke H8:终态 + 晚到帧 → 真实 `checkDesync` 触发一次 heal;H4 仍不 heal |
38
+ | Task 3 | A7 A8 + spec 附录 | 文档纠正(可单独 revert)+ 全量 verify(typecheck/test/coverage/pack) |
39
+ | U1(无 task) | U1 | 人工观察项,合并后执行,非阻塞(见文末) |
40
+
41
+ ---
42
+
43
+ ### Task 1: 单测红证 + 终态学习实现
44
+
45
+ **Files:**
46
+ - Modify: `$WT/src/core/pty-attach-jiggle-controller.mjs`(`feed()`,当前 `:219-231`)
47
+ - Test: `$WT/test/pty-attach-jiggle-controller.test.mjs`(追加到文件末尾)
48
+
49
+ **Interfaces:**
50
+ - Consumes: `feedOutput(state, data, carry) → { state, carry, clearFound, frameStartFound }`(`src/core/pty-attach-jiggle-retry.mjs`,不改);`createJiggleRetryState()`/`stopRetry()`/`advanceRetry()`;`fakeScheduler()`/`makeController()`(测试内既有夹具,`resizes` 数组 + `scheduler.timers`)。
51
+ - Produces(Task 2 依赖): `feed()` 终态分支契约——终态(`state.clearDetected || state.stopped`)下:`tuiFrameSeen` 已为 true 时立即返回;否则用 `feedOutput()` 扫本块并推进 `carry`,命中帧标记即置 `tuiFrameSeen = true`;**不调用 `sendResize`、不设/清 timer、不修改 `state`**。
52
+
53
+ - [ ] **Step 1: 追加三条失败测试**(追加到 `test/pty-attach-jiggle-controller.test.mjs` 末尾)
54
+
55
+ ```js
56
+ // --- issue #106: terminal chain states must still learn frame cognition ---
57
+
58
+ test("issue #106: G2-exhausted chain still learns a late first frame (cognition only, zero side effects)", () => {
59
+ const { controller, scheduler, resizes } = makeController();
60
+ controller.start(170, 36);
61
+ scheduler.fireNext(); // G1 fires first in Map order (6000): restore, no frame seen
62
+ for (let i = 0; i < 8; i++) scheduler.fireNext(); // exhaust the 8-entry chain (G2)
63
+ assert.equal(controller.getState().stopped, true);
64
+ assert.equal(controller.getState().tuiFrameSeen, false);
65
+ const resizesBefore = resizes.length;
66
+ const timersBefore = scheduler.timers.size;
67
+ controller.feed("\x1b[?2026h late frame");
68
+ assert.equal(controller.getState().tuiFrameSeen, true, "cognition must be learned after the chain settled");
69
+ assert.equal(resizes.length, resizesBefore, "terminal feed must not probe");
70
+ assert.equal(scheduler.timers.size, timersBefore, "terminal feed must not arm timers");
71
+ assert.equal(controller.getState().stopped, true, "terminal state must not be rewritten");
72
+ assert.equal(controller.heal(170, 36), true, "gate 2 open: heal() must be reachable again");
73
+ });
74
+
75
+ test("issue #106: clearDetected without any frame still learns a late first frame", () => {
76
+ const { controller, resizes } = makeController();
77
+ controller.start(170, 36);
78
+ controller.feed("\x1b[2J\x1b[Hplain shell output"); // clear wins → terminal, still no frame
79
+ assert.equal(controller.getState().clearDetected, true);
80
+ assert.equal(controller.getState().tuiFrameSeen, false);
81
+ const resizesBefore = resizes.length;
82
+ controller.feed("\x1b[?2026h the TUI boots later");
83
+ assert.equal(controller.getState().tuiFrameSeen, true, "cognition must be learned after clearDetected");
84
+ assert.equal(resizes.length, resizesBefore, "terminal feed must not probe");
85
+ assert.equal(controller.getState().held, false, "terminal feed must not re-arm a hold");
86
+ });
87
+
88
+ test("issue #106: a frame marker split across chunks is still caught in a terminal state", () => {
89
+ const { controller } = makeController();
90
+ controller.start(170, 36);
91
+ controller.feed("\x1b[2J"); // terminal via clear, still no frame
92
+ controller.feed("\x1b[?202"); // first half of the 2026h marker
93
+ assert.equal(controller.getState().tuiFrameSeen, false, "a partial marker alone must not latch");
94
+ controller.feed("6h TUI frame");
95
+ assert.equal(controller.getState().tuiFrameSeen, true, "carry must bridge the chunk boundary");
96
+ });
97
+ ```
98
+
99
+ - [ ] **Step 2: 跑测试确认红**(A9 unit 段证据)
100
+
101
+ Run: `cd $WT && node --test test/pty-attach-jiggle-controller.test.mjs`
102
+ Expected: **FAIL 3 条**(新用例),既有 27 条仍 PASS。失败形态:
103
+ - 用例 1:`cognition must be learned after the chain settled`(实际 false)
104
+ - 用例 2:`cognition must be learned after clearDetected`(实际 false)
105
+ - 用例 3:`carry must bridge the chunk boundary`(实际 false)
106
+
107
+ 把这段输出留档(Task 3 用于 PR 描述的红证)。
108
+
109
+ - [ ] **Step 3: 实现终态学习分支**
110
+
111
+ 把 `feed()` 开头的两条早退替换为终态分支(`src/core/pty-attach-jiggle-controller.mjs`):
112
+
113
+ 旧:
114
+ ```js
115
+ function feed(data) {
116
+ if (state.clearDetected) return; // chain done; nothing left to detect
117
+ if (state.stopped) return; // chain ended (G2/G3/G4); output is inert
118
+ const result = feedOutput(state, data, carry);
119
+ ```
120
+
121
+ 新:
122
+ ```js
123
+ function feed(data) {
124
+ // Terminal chain states: a clear was seen (chain done) or the chain ended
125
+ // (G2/G3/G4). Output no longer drives the retry protocol — but frame
126
+ // cognition must still be learned from it (issue #106): a TUI whose first
127
+ // frame lands after the chain settled must still open the runtime desync
128
+ // backstop's gate 2 (issue #11), otherwise heal() stays unreachable for the
129
+ // rest of this connection. Cognition only — no timers, no resizes, and no
130
+ // retry-state change: re-opening the protocol is heal()'s job (rate-limited
131
+ // and lifetime-capped), not an output chunk's.
132
+ if (state.clearDetected || state.stopped) {
133
+ if (tuiFrameSeen) return; // latched already — nothing left to learn
134
+ const terminal = feedOutput(state, data, carry);
135
+ carry = terminal.carry; // keep cross-chunk marker detection intact
136
+ if (terminal.frameStartFound) tuiFrameSeen = true;
137
+ return;
138
+ }
139
+ const result = feedOutput(state, data, carry);
140
+ ```
141
+
142
+ 同时把 `feed()` 的 doc-comment 补一句终态说明(紧跟「…so the running child still sees a width delta (F1 slow-boot probe).」之后):
143
+
144
+ ```
145
+ * Terminal chain states still learn frame cognition from later output
146
+ * (issue #106) — cognition only; re-opening the protocol stays heal()'s job.
147
+ ```
148
+
149
+ 注意:本 step **不动** `feed()` 其余任何行、不动 `ensureHold`/`tickChain`/`heal`/`start`。
150
+
151
+ - [ ] **Step 4: 跑测试确认绿**
152
+
153
+ Run: `cd $WT && node --test test/pty-attach-jiggle-controller.test.mjs`
154
+ Expected: **PASS 30 条**(27 既有 + 3 新增)。特别确认既有 `no re-arm probe after chain exhausted (stopped)` 仍绿(A6:终态零副作用)。
155
+
156
+ - [ ] **Step 5: 跑相邻回归**
157
+
158
+ Run: `cd $WT && node --test test/pty-attach-jiggle-retry.test.mjs test/pty-attach-desync-heal.test.mjs test/pty-attach-cold-start-e2e.test.mjs test/pty-attach-reconnect.test.mjs`
159
+ Expected: 全 PASS(纯层未改、终态行为只在新增分支上变化)。
160
+
161
+ - [ ] **Step 6: Commit**
162
+
163
+ ```bash
164
+ git -C $WT add src/core/pty-attach-jiggle-controller.mjs test/pty-attach-jiggle-controller.test.mjs
165
+ git -C $WT commit -m "fix(attach): learn TUI frame cognition in terminal chain states (issue #106)"
166
+ ```
167
+
168
+ ---
169
+
170
+ ### Task 2: 组件级 smoke H8(gate 2 真打开 → 真触发一次 heal)
171
+
172
+ **Files:**
173
+ - Modify: `$WT/test-support/desync-heal-smoke.ts`(新增 H8 场景 + 顶部注释与 `out` 记录)
174
+ - Test: `$WT/test/pty-attach-desync-heal.test.mjs`(新增一条断言)
175
+
176
+ **Interfaces:**
177
+ - Consumes: `PtyAttachComponent` 真实实例 + `write()`(`pushOutput` + `checkClearSequence`,即真实 socket-data 路径)、`desyncFrame`/`CLEAR` 常量、`resizes(sent)` 辅助、H4 的「无帧」构造形状。
178
+ - Produces: smoke 输出字段 `lateFrameHeals: boolean`(供 `test/pty-attach-desync-heal.test.mjs` 断言)。
179
+
180
+ - [ ] **Step 1: 在 smoke 脚本中新增 H8 场景**(`test-support/desync-heal-smoke.ts`,放在 H7 场景之前的 `main()` 内;同时把 H 列表注释与 `out` 类型注释更新)
181
+
182
+ ```ts
183
+ // H8 (issue #106): the chain reached a terminal state before ANY TUI frame
184
+ // (shell clear, H4 shape); a first frame arriving later must still open gate 2
185
+ // and let checkDesync() heal exactly once.
186
+ {
187
+ const { attach, sent, clock } = makeAttach();
188
+ await write(attach, CLEAR + "plain shell output, no TUI frame"); // terminal, no frame
189
+ await write(attach, `${ESC}[?2026h`); // the TUI boots later
190
+ await write(attach, desyncFrame);
191
+ attach.finishAttachTransition();
192
+ clock.now += 10_000; // all other gates open — only gate 2 could block the heal
193
+ attach.checkDesync();
194
+ out.lateFrameHeals = attach.jiggleRetry.getState().healCount === 1 && resizes(sent) === 1;
195
+ }
196
+ ```
197
+
198
+ - [ ] **Step 2: 在测试文件里断言 H8**(`test/pty-attach-desync-heal.test.mjs`,紧随既有 `out.noFrameNoHeal` 断言之后)
199
+
200
+ ```js
201
+ assert.equal(parsed.lateFrameHeals, true, "H8 late first frame must open gate 2 and heal once (issue #106)");
202
+ ```
203
+
204
+ - [ ] **Step 3: teeth check —— 证明 H8 有牙齿(红证)**
205
+
206
+ ```bash
207
+ cd $WT
208
+ git checkout 1529555 -- src/core/pty-attach-jiggle-controller.mjs # 临时回到修复前源码(spec 提交)
209
+ node --test test/pty-attach-desync-heal.test.mjs # 期望 FAIL:H8 断言
210
+ git status --short # 期望仅 src/... 显示为 staged 的修改
211
+ git checkout HEAD -- src/core/pty-attach-jiggle-controller.mjs # 恢复修复
212
+ git status --short # 期望干净
213
+ ```
214
+
215
+ Expected(红):`H8 late first frame must open gate 2 and heal once (issue #106)` 失败;其余 H1–H7 断言仍 true。
216
+ 把这段输出留档(A9 integration 段证据)。
217
+
218
+ - [ ] **Step 4: 跑测试确认绿**
219
+
220
+ Run: `cd $WT && node --test test/pty-attach-desync-heal.test.mjs`
221
+ Expected: **PASS**,且 `parsed.noFrameNoHeal === true`(H4 回归:无帧仍不 heal)。
222
+
223
+ - [ ] **Step 5: Commit**
224
+
225
+ ```bash
226
+ git -C $WT add test-support/desync-heal-smoke.ts test/pty-attach-desync-heal.test.mjs
227
+ git -C $WT commit -m "test(attach): component-level coverage for terminal-state frame cognition (issue #106)"
228
+ ```
229
+
230
+ ---
231
+
232
+ ### Task 3: 文档纠正 + 全量 verify
233
+
234
+ **Files:**
235
+ - Modify: `$WT/src/core/pty-attach-jiggle-controller.mjs`(`feed()` doc-comment 一处陈述)
236
+
237
+ **Interfaces:**
238
+ - Consumes: 无新增。
239
+ - Produces: 无(纯注释;spec 附录条目)。
240
+
241
+ - [ ] **Step 1: 纠正 doc-comment 里的错误成因**
242
+
243
+ 旧(`feed()` doc-comment 内):
244
+ ```
245
+ * chunk: a clear only proves the child redraws, not that it isn't a TUI
246
+ * (screen-log replay bundles historical frames with clears, issue #11).
247
+ ```
248
+ 新:
249
+ ```
250
+ * chunk: a clear only proves the child redraws, not that it isn't a TUI
251
+ * (a live fullRender chunk bundles a frame start with its clear, issue #11).
252
+ ```
253
+ 依据(spec 附录):`replayScreenLog()`(`src/ui/pty-attach.ts:1091`)只调 `pushOutput()`,replay 不喂 controller;`checkClearSequence()` 只在 `onSocketData` 的 live `output` 分支被调(`:1050`)。
254
+
255
+ - [ ] **Step 2: 全量静态与测试**(A7、A8)
256
+
257
+ Run: `cd $WT && npm run typecheck && npm test`
258
+ Expected: typecheck 无错误;`npm test` 全绿(既有总数 + 3 条新单测)。
259
+
260
+ - [ ] **Step 3: 全量 verify**(A8)
261
+
262
+ Run: `cd $WT && npm run verify`
263
+ Expected: `typecheck` + `test` + `test:coverage` + `pack:dry` 全通过;无新增未覆盖分支告警。
264
+
265
+ - [ ] **Step 4: Commit**
266
+
267
+ ```bash
268
+ git -C $WT add src/core/pty-attach-jiggle-controller.mjs
269
+ git -C $WT commit -m "docs(attach): correct the clear-wins rationale in feed() (issue #106)"
270
+ ```
271
+
272
+ (若 review 要求最小 diff,此 commit 可单独 revert,不影响修复正确性。)
273
+
274
+ ---
275
+
276
+ ### Task 4: 对账与 PR 准备(无代码改动)
277
+
278
+ - [ ] **Step 1: 逐条对账验收矩阵**
279
+
280
+ 对 A1–A9 逐项贴出实际命令与结果(A1–A4/A6:Task 1 Step 4 输出;A5:Task 2 Step 4 输出;A9 红证:Task 1 Step 2 与 Task 2 Step 3 输出;A7/A8:Task 3 Step 2–3 输出);U1 标 `pending`(见下)。
281
+
282
+ - [ ] **Step 2: 确认工作区干净且提交序列正确**
283
+
284
+ Run: `git -C $WT status --short && git -C $WT log --oneline main..HEAD`
285
+ Expected: 干净;4 个 commit(spec / fix+tests / smoke / docs)。
286
+
287
+ - [ ] **Step 3: 暂停等用户许可**(流程硬门)
288
+
289
+ push 与开 PR 前**必须**获得用户明确许可(用户 AGENTS.md:不自动 commit/push 除非明确许可;本仓库流程:push/开 PR 前暂停)。许可后按 zima-pr-monitor 流程:push → 开 PR(正文标注「`clearDetected` 分支为推断场景 + 代码依据」)→ 打 `zima:needs-review` → 同 turn 前台阻塞等待 CR → 逐件修复 → 收敛后合并。
290
+
291
+ ---
292
+
293
+ ## U1(人工观察项,合并后执行,非阻塞)
294
+
295
+ 步骤:1) attach 到一个 shell 型或长静默的 host session;2) 让链走完预算(>60s 无帧无 clear)或先真实 resize 一次(G4);3) 在该 session 内启动 pi;4) 观察画面是否出现周期性全清重绘。
296
+
297
+ 通过标准:无周期性全清重绘(无误触发);若此时发生真失配,限速周期(10s)内自愈。
298
+
299
+ pending 理由(与 spec 一致):复现需真实 ≥56s 无帧窗口 + 人为构造失配,脚本化成本高、收益低;与 #11 的 U1 同性质,合并观察。