@zhuxixi/pi-agent-board 0.3.0 → 0.3.1
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/README.md +3 -2
- package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +797 -0
- package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +576 -0
- package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +182 -0
- package/docs/superpowers/plans/2026-08-22-stable-list-order.md +51 -0
- package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +106 -0
- package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +109 -0
- package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +80 -0
- package/package.json +1 -1
- package/runner/job-runner.mjs +28 -4
- package/src/core/auto-state.mjs +62 -7
- package/src/core/derive.mjs +4 -4
- package/src/core/events.mjs +3 -2
- package/src/core/pty-attach-jiggle-controller.mjs +112 -0
- package/src/core/pty-attach-jiggle-retry.mjs +29 -10
- package/src/core/pty-attach-render.mjs +21 -0
- package/src/core/rows.mjs +23 -8
- package/src/core/types.mjs +2 -2
- package/src/runtime/service.mjs +11 -1
- package/src/ui/dashboard.ts +2 -2
- package/src/ui/pty-attach.ts +26 -77
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Rename State Labels Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Rename dashboard state labels so each waiting state names the user action it waits for: `needs_input` → "Needs answer", `idle` → "Needs instructions", plus header wording "awaiting input" → "needs answer".
|
|
6
|
+
|
|
7
|
+
**Architecture:** Display-only rename riding the existing label pipeline: `GROUP_LABELS` (group headers, confirm prompts, filter aliases) and `fallbackStatusText` (row summaries) are the two label sources; persisted summaries normalize at render time via `normalizeGenericStatusText` + `GENERIC_STATUS_TEXT` legacy sets, so old rows migrate visually with zero data writes.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js ESM (`.mjs`) core + TypeScript UI file, `node --test` test runner, JSDoc typedefs.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- No changes to `SEMANTIC_STATES`, store schemas, state transitions, or classifier internals (issue #14's scope).
|
|
14
|
+
- `GENERIC_STATUS_TEXT` sets only ever GROW — "Needs input", "Idle", "In Progress" must remain recognized legacy texts.
|
|
15
|
+
- Do NOT edit `PRD.md` or `IMPLEMENTATION_PLAN.md` (historical point-in-time docs).
|
|
16
|
+
- Commit messages in English, conventional-commits format; stage files explicitly (never `git add -A`).
|
|
17
|
+
- All work happens in the worktree `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-15-rename-state-labels` (branch `issue-15-rename-state-labels`). Never touch the main checkout.
|
|
18
|
+
- Exact new labels: `"Needs answer"` (needs_input), `"Needs instructions"` (idle), header `"needs answer"` / compact `"answer"`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: Core label constants and normalization (types.mjs, derive.mjs)
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Modify: `src/core/types.mjs:65,70` (`GROUP_LABELS`)
|
|
26
|
+
- Modify: `src/core/derive.mjs:19-20,52-54` (`GENERIC_STATUS_TEXT`, `fallbackStatusText`)
|
|
27
|
+
- Test: `test/derive.test.mjs:92-98`, `test/rows.test.mjs:85-88`
|
|
28
|
+
|
|
29
|
+
**Interfaces:**
|
|
30
|
+
- Consumes: none (leaf change).
|
|
31
|
+
- Produces: `GROUP_LABELS.needs_input === "Needs answer"`, `GROUP_LABELS.idle === "Needs instructions"`; `fallbackStatusText("needs_input") === "Needs answer"`, `fallbackStatusText("idle") === "Needs instructions"`; `normalizeGenericStatusText` recognizes "Needs input" → needs_input label, {"Idle","In Progress"} → idle label.
|
|
32
|
+
|
|
33
|
+
- [ ] **Step 1: Update the failing tests**
|
|
34
|
+
|
|
35
|
+
In `test/derive.test.mjs`, extend the import (line 3) and replace/extend the fallback test:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { deriveSummary, fallbackStatusText, finalizeSemanticState, normalizeGenericStatusText } from "../src/core/derive.mjs";
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
test("fallbackStatusText", () => {
|
|
43
|
+
assert.equal(fallbackStatusText("queued"), "Queued");
|
|
44
|
+
assert.equal(fallbackStatusText("working"), "Running…");
|
|
45
|
+
assert.equal(fallbackStatusText("needs_input"), "Needs answer");
|
|
46
|
+
assert.equal(fallbackStatusText("idle"), "Needs instructions");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("normalizeGenericStatusText maps legacy labels to current ones", () => {
|
|
50
|
+
assert.equal(normalizeGenericStatusText("idle", "Idle"), "Needs instructions");
|
|
51
|
+
assert.equal(normalizeGenericStatusText("idle", "In Progress"), "Needs instructions");
|
|
52
|
+
assert.equal(normalizeGenericStatusText("needs_input", "Needs input"), "Needs answer");
|
|
53
|
+
assert.equal(normalizeGenericStatusText("idle", "Custom summary"), "Custom summary");
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
In `test/rows.test.mjs`, replace the existing normalization test (lines 85-88) with:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
test("rowView normalizes generic status labels to current display names", () => {
|
|
61
|
+
assert.equal(rowView(row("a", "working", { summary: "Working…" }), 0).summary, "Running…");
|
|
62
|
+
assert.equal(rowView(row("b", "idle", { summary: "Idle" }), 0).summary, "Needs instructions");
|
|
63
|
+
assert.equal(rowView(row("c", "idle", { summary: "In Progress" }), 0).summary, "Needs instructions");
|
|
64
|
+
assert.equal(rowView(row("d", "needs_input", { summary: "Needs input" }), 0).summary, "Needs answer");
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
(`row(id, state, overrides)` is the existing test helper in that file.)
|
|
69
|
+
|
|
70
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
71
|
+
|
|
72
|
+
Run: `cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-15-rename-state-labels && npm ci --no-audit --no-fund && node --test test/derive.test.mjs test/rows.test.mjs`
|
|
73
|
+
Expected: FAIL — fallbackStatusText/rowView assertions return "Needs input"/"In Progress". (Skip `npm ci` if node_modules already exists.)
|
|
74
|
+
|
|
75
|
+
- [ ] **Step 3: Implement**
|
|
76
|
+
|
|
77
|
+
`src/core/types.mjs` — in `GROUP_LABELS`:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
needs_input: "Needs answer",
|
|
81
|
+
```
|
|
82
|
+
```js
|
|
83
|
+
idle: "Needs instructions",
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`src/core/derive.mjs` — legacy sets (keep old entries, add new):
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
needs_input: new Set(["Needs input", "Needs answer"]),
|
|
90
|
+
idle: new Set(["Idle", "In Progress", "Needs instructions"]),
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`fallbackStatusText` cases:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
case "needs_input":
|
|
97
|
+
return "Needs answer";
|
|
98
|
+
case "idle":
|
|
99
|
+
return "Needs instructions";
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
103
|
+
|
|
104
|
+
Run: `node --test test/derive.test.mjs test/rows.test.mjs`
|
|
105
|
+
Expected: PASS (all tests in both files).
|
|
106
|
+
|
|
107
|
+
- [ ] **Step 5: Commit**
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git add src/core/types.mjs src/core/derive.mjs test/derive.test.mjs test/rows.test.mjs
|
|
111
|
+
git commit -m "feat: rename needs_input/idle labels to Needs answer/Needs instructions (issue #15)"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Task 2: Call-site labels and header wording (service.mjs, dashboard.ts, README)
|
|
115
|
+
|
|
116
|
+
**Files:**
|
|
117
|
+
- Modify: `src/runtime/service.mjs:961` (reconciled-host summary)
|
|
118
|
+
- Modify: `src/ui/dashboard.ts:926` (compat-guard blank state summary), `src/ui/dashboard.ts:1757` (header stage part)
|
|
119
|
+
- Modify: `README.md:65` (state list)
|
|
120
|
+
- Test: none new — grep-confirmed zero test assertions on these exact strings; suite must stay green.
|
|
121
|
+
|
|
122
|
+
**Interfaces:**
|
|
123
|
+
- Consumes: Task 1 label values (only literals here; no imports of the strings).
|
|
124
|
+
- Produces: none.
|
|
125
|
+
|
|
126
|
+
- [ ] **Step 1: Edit the four call sites**
|
|
127
|
+
|
|
128
|
+
`src/runtime/service.mjs` (~L961, inside `reconcile()`):
|
|
129
|
+
```js
|
|
130
|
+
s.summary = failed ? "Failed (PTY host exited)" : "Needs instructions";
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`src/ui/dashboard.ts` (~L926, blank-state compat guard):
|
|
134
|
+
```js
|
|
135
|
+
summary: "Needs instructions",
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`src/ui/dashboard.ts` (~L1757, `headerStageSummary`):
|
|
139
|
+
```js
|
|
140
|
+
headerStagePart(theme, "needs_input", counts.needs, compact ? "answer" : "needs answer"),
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`README.md` (L65):
|
|
144
|
+
```markdown
|
|
145
|
+
- Watch rows move through `Queued`, `Running`, `Needs answer`, `Needs instructions`, `Done`, `Failed`, and `Stopped`.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- [ ] **Step 2: Run the full suite and typecheck**
|
|
149
|
+
|
|
150
|
+
Run: `npm test && npm run typecheck`
|
|
151
|
+
Expected: PASS — `node --test test/*.test.mjs` all green, `tsc --noEmit` clean.
|
|
152
|
+
|
|
153
|
+
- [ ] **Step 3: Commit**
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
git add src/runtime/service.mjs src/ui/dashboard.ts README.md
|
|
157
|
+
git commit -m "feat: apply Needs answer/Needs instructions wording to service, header, and README (issue #15)"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Task 3: Full verification gate
|
|
161
|
+
|
|
162
|
+
**Files:**
|
|
163
|
+
- Modify: none (verification only; fix and amend-with-new-commit if anything fails).
|
|
164
|
+
|
|
165
|
+
**Interfaces:**
|
|
166
|
+
- Consumes: Tasks 1-2.
|
|
167
|
+
- Produces: verified branch ready for review.
|
|
168
|
+
|
|
169
|
+
- [ ] **Step 1: Run the project's standard gate**
|
|
170
|
+
|
|
171
|
+
Run: `npm run verify`
|
|
172
|
+
Expected: typecheck + full test suite + pack:dry all pass.
|
|
173
|
+
|
|
174
|
+
- [ ] **Step 2: Sweep for stale label strings**
|
|
175
|
+
|
|
176
|
+
Run: `grep -rn '"In Progress"' src/ README.md; grep -rn 'awaiting input' src/`
|
|
177
|
+
Expected: "In Progress" appears ONLY in `src/core/derive.mjs` legacy set; "awaiting input" zero hits.
|
|
178
|
+
|
|
179
|
+
- [ ] **Step 3: Confirm clean tree**
|
|
180
|
+
|
|
181
|
+
Run: `git status --short && git log --oneline main..HEAD`
|
|
182
|
+
Expected: clean tree; commits = spec + task 1 + task 2.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Stable List Order Fix Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make the dashboard list order stable — rows must never reshuffle while sessions are running, so the cursor never loses its target (issue #20).
|
|
6
|
+
|
|
7
|
+
**Architecture:** Replace activity-time sort keys with fixed creation-time keys. `lastActivityAt` stays in `RowView` for the "3s ago" display only, never for ordering. Rows sort by `pinned desc → createdAt asc → id asc`; folders sort by `pinned desc → min(createdAt) asc → name asc`, so a folder's position is anchored by its first-created row and doesn't jump when any row inside gets busy. New sessions append at the bottom (createdAt ascending).
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node 20+, ESM JavaScript (`.mjs` core), `node --test` test runner, no new dependencies.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- No new npm dependencies.
|
|
14
|
+
- All commits in this worktree branch `issue-20-stable-list-order`; never touch `main` checkout.
|
|
15
|
+
- `git add` per-file; never `git add -A`.
|
|
16
|
+
- Commit messages in English, conventional-commits format, reference `issue #20`.
|
|
17
|
+
- `npm run verify` (typecheck + tests + pack dry-run) must pass before the PR.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### Task 1: Stable row sort in `sortRowViews`
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- Modify: `src/core/rows.mjs` (function `sortRowViews`)
|
|
25
|
+
- Test: `test/rows.test.mjs`
|
|
26
|
+
|
|
27
|
+
- [x] Add `createdAt` to `RowView` typedef and `rowView()` output (fallback `meta.updatedAt ?? 0`).
|
|
28
|
+
- [x] Change sort keys to `pinned desc → createdAt asc → id asc`.
|
|
29
|
+
- [x] Update `groupRows` doc comment ("pinned first, then creation order").
|
|
30
|
+
- [x] Rewrite `groupRows sorts pinned first then recent` test → creation order, plus new regression test `groupRows ignores activity recency so order stays stable`.
|
|
31
|
+
|
|
32
|
+
### Task 2: Stable folder sort in `groupRowsByFolder`
|
|
33
|
+
|
|
34
|
+
**Files:**
|
|
35
|
+
- Modify: `src/core/rows.mjs` (function `groupRowsByFolder`)
|
|
36
|
+
- Test: `test/rows.test.mjs`
|
|
37
|
+
|
|
38
|
+
- [x] Folder key `lastActivityAt` → `createdAt`, computed as `Math.min` over folder rows (first-created row anchors the folder).
|
|
39
|
+
- [x] Folder sort keys → `pinned desc → createdAt asc → name asc`; update JSDoc return type.
|
|
40
|
+
- [x] Update `groupRowsByFolder nests rows by folder inside each stage` expectations, plus new regression test `groupRowsByFolder keeps folder order stable regardless of activity`.
|
|
41
|
+
|
|
42
|
+
### Task 3: Verify
|
|
43
|
+
|
|
44
|
+
- [x] `node --test test/rows.test.mjs` — 18 pass.
|
|
45
|
+
- [x] `npm run verify` — typecheck + full suite (200 pass) + pack dry-run.
|
|
46
|
+
|
|
47
|
+
## Non-goals
|
|
48
|
+
|
|
49
|
+
- Display of relative age ("3s ago") unchanged — still activity-based.
|
|
50
|
+
- State-group order (`GROUP_ORDER`) unchanged — already fixed.
|
|
51
|
+
- No reordering of the roster on disk; ordering is a pure view-model concern.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Spec:attach 冷启动双光标根治 — jiggle 重试链编排修复(issue #10)
|
|
2
|
+
|
|
3
|
+
## 日期
|
|
4
|
+
2026-08-21(v2:评审后修订,补三项编排严谨性修正)
|
|
5
|
+
|
|
6
|
+
## 问题
|
|
7
|
+
|
|
8
|
+
d10f21d(issue #2 修复)的 jiggle 重试链在**编排层**有三个不覆盖点,导致冷启动 attach 双光标未根治:
|
|
9
|
+
|
|
10
|
+
1. **启动时机错位**:链从 socket connect 即启动(start&attach 流程下 = spawn+0.3-0.5s),而冷启动 pi-tui 要 ~5.1-5.2s 才装好 resize 监听(EXP-2 实测 5180/5199/5191ms),链的窗口(connect+0.12~5.12s)全部落在 TUI 不存在的时段,注定空枪。
|
|
11
|
+
2. **jiggle 结构脆弱**:±1 尺寸、shrink→restore 仅隔 40ms。初始 sendResize 在 TUI 启动前白花掉唯一一次真实尺寸变化(120x36→196x39);后续重试全是围绕已正确尺寸的 ±1 噪声,启动期繁忙事件循环 + pi-tui 16ms 渲染节流可将其合并成净零变化 → 命中也不清屏。
|
|
12
|
+
3. **链断零自愈**:链耗尽即 stopRetry,一次性补偿已被 d10f21d 移除;此后脏屏只能等用户 detach/reattach 或拖窗口。
|
|
13
|
+
|
|
14
|
+
状态机本身(pty-attach-jiggle-retry.mjs)无 bug,不推翻,只改编排。
|
|
15
|
+
|
|
16
|
+
## 设计原则
|
|
17
|
+
|
|
18
|
+
- 沿用"可测性优先":新增逻辑抽成纯函数/可注入控制器,验收 = 单测 + 一个确定性 E2E。
|
|
19
|
+
- 不碰 runner(它即时应用 resize,无 debounce,无责)。
|
|
20
|
+
- 保留 settle-survive(ADR 202608211244444979 已记录取舍)。
|
|
21
|
+
- 失同步检测兜底**不在本 issue**(→ #11)。
|
|
22
|
+
|
|
23
|
+
## 方案(四个改动)
|
|
24
|
+
|
|
25
|
+
### 改动 1:重试链 re-arm — 首个 TUI 帧触发(一次性锁存)
|
|
26
|
+
|
|
27
|
+
**信号**:子 pi-tui 每帧(含差分帧)以 `\x1b[?2026h`(synchronized output begin)开头;冷启动 boot 期扩展输出不含该序列(EXP-2 screen.log 验证)→ 首个 `\x1b[?2026h` = "TUI 已开始渲染"的可靠信号。
|
|
28
|
+
|
|
29
|
+
**纯逻辑层**(`src/core/pty-attach-jiggle-retry.mjs` 扩展):
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
/** 检测数据中是否含 TUI 帧开始序列(跨 chunk 安全) */
|
|
33
|
+
function hasTuiFrameStart(data: string): boolean; // 查 \x1b[?2026h
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**跨 chunk carry 修正(评审修正 #2)**:现有 `CARRY_LEN = 3` 只服务四字节的 `\x1b[2J`;`\x1b[?2026h` 是 8 字节序列,跨 chunk 时会漏检。**carry 统一扩到 7 字节**(两个目标序列的公共前缀都是 `\x1b[`;`tailCarry` 从尾部 7 字节里找最后一个 `\x1b` 起携带,逻辑同构)。feedOutput 返回值增加 `frameStartFound`,与 `clearFound` 共用同一次扫描与 carry。
|
|
37
|
+
|
|
38
|
+
**编排层**(`src/ui/pty-attach.ts`):
|
|
39
|
+
- connect 时照旧 `startJiggleRetry()`(保热 attach 现状)。
|
|
40
|
+
- 新增组件字段 `tuiFrameSeen = false`(**一次性锁存,评审修正 #1**):output 路径检测到首个 `\x1b[?2026h` 且 `!tuiFrameSeen && !clearDetected` 时,置 `tuiFrameSeen = true` 并重置链为新状态机(retryIndex=0、预算计满)。**没有锁存的话 streaming 中每一帧都会 re-arm,退避永远走不完(评审发现的设计缺陷)。**
|
|
41
|
+
- **clear 优先规则(评审修正 #2 附带)**:同一 chunk 同时含 frame-start 与 clear(热 attach 常见:首帧即全清帧)时,先处理 clear(停链),re-arm 判断以"clear 处理后的最新状态"为准,禁止刚成功又被重置。
|
|
42
|
+
- 冷启动效果:TUI 开始渲染的瞬间链重启,第一个 jiggle 必落在活 TUI 上 → ~120ms 内见到 \x1b[2J。
|
|
43
|
+
- 热 attach 效果:首帧几乎与 connect 同时到达,链重置一次后首个 jiggle 立即成功自停;等价于现状。
|
|
44
|
+
|
|
45
|
+
### 改动 2:退避尾部拉长(与 60s 验收对齐,评审修正 #3)
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const BACKOFF_MS = [120, 500, 1500, 3000, 6000, 10000, 15000, 20000]; // 8 次,累计 56.12s
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- 依据:jiggle 对健康 session 无害(见到 clear 自停);长尾部只在前序全失败(画面本就脏)时才走完,代价已在 settle-survive ADR 中接受。
|
|
52
|
+
- **累计窗口必须与"60s 内自愈"验收标准一致(评审修正 #3)**:v1 草案的 ~92s 与验收冲突,修正为 56.12s。
|
|
53
|
+
- 与改动 1 的关系:改动 1 解决"时机",改动 2 兜底"比 5s 更极端的慢启动/合并丢失"。
|
|
54
|
+
|
|
55
|
+
### 改动 3:jiggle 抗合并 — restore 延时 40ms → 200ms
|
|
56
|
+
|
|
57
|
+
- `forceChildRedraw` 的 `redrawTimer` 延时抽为常量 `JIGGLE_RESTORE_MS = 200`。
|
|
58
|
+
- 依据:200ms > pi-tui 16ms 节流一个数量级,启动期繁忙循环也能把 shrink/restore 分成两次独立渲染(各触发一次 fullRender)。
|
|
59
|
+
- 副作用评估:jiggle 只发给子端 PTY,不改本地 xterm 尺寸;settle defer 已覆盖 redraw 窗口;post-settle 残余 jiggle 的闪烁取舍同 ADR。
|
|
60
|
+
- 不采纳的备选:交替幅度 -1/-2(对"基线已正确"场景无效,徒增复杂度)。
|
|
61
|
+
|
|
62
|
+
### 改动 4:确定性 E2E 测试(验收核心)
|
|
63
|
+
|
|
64
|
+
编排层抽取:把"timer 调度 + jiggle 发送 + output 喂入"从 PtyAttachComponent 抽成**可注入控制器**(注入 scheduler 与 sendJiggle 回调),组件退化为薄 adapter。控制器可脱离 TUI 单测/E2E。
|
|
65
|
+
|
|
66
|
+
- `test/pty-attach-jiggle-controller.test.mjs`(单测级):首帧检测(含跨 chunk 边界切割)、`tuiFrameSeen` 一次性锁存(第二帧不再 re-arm)、clear 优先规则、新退避表累计值、链在见 clear 后自停。
|
|
67
|
+
- `test/pty-attach-cold-start-e2e.test.mjs`:真实 pty-runner(node-pty)+ **stub 子进程**(node 脚本:延迟 8s 才"启动 TUI"——开始发 `\x1b[?2026h` 帧并安装 resize 监听;监听到 resize 且 TUI 已启动时回发 fullRender + `\x1b[2J`)+ 真实控制器。断言 **30s 内检测到 \x1b[2J**。
|
|
68
|
+
- 反证有效性:stub 延迟 8s > 旧链窗口 5.12s,旧编排在该测试下必失败。
|
|
69
|
+
- 时长 ~10-15s,可进 `npm test`;如 CI 敏感再拆慢测试脚本。
|
|
70
|
+
|
|
71
|
+
## 决策表
|
|
72
|
+
|
|
73
|
+
| 决策点 | 选择 | 理由 |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| TUI 就绪信号 | `\x1b[?2026h` | 每帧必有;boot 噪声不含(实测);零成本 |
|
|
76
|
+
| re-arm vs 仅首帧启动 | re-arm(connect 链保留) | 热/冷统一;热路径不变 |
|
|
77
|
+
| re-arm 次数 | 每次连接仅一次(tuiFrameSeen 锁存) | 防 streaming 每帧重置链 |
|
|
78
|
+
| frame-start 与 clear 同 chunk | clear 优先 | 防刚成功又被重置 |
|
|
79
|
+
| restore 延时 | 200ms 常量 | ≫16ms 节流;对 attach 投影无影响 |
|
|
80
|
+
| 退避尾部 | 8 次 / 累计 56.12s | 对齐 60s 验收;健康 session 自停 |
|
|
81
|
+
| E2E 方式 | stub 子进程 + 真 runner + 真控制器 | 确定性、快、可 CI |
|
|
82
|
+
|
|
83
|
+
## 降级
|
|
84
|
+
|
|
85
|
+
- pi-tui 若改掉 2026h:re-arm 失效但 connect 链 + 长预算仍在,退化为"比现在好"(文档化注释)。
|
|
86
|
+
- pi-tui 若 fullRender 不再发 \x1b[2J:同 d10f21d 已文档化的退化(链按预算走完即停,不 worse than one-shot)。
|
|
87
|
+
|
|
88
|
+
## 非目标
|
|
89
|
+
|
|
90
|
+
- 失同步检测渲染兜底(→ #11)。
|
|
91
|
+
- screen.log 重放无锚点问题(#2 已分析,jiggle 即其解药)。
|
|
92
|
+
- runner / pi-tui 侧改动。
|
|
93
|
+
- 外层 ghost 内容("[Themes] light-warm" 残留)——独立 cosmetic 问题,不在此列。
|
|
94
|
+
|
|
95
|
+
## 验收标准
|
|
96
|
+
|
|
97
|
+
1. 新 E2E 通过(stub 8s 延迟 TUI,30s 内见 \x1b[2J);且能证明旧编排下失败。
|
|
98
|
+
2. 新增单测通过;既有测试(jiggle-retry 状态机等)不回归;`npm run typecheck` 过。
|
|
99
|
+
3. 人工实机验证:start & attach 冷启动一个重扩展 session,双光标/脏帧在 60s 内自愈,无需 detach/reattach。
|
|
100
|
+
4. 热 session attach 无劣化(首个 jiggle 见 clear 自停,无多余重绘)。
|
|
101
|
+
|
|
102
|
+
## 评审修订记录(v2)
|
|
103
|
+
|
|
104
|
+
1. re-arm 必须加一次性锁存 `tuiFrameSeen`(否则 streaming 每帧重置链,退避失效)。
|
|
105
|
+
2. 跨 chunk carry 从 3 字节扩到 7 字节(`\x1b[?2026h` 为 8 字节序列);同 chunk 含 frame-start+clear 时 clear 优先。
|
|
106
|
+
3. 退避表从 ~92s 收敛为累计 56.12s,与"60s 内自愈"验收标准对齐。
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Spec: 运行中提问归类 needs_input + 永不自动 done(issue #14)
|
|
2
|
+
|
|
3
|
+
- Date: 2026-08-21
|
|
4
|
+
- Issue: https://github.com/zhuxixi/pi-agent-board/issues/14
|
|
5
|
+
- Status: design approved by user (2026-08-21 23:49)
|
|
6
|
+
|
|
7
|
+
## 1. 背景与现状(research 已闭环)
|
|
8
|
+
|
|
9
|
+
- hosted/foreground 场景的 `ask_questions` 工具提问 → `needs_input` **已实现**(#42, commit 752db70,`preservePendingQuestion()`),测试已覆盖。
|
|
10
|
+
- **Gap A**:自然语言文本提问(`detectNeedsInput` 命中)运行中只写 `status.question`,`semanticState` 仍强制 `working`(`events.mjs` `message_end` 分支)。
|
|
11
|
+
- **Gap B**:detached(job-runner)场景 `ask_questions` 不 track——**用户确认不用管**(headless 不会提问,也回复不了)。非目标。
|
|
12
|
+
- **Gap C**:进程退出后 auto-state(`heuristicAutoState().hasDoneSignal()` + state-runner 模型分类)会把行自动归类 `completed`。`finalizeSemanticState()` 本身不产生 completed。
|
|
13
|
+
|
|
14
|
+
## 2. 需求
|
|
15
|
+
|
|
16
|
+
1. 运行中(`processState === "alive"`)session 的**文本提问**也归类为 `needs_input`(面板 input 类型),覆盖 hosted / detached / foreground 全部场景。
|
|
17
|
+
2. **永不自动归类 `completed`(done)**;`completed` 只由用户手动 mark completed 产生。
|
|
18
|
+
|
|
19
|
+
## 3. 设计决策
|
|
20
|
+
|
|
21
|
+
| 决策点 | 结论 | 依据 |
|
|
22
|
+
|--------|------|------|
|
|
23
|
+
| 提问检测 | 复用现有 `detectNeedsInput`(保守:结尾问号或短语命中) | 已在 `finalizeRun` 长期使用,行为成熟 |
|
|
24
|
+
| 恢复机制 | 依赖既有 `message_start` / `tool_execution_start` / 无问题 `message_end` 设回 `working` | 天然存在,不加新状态机 |
|
|
25
|
+
| pending 优先 | `preservePendingQuestion()` 保持最后调用,pending 存在时覆盖为 `needs_input` | #42 并行语义不回退 |
|
|
26
|
+
| 不自动 done 的强度 | **彻底**:auto-state 永不产出 done(启发式 + 模型输出均降级为 in_progress/idle) | 用户拍板 |
|
|
27
|
+
| 回退开关 | `AGENT_BOARD_AUTO_STATE_NO_DONE`,默认开(不自动 done);`0`/`false`/`off` 恢复旧行为 | 保守可回退 |
|
|
28
|
+
| detached 回复路径 | 不改(保持 #42 现状) | 用户确认不用管 |
|
|
29
|
+
| alive guard | 不改(运行中分类全由 events.mjs 负责;auto-state 仅在 run 结束后触发,无运行中调用路径) | 改动面最小 |
|
|
30
|
+
|
|
31
|
+
## 4. 改动点(文件级)
|
|
32
|
+
|
|
33
|
+
### 4.1 `src/core/events.mjs` — `reduceEvent()` `message_end` 分支
|
|
34
|
+
|
|
35
|
+
现状:
|
|
36
|
+
```js
|
|
37
|
+
if (text) {
|
|
38
|
+
status.latestAssistantPreview = truncate(text, PREVIEW_MAX);
|
|
39
|
+
const nb = detectNeedsInput(text);
|
|
40
|
+
status.question = nb.question;
|
|
41
|
+
}
|
|
42
|
+
status.semanticState = "working";
|
|
43
|
+
preservePendingQuestion(status);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
改为(仅一行条件):
|
|
47
|
+
```js
|
|
48
|
+
if (text) {
|
|
49
|
+
status.latestAssistantPreview = truncate(text, PREVIEW_MAX);
|
|
50
|
+
const nb = detectNeedsInput(text);
|
|
51
|
+
status.question = nb.question;
|
|
52
|
+
}
|
|
53
|
+
status.semanticState = nb.needsInput ? "needs_input" : "working";
|
|
54
|
+
preservePendingQuestion(status);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
注意 `nb` 需提到 `if (text)` 作用域外(`let nb = { needsInput: false, question: null }` 初始值,空文本时不误判)。
|
|
58
|
+
|
|
59
|
+
### 4.2 `src/core/auto-state.mjs` — 永不自动 done
|
|
60
|
+
|
|
61
|
+
1. `autoStateDoneDisabled(env)` 辅助:读 `AGENT_BOARD_AUTO_STATE_NO_DONE`(默认**开**=禁用自动 done)。
|
|
62
|
+
2. `heuristicAutoState()`:`done && !pending` 分支移除;`hasDoneSignal` 命中时返回 `in_progress`(reason 说明"completion detected but auto-done disabled")。
|
|
63
|
+
3. `parseAutoStateModelOutput()`:解析到 `done` 且开关开 → 降级 `in_progress`;开关关 → 保持 `done`。
|
|
64
|
+
4. `buildAutoStatePrompt()`:开关开时 prompt 只有两态(`needs_input` / `in_progress`),并提示"completed 由用户手动标记";开关关时恢复原三态 prompt。
|
|
65
|
+
5. `applyAutoStateToStatus()` / `applyAutoStateToViewState()`:guard 增加 `semanticState === "completed"` 直接 return false(**手动完成的 state 不被自动分类覆盖**,现有 guard 只排除 failed/stopped)。
|
|
66
|
+
|
|
67
|
+
(开关关 = 全部旧行为,保证可回退。)
|
|
68
|
+
|
|
69
|
+
### 4.3 测试
|
|
70
|
+
|
|
71
|
+
1. `test/events.test.mjs` 新增:
|
|
72
|
+
- "message_end 文本以问号结尾 → semanticState=needs_input,question 非空(无 pending 场景)"
|
|
73
|
+
- "needs_input 后下一条无问题 message_end → 回 working"
|
|
74
|
+
- "pending question 存在时 message_end 文本无问题 → 仍 needs_input(pending 优先回归)"
|
|
75
|
+
2. `test/auto-state.test.mjs` 更新:
|
|
76
|
+
- L6-11 模型输出 done → 默认降级 in_progress;开关关 → done(新增开关关用例)
|
|
77
|
+
- L25 启发式 "Done. Fixed the bug and tests pass." → in_progress
|
|
78
|
+
- L29-42 applyAutoStateToStatus → 默认 idle(不再 completed);开关关 → completed(保持旧断言)
|
|
79
|
+
3. 全量 `npm run verify` 绿。
|
|
80
|
+
|
|
81
|
+
## 5. 验收标准(Success Criteria)
|
|
82
|
+
|
|
83
|
+
1. 运行中 hosted session,assistant 文本以问题结尾 → 面板归类 input(`needs_input`),glyph ◇,分组进 needs_input。
|
|
84
|
+
2. 运行中 detached session 文本提问 → 同上。
|
|
85
|
+
3. `ask_questions` 工具提问归类 needs_input 不回退(现有测试仍绿)。
|
|
86
|
+
4. 提问后 agent 继续工作(新消息/工具)→ 自动回 working。
|
|
87
|
+
5. run 结束后启发式/模型分类**不产生** completed;行停留在 idle 或 needs_input。
|
|
88
|
+
6. 手动 mark completed 仍可用,且手动完成的 state 不被 auto-state 覆盖。
|
|
89
|
+
7. `AGENT_BOARD_AUTO_STATE_NO_DONE=0` 时恢复旧行为(可自动 done)。
|
|
90
|
+
8. `npm run verify`(typecheck + test + pack dry-run)全绿。
|
|
91
|
+
|
|
92
|
+
## 6. 非目标
|
|
93
|
+
|
|
94
|
+
- detached 场景的回复路径改造。
|
|
95
|
+
- auto-state alive guard 放宽(无调用路径,不加死代码)。
|
|
96
|
+
- UI 分组/排序/颜色变化(`semanticState` 已驱动全部显示)。
|
|
97
|
+
- 模型分类器更换或精度调优。
|
|
98
|
+
|
|
99
|
+
## 7. 风险与缓解
|
|
100
|
+
|
|
101
|
+
| 风险 | 等级 | 缓解 |
|
|
102
|
+
|------|------|------|
|
|
103
|
+
| 文本提问误报(引用问句) | 低 | 检测器只认结尾问号/短语;下一条消息自动恢复 working |
|
|
104
|
+
| pre-existing flaky 集成测试干扰 verify | 低 | 单文件重跑验证;与本改动无关的失败单独记录 |
|
|
105
|
+
| 模型降级路径缓存 | 低 | prompt 变化 → textHash 变化 → 自动重分类一次,无害 |
|
|
106
|
+
|
|
107
|
+
## 8. 成功率评估(用户已确认)
|
|
108
|
+
|
|
109
|
+
核心实现成功率 ~90-95%;残余风险为自然语言边缘判定与 pre-existing flaky 测试。流程保障:TDD → 本地 CR → Zima 单 Bot CR → merge。
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Spec: Rename state labels — needs_input → "Needs answer", idle → "Needs instructions"
|
|
2
|
+
|
|
3
|
+
- **Issue**: zhuxixi/pi-agent-board#15
|
|
4
|
+
- **Date**: 2026-08-22
|
|
5
|
+
- **Status**: approved (2026-08-22) — implemented on branch issue-15-rename-state-labels
|
|
6
|
+
- **Type**: display-only rename, no data-model change
|
|
7
|
+
|
|
8
|
+
## 1. Problem
|
|
9
|
+
|
|
10
|
+
Dashboard semantic-state labels mislead. `idle` shows as "In Progress" but means *run ended, nothing asked, not confidently done* — the opposite of active work implied next to "Running". `needs_input` shows as "Needs input" but its distinct user action is *answering an explicit question*. The header collides lexically: needs_input counts read "awaiting input" while idle rows group under "In Progress".
|
|
11
|
+
|
|
12
|
+
History (research): upstream `5c67518` renamed idle "Idle" → "In Progress" and introduced the legacy-text normalization mechanism; `99691e1` later made idle the auto-state classifier's "ended but not done" bucket, at which point the label started systematically misleading. Issue #14 (open) will make idle the default terminal bucket, increasing exposure.
|
|
13
|
+
|
|
14
|
+
## 2. Goals
|
|
15
|
+
|
|
16
|
+
- Each waiting state's label names the user action it waits for:
|
|
17
|
+
- `needs_input` → **"Needs answer"** — agent asked a question; it needs your reply.
|
|
18
|
+
- `idle` → **"Needs instructions"** — run ended cleanly; it needs your next directive (follow-up, new task, or mark-done).
|
|
19
|
+
- Header needs_input stage reads "needs answer" (compact "answer") — no "awaiting" wording for needs_input, so "awaiting"-family ambiguity disappears.
|
|
20
|
+
- Persisted rows display the new labels with zero migration.
|
|
21
|
+
|
|
22
|
+
## 3. Non-goals
|
|
23
|
+
|
|
24
|
+
- No changes to `SEMANTIC_STATES` internal names, store schemas (`meta.json`/`state.json`/`status.json`), state transitions, or the auto-state classifier internals — that is issue #14's scope.
|
|
25
|
+
- No changes to historical docs (`PRD.md`, `IMPLEMENTATION_PLAN.md`) — they are point-in-time records.
|
|
26
|
+
- No i18n layer.
|
|
27
|
+
|
|
28
|
+
## 4. Design
|
|
29
|
+
|
|
30
|
+
### 4.1 Label sources (code changes)
|
|
31
|
+
|
|
32
|
+
| File | Change |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `src/core/types.mjs` | `GROUP_LABELS.needs_input: "Needs input"` → `"Needs answer"`; `GROUP_LABELS.idle: "In Progress"` → `"Needs instructions"` |
|
|
35
|
+
| `src/core/derive.mjs` | `fallbackStatusText("needs_input")` → `"Needs answer"`; `fallbackStatusText("idle")` → `"Needs instructions"` |
|
|
36
|
+
| `src/runtime/service.mjs` (~L961) | reconciled-host summary `"In Progress"` → `"Needs instructions"` |
|
|
37
|
+
| `src/ui/dashboard.ts` (~L926) | placeholder row summary `"In Progress"` → `"Needs instructions"` |
|
|
38
|
+
| `src/ui/dashboard.ts` (~L1755) | headerStageSummary needs_input part: `"awaiting input"`/`"awaiting"` → `"needs answer"`/`"answer"` |
|
|
39
|
+
| `README.md` (L65) | state list: `Needs answer`, `Needs instructions` |
|
|
40
|
+
|
|
41
|
+
Stage headers render via `renderStageHeader` → `label.toUpperCase()`: displays "NEEDS ANSWER" / "NEEDS INSTRUCTIONS".
|
|
42
|
+
|
|
43
|
+
### 4.2 Backward compatibility (reuse the 5c67518 pattern)
|
|
44
|
+
|
|
45
|
+
`GENERIC_STATUS_TEXT` in `derive.mjs` is the recognized-legacy set consumed by `normalizeGenericStatusText(state, text)`, which `rowView()` (`rows.mjs`) applies at render time. Extend the sets; never shrink them:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
needs_input: new Set(["Needs input", "Needs answer"]),
|
|
49
|
+
idle: new Set(["Idle", "In Progress", "Needs instructions"]),
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Effect: rows persisted with any historical generic summary ("Idle", "In Progress", "Needs input") auto-display the current label. Non-generic summaries (real question text, error text) pass through untouched. No data migration, no store write.
|
|
53
|
+
|
|
54
|
+
### 4.3 Auto-following surfaces (verify only)
|
|
55
|
+
|
|
56
|
+
- Delete-confirm prompt and notices use `GROUP_LABELS[state].toLowerCase()` → "delete 3 needs instructions sessions?" (grammar acceptable).
|
|
57
|
+
- Filter aliases (`rows.mjs`) = `[stateName, GROUP_LABELS[state]]` → "needs answer", "idle", "needs instructions" match; "in progress" stops matching (accepted; state name "idle" still matches).
|
|
58
|
+
- Peek panel and header counts render derived values only.
|
|
59
|
+
|
|
60
|
+
### 4.4 Tests
|
|
61
|
+
|
|
62
|
+
Update string expectations:
|
|
63
|
+
- `test/derive.test.mjs:96-97` — fallbackStatusText for both states.
|
|
64
|
+
- `test/rows.test.mjs:87` — rowView normalization (legacy "Idle" → "Needs instructions").
|
|
65
|
+
- `test/service.test.mjs` (~L334) — summary assertions touching "Needs input".
|
|
66
|
+
|
|
67
|
+
Add normalization coverage:
|
|
68
|
+
- legacy "In Progress" → "Needs instructions"; legacy "Needs input" → "Needs answer" (render-level, via rowView).
|
|
69
|
+
- New fallback values appear for fresh runs.
|
|
70
|
+
|
|
71
|
+
## 5. Error handling / edge cases
|
|
72
|
+
|
|
73
|
+
- Old dashboard process + new code reading same store: summaries normalize on read; safe.
|
|
74
|
+
- Rows whose summary was manually set to a non-generic string: untouched (pass-through preserved).
|
|
75
|
+
- Classifier (`auto-state.mjs`) and steering prompts use internal kind names (`needs_input`/`in_progress`/`done`), not display labels — unaffected.
|
|
76
|
+
|
|
77
|
+
## 6. Verification
|
|
78
|
+
|
|
79
|
+
- `npm run verify` (project's standard gate) with updated tests.
|
|
80
|
+
- Manual: open dashboard with pre-existing rows carrying old summaries → group headers and row summaries show new labels; header shows "needs answer".
|
package/package.json
CHANGED
package/runner/job-runner.mjs
CHANGED
|
@@ -13,14 +13,14 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
import { appendLine, readJson } from "../src/core/atomic.mjs";
|
|
14
14
|
import { createRunStatus, finalizeRun, projectViewState, reduceEvent } from "../src/core/events.mjs";
|
|
15
15
|
import { encodePromptForCliArg } from "../src/core/prompt-transport.mjs";
|
|
16
|
-
import { applyAutoStateToStatus, autoStateEnabled, autoStateFromModelOrHeuristic, autoStateModel, buildAutoStatePrompt, heuristicAutoState } from "../src/core/auto-state.mjs";
|
|
16
|
+
import { applyAutoStateToStatus, autoStateEnabled, autoStateFromModelOrHeuristic, autoStateModel, buildAutoStatePrompt, heuristicAutoState, isManualCompletion } from "../src/core/auto-state.mjs";
|
|
17
17
|
import { appendDiagnostic } from "../src/core/diagnostics.mjs";
|
|
18
18
|
import { emptyEvidenceSnapshot, finalizeEvidence, reduceEvidence, summarizeEvidence, writeEvidence, writeRunEvidence } from "../src/core/evidence.mjs";
|
|
19
19
|
import { claimNextFollowUp, completeFollowUp, releaseFollowUp } from "../src/core/follow-up-queue.mjs";
|
|
20
20
|
import { newRunId } from "../src/core/ids.mjs";
|
|
21
21
|
import { launchRun } from "../src/core/launch.mjs";
|
|
22
22
|
import * as P from "../src/core/paths.mjs";
|
|
23
|
-
import { readState, writeState, writeStatus } from "../src/core/store.mjs";
|
|
23
|
+
import { readState, readStatus, writeState, writeStatus } from "../src/core/store.mjs";
|
|
24
24
|
import { readSteering, recordPlanReady } from "../src/core/steering.mjs";
|
|
25
25
|
import { buildApprovePlanPrompt, buildPlanChangesPrompt, buildPlanRequestPrompt } from "../src/core/steering-prompts.mjs";
|
|
26
26
|
|
|
@@ -102,6 +102,19 @@ function main() {
|
|
|
102
102
|
dirty = false;
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Persist only if the user hasn't marked the row done manually since the last
|
|
107
|
+
* persist. projectViewState() overwrites the row state unconditionally, so a
|
|
108
|
+
* post-exit model pass must never persist its stale in-memory status over a
|
|
109
|
+
* fresh manual completion.
|
|
110
|
+
*/
|
|
111
|
+
const persistUnlessManual = (force = false) => {
|
|
112
|
+
const latestView = readState(root, viewId);
|
|
113
|
+
if (isManualCompletion(latestView)) return false;
|
|
114
|
+
persist(force);
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
|
|
105
118
|
const scheduleFlush = () => {
|
|
106
119
|
if (flushTimer) {
|
|
107
120
|
dirty = true;
|
|
@@ -206,12 +219,12 @@ function main() {
|
|
|
206
219
|
if (changed) {
|
|
207
220
|
finalizeEvidence(evidence, status, Date.now());
|
|
208
221
|
status.evidenceSummary = summarizeEvidence(evidence);
|
|
209
|
-
|
|
222
|
+
persistUnlessManual(true);
|
|
210
223
|
}
|
|
211
224
|
return maybeModelSummary(config, status);
|
|
212
225
|
})
|
|
213
226
|
.then((changed) => {
|
|
214
|
-
if (changed)
|
|
227
|
+
if (changed) persistUnlessManual(true);
|
|
215
228
|
})
|
|
216
229
|
.catch(() => {})
|
|
217
230
|
.finally(() => {
|
|
@@ -333,6 +346,13 @@ async function maybeModelAutoState(config, status, evidence) {
|
|
|
333
346
|
[...config.piArgsPrefix, "--mode", "json", "-p", "--no-session", "--model", model, prompt],
|
|
334
347
|
15000,
|
|
335
348
|
);
|
|
349
|
+
// Fresh read: the user may have marked the row done manually during the model
|
|
350
|
+
// call. completeView clears autoState in both state.json and status.json, so a
|
|
351
|
+
// manual completion is detectable here; applying the classification to the stale
|
|
352
|
+
// in-memory status would clobber the user's verdict.
|
|
353
|
+
const fresh = readStatus(config.root, config.viewId, config.runId);
|
|
354
|
+
if (!fresh || isManualCompletion(fresh)) return false;
|
|
355
|
+
Object.assign(status, fresh);
|
|
336
356
|
const classification = autoStateFromModelOrHeuristic(out, latest, { lastAgentActivityAt: status.lastAgentActivityAt ?? null });
|
|
337
357
|
const changed = applyAutoStateToStatus(status, classification, Date.now());
|
|
338
358
|
if (changed) {
|
|
@@ -366,6 +386,10 @@ async function maybeModelSummary(config, status) {
|
|
|
366
386
|
[...config.piArgsPrefix, "--mode", "json", "-p", "--no-session", "--model", model, prompt],
|
|
367
387
|
15000,
|
|
368
388
|
);
|
|
389
|
+
// The user may have marked the row done manually during the summary call.
|
|
390
|
+
// Updating the stale status and letting the caller persist would clobber the
|
|
391
|
+
// manual completion, so bail out before touching the in-memory status.
|
|
392
|
+
if (isManualCompletion(readState(config.root, config.viewId))) return false;
|
|
369
393
|
const text = out.trim().split("\n").slice(-1)[0]?.trim();
|
|
370
394
|
if (text) {
|
|
371
395
|
status.summary = text.replace(/^["']|["']$/g, "").slice(0, 80);
|