@zhuxixi/pi-agent-board 0.3.0 → 0.4.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/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-launch-cwd-favorites.md +916 -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-cwd-favorites-design.md +131 -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/commands/agent-board.ts +1 -0
- package/src/core/auto-state.mjs +62 -7
- package/src/core/cwd-stats.mjs +112 -0
- package/src/core/derive.mjs +4 -4
- package/src/core/events.mjs +3 -2
- package/src/core/launch-options.mjs +47 -0
- package/src/core/paths.mjs +2 -0
- 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 +15 -1
- package/src/ui/dashboard.ts +64 -15
- package/src/ui/pty-attach.ts +26 -77
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# Issue #14: Needs-input While Running + No Auto-Done 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:** 运行中(alive)session 的文本提问归类 `needs_input`;auto-state 永不自动归类 `completed`(开关可回退)。
|
|
6
|
+
|
|
7
|
+
**Architecture:** 纯函数级改动。`events.mjs` `reduceEvent()` 的 `message_end` 分支按 `detectNeedsInput` 结果设 semanticState;`auto-state.mjs` 内新增 `autoStateDoneDisabled(env)` 开关(`AGENT_BOARD_AUTO_STATE_NO_DONE`,默认禁用自动 done),启发式与模型分类统一降级为 `in_progress`;`applyAutoState*` 增加 completed guard 保护手动标记。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js (>=20), node:test, plain .mjs modules, JSDoc typedefs(无 TypeScript 构建)。
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- 所有测试用 `node --test`,断言库 `node:assert/strict`(与现有 test/*.test.mjs 一致)。
|
|
14
|
+
- 不改 UI 显示/分组/排序;不改 detached 回复路径;不改 alive guard。
|
|
15
|
+
- 环境变量名:`AGENT_BOARD_AUTO_STATE_NO_DONE`(默认未设置 = 禁用自动 done;`0`/`false`/`off`/`no` = 恢复自动 done)。
|
|
16
|
+
- 复用 `isOff()` 语义(`/^(0|false|off|no)$/i`)。
|
|
17
|
+
- 所有改动在 worktree `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-14-needs-input-no-auto-done` 内,禁止碰 main checkout。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### Task 1: 运行中文本提问 → needs_input(events.mjs)
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- Modify: `src/core/events.mjs`(`reduceEvent()` 的 `message_end` 分支,约 L121-139)
|
|
25
|
+
- Test: `test/events.test.mjs`(新增 2 用例)
|
|
26
|
+
|
|
27
|
+
**Interfaces:**
|
|
28
|
+
- Consumes: `detectNeedsInput(text)` → `{ needsInput: boolean, question: string|null }`(`src/core/heuristics.mjs` 已有,勿改)。
|
|
29
|
+
- Produces: 运行中 `status.semanticState` 可为 `"needs_input"`;`projectViewState().needsInput` 联动(已有逻辑,勿改)。
|
|
30
|
+
|
|
31
|
+
- [ ] **Step 1: 写失败测试(在 `test("message_end assistant updates preview and detects question"` 用例之后追加)**
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
test("message_end text ending with a question moves alive run to needs_input", () => {
|
|
35
|
+
const s = createRunStatus(cfg(), 1, 1000);
|
|
36
|
+
reduceEvent(
|
|
37
|
+
s,
|
|
38
|
+
{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "Should I continue with option A?" }] } },
|
|
39
|
+
2000,
|
|
40
|
+
);
|
|
41
|
+
assert.equal(s.semanticState, "needs_input");
|
|
42
|
+
assert.match(s.question, /option A/);
|
|
43
|
+
assert.equal(projectViewState(s, 2100).needsInput, true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("alive run returns to working after a following message without a question", () => {
|
|
47
|
+
const s = createRunStatus(cfg(), 1, 1000);
|
|
48
|
+
reduceEvent(
|
|
49
|
+
s,
|
|
50
|
+
{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "Which target should I use?" }] } },
|
|
51
|
+
2000,
|
|
52
|
+
);
|
|
53
|
+
assert.equal(s.semanticState, "needs_input");
|
|
54
|
+
reduceEvent(
|
|
55
|
+
s,
|
|
56
|
+
{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "Proceeding with the default target." }] } },
|
|
57
|
+
2500,
|
|
58
|
+
);
|
|
59
|
+
assert.equal(s.semanticState, "working");
|
|
60
|
+
assert.equal(s.question, null);
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- [ ] **Step 2: 运行测试验证失败**
|
|
65
|
+
|
|
66
|
+
Run: `cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-14-needs-input-no-auto-done && node --test test/events.test.mjs`
|
|
67
|
+
Expected: 2 个新用例 FAIL(`assert.equal(s.semanticState, "needs_input")` 实际为 `"working"`)。
|
|
68
|
+
|
|
69
|
+
- [ ] **Step 3: 最小实现(`src/core/events.mjs` `message_end` 分支)**
|
|
70
|
+
|
|
71
|
+
将现有:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
if (msg?.role === "assistant") {
|
|
75
|
+
status.turns += 1;
|
|
76
|
+
if (msg.model && !status.model) status.model = msg.model;
|
|
77
|
+
if (msg.stopReason) status.stopReason = msg.stopReason;
|
|
78
|
+
if (msg.errorMessage) status.error = msg.errorMessage;
|
|
79
|
+
else if (msg.stopReason === "stop") status.error = null;
|
|
80
|
+
const text = assistantText(msg);
|
|
81
|
+
if (text) {
|
|
82
|
+
// Store the full latest text (truncated) so peek shows meaningful output;
|
|
83
|
+
// deriveSummary() condenses it to a first sentence for the row.
|
|
84
|
+
status.latestAssistantPreview = truncate(text, PREVIEW_MAX);
|
|
85
|
+
const nb = detectNeedsInput(text);
|
|
86
|
+
status.question = nb.question;
|
|
87
|
+
}
|
|
88
|
+
status.semanticState = "working";
|
|
89
|
+
preservePendingQuestion(status);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
改为:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
if (msg?.role === "assistant") {
|
|
96
|
+
status.turns += 1;
|
|
97
|
+
if (msg.model && !status.model) status.model = msg.model;
|
|
98
|
+
if (msg.stopReason) status.stopReason = msg.stopReason;
|
|
99
|
+
if (msg.errorMessage) status.error = msg.errorMessage;
|
|
100
|
+
else if (msg.stopReason === "stop") status.error = null;
|
|
101
|
+
const text = assistantText(msg);
|
|
102
|
+
let nb = { needsInput: false, question: null };
|
|
103
|
+
if (text) {
|
|
104
|
+
// Store the full latest text (truncated) so peek shows meaningful output;
|
|
105
|
+
// deriveSummary() condenses it to a first sentence for the row.
|
|
106
|
+
status.latestAssistantPreview = truncate(text, PREVIEW_MAX);
|
|
107
|
+
nb = detectNeedsInput(text);
|
|
108
|
+
status.question = nb.question;
|
|
109
|
+
}
|
|
110
|
+
status.semanticState = nb.needsInput ? "needs_input" : "working";
|
|
111
|
+
preservePendingQuestion(status);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
注意:`preservePendingQuestion(status)` 保持在最后一行,保证 pending question 优先语义不变。
|
|
115
|
+
|
|
116
|
+
- [ ] **Step 4: 运行测试验证通过**
|
|
117
|
+
|
|
118
|
+
Run: `node --test test/events.test.mjs`
|
|
119
|
+
Expected: 全部 PASS(含既有 "interactive ask_questions waits for input" 与 "interactive questions remain visible across parallel activity until each call ends" 回归绿)。
|
|
120
|
+
|
|
121
|
+
- [ ] **Step 5: Commit**
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cd /home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-14-needs-input-no-auto-done
|
|
125
|
+
git add test/events.test.mjs src/core/events.mjs
|
|
126
|
+
git commit -m "feat: classify running text questions as needs_input (issue #14)"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### Task 2: heuristicAutoState 永不自动 done + env 开关(auto-state.mjs)
|
|
132
|
+
|
|
133
|
+
**Files:**
|
|
134
|
+
- Modify: `src/core/auto-state.mjs`(新增 `autoStateDoneDisabled()`;改 `heuristicAutoState()`)
|
|
135
|
+
- Test: `test/auto-state.test.mjs`(更新 1 断言 + 新增 1 用例)
|
|
136
|
+
|
|
137
|
+
**Interfaces:**
|
|
138
|
+
- Produces: `export function autoStateDoneDisabled(env = process.env) → boolean`(默认 true;`AGENT_BOARD_AUTO_STATE_NO_DONE` 为 `0/false/off/no` 时返回 false)。
|
|
139
|
+
- `heuristicAutoState(latestAssistantText, opts)` 的 `opts` 新增可选 `env`(默认 `process.env`)。既有调用点不传 env,行为自动跟随进程环境。
|
|
140
|
+
|
|
141
|
+
- [ ] **Step 1: 写失败测试**
|
|
142
|
+
|
|
143
|
+
在 `test/auto-state.test.mjs` 顶部 import 行追加 `autoStateDoneDisabled`:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
import { applyAutoStateToStatus, autoStateDoneDisabled, autoStateFromModelOrHeuristic, heuristicAutoState, parseAutoStateModelOutput } from "../src/core/auto-state.mjs";
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
将既有断言:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
test("heuristicAutoState detects done and in-progress turns", () => {
|
|
153
|
+
assert.equal(heuristicAutoState("Done. Fixed the bug and tests pass.").kind, "done");
|
|
154
|
+
assert.equal(heuristicAutoState("I updated one file. Next step is to add tests.").kind, "in_progress");
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
改为(默认不再 done):
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
test("heuristicAutoState detects done and in-progress turns", () => {
|
|
162
|
+
assert.equal(heuristicAutoState("Done. Fixed the bug and tests pass.").kind, "in_progress");
|
|
163
|
+
assert.equal(heuristicAutoState("I updated one file. Next step is to add tests.").kind, "in_progress");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("heuristicAutoState restores done classification when auto-done flag is off", () => {
|
|
167
|
+
assert.equal(
|
|
168
|
+
heuristicAutoState("Done. Fixed the bug and tests pass.", { env: { AGENT_BOARD_AUTO_STATE_NO_DONE: "0" } }).kind,
|
|
169
|
+
"done",
|
|
170
|
+
);
|
|
171
|
+
assert.equal(autoStateDoneDisabled({}), true);
|
|
172
|
+
assert.equal(autoStateDoneDisabled({ AGENT_BOARD_AUTO_STATE_NO_DONE: "0" }), false);
|
|
173
|
+
assert.equal(autoStateDoneDisabled({ AGENT_BOARD_AUTO_STATE_NO_DONE: "off" }), false);
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- [ ] **Step 2: 运行测试验证失败**
|
|
178
|
+
|
|
179
|
+
Run: `node --test test/auto-state.test.mjs`
|
|
180
|
+
Expected: `heuristicAutoState detects done and in-progress turns` FAIL(实际 `"done"` 不等于 `"in_progress"`);新增用例 FAIL(`autoStateDoneDisabled` 未导出)。
|
|
181
|
+
|
|
182
|
+
- [ ] **Step 3: 最小实现(`src/core/auto-state.mjs`)**
|
|
183
|
+
|
|
184
|
+
在 `autoStateModel()` 函数之后、`isOff()` 之前插入:
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
/**
|
|
188
|
+
* Whether automatic `done` classification is disabled.
|
|
189
|
+
* Default (env unset): disabled — completed is only ever set by the user.
|
|
190
|
+
* Set AGENT_BOARD_AUTO_STATE_NO_DONE to 0/false/off/no to restore auto-done.
|
|
191
|
+
* @param {NodeJS.ProcessEnv|Record<string,string|undefined>} [env]
|
|
192
|
+
*/
|
|
193
|
+
export function autoStateDoneDisabled(env = process.env) {
|
|
194
|
+
const raw = env.AGENT_BOARD_AUTO_STATE_NO_DONE;
|
|
195
|
+
if (typeof raw !== "string" || !raw.trim()) return true;
|
|
196
|
+
return !isOff(raw);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`heuristicAutoState` 的 done 分支(现有代码):
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
const lower = text.toLowerCase();
|
|
204
|
+
const pending = hasPendingSignal(lower);
|
|
205
|
+
const done = hasDoneSignal(lower);
|
|
206
|
+
if (done && !pending) {
|
|
207
|
+
return makeClassification("done", {
|
|
208
|
+
source: "heuristic",
|
|
209
|
+
confidence: hasStrongDoneSignal(lower) ? "high" : "medium",
|
|
210
|
+
reason: "Assistant reported the work is complete",
|
|
211
|
+
now: opts.now,
|
|
212
|
+
lastAgentActivityAt: opts.lastAgentActivityAt ?? null,
|
|
213
|
+
latestAssistantText: text,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
改为:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
const lower = text.toLowerCase();
|
|
222
|
+
const pending = hasPendingSignal(lower);
|
|
223
|
+
const done = hasDoneSignal(lower);
|
|
224
|
+
const env = opts.env ?? process.env;
|
|
225
|
+
if (done && !pending && !autoStateDoneDisabled(env)) {
|
|
226
|
+
return makeClassification("done", {
|
|
227
|
+
source: "heuristic",
|
|
228
|
+
confidence: hasStrongDoneSignal(lower) ? "high" : "medium",
|
|
229
|
+
reason: "Assistant reported the work is complete",
|
|
230
|
+
now: opts.now,
|
|
231
|
+
lastAgentActivityAt: opts.lastAgentActivityAt ?? null,
|
|
232
|
+
latestAssistantText: text,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (done && !pending) {
|
|
236
|
+
// Auto-done disabled: completion signals stay out of the completed bucket;
|
|
237
|
+
// the user marks completed manually from the dashboard.
|
|
238
|
+
return makeClassification("in_progress", {
|
|
239
|
+
source: "heuristic",
|
|
240
|
+
confidence: hasStrongDoneSignal(lower) ? "medium" : "low",
|
|
241
|
+
reason: "Assistant reported completion but auto-done is disabled",
|
|
242
|
+
now: opts.now,
|
|
243
|
+
lastAgentActivityAt: opts.lastAgentActivityAt ?? null,
|
|
244
|
+
latestAssistantText: text,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
同时更新 `heuristicAutoState` 的 JSDoc opts 行:`@param {{ now?: number, lastAgentActivityAt?: number|null, env?: NodeJS.ProcessEnv|Record<string,string|undefined> }} [opts]`。
|
|
250
|
+
|
|
251
|
+
- [ ] **Step 4: 运行测试验证通过**
|
|
252
|
+
|
|
253
|
+
Run: `node --test test/auto-state.test.mjs`
|
|
254
|
+
Expected: 全部 PASS。
|
|
255
|
+
|
|
256
|
+
- [ ] **Step 5: Commit**
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
git add test/auto-state.test.mjs src/core/auto-state.mjs
|
|
260
|
+
git commit -m "feat: disable auto-done in heuristic classifier by default (issue #14)"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
### Task 3: 模型输出 done 降级 + prompt 两态化(auto-state.mjs)
|
|
266
|
+
|
|
267
|
+
**Files:**
|
|
268
|
+
- Modify: `src/core/auto-state.mjs`(`parseAutoStateModelOutput()`、`buildAutoStatePrompt()`)
|
|
269
|
+
- Test: `test/auto-state.test.mjs`(更新 1 断言 + 新增 1 用例)
|
|
270
|
+
|
|
271
|
+
**Interfaces:**
|
|
272
|
+
- `parseAutoStateModelOutput(raw, opts)` 的 `opts` 新增可选 `env`;`buildAutoStatePrompt(latestAssistantText, env = process.env)` 新增第二参数(既有调用点不传,兼容)。
|
|
273
|
+
- 调用点(`runner/job-runner.mjs`、`runner/state-runner.mjs`)无需改动。
|
|
274
|
+
|
|
275
|
+
- [ ] **Step 1: 写失败测试**
|
|
276
|
+
|
|
277
|
+
将既有断言:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
test("parseAutoStateModelOutput normalizes model JSON", () => {
|
|
281
|
+
const c = parseAutoStateModelOutput('{"state":"done","confidence":"high","reason":"tests passed","question":null}', {
|
|
282
|
+
latestAssistantText: "Done. Tests passed.",
|
|
283
|
+
lastAgentActivityAt: 42,
|
|
284
|
+
});
|
|
285
|
+
assert.equal(c.kind, "done");
|
|
286
|
+
assert.equal(c.semanticState, "completed");
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
改为:
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
test("parseAutoStateModelOutput normalizes model JSON", () => {
|
|
293
|
+
const c = parseAutoStateModelOutput('{"state":"done","confidence":"high","reason":"tests passed","question":null}', {
|
|
294
|
+
latestAssistantText: "Done. Tests passed.",
|
|
295
|
+
lastAgentActivityAt: 42,
|
|
296
|
+
});
|
|
297
|
+
assert.equal(c.kind, "in_progress");
|
|
298
|
+
assert.equal(c.semanticState, "idle");
|
|
299
|
+
assert.match(c.reason, /auto-done is disabled/i);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
(其余 `source/confidence/lastAgentActivityAt` 断言保留。)
|
|
303
|
+
|
|
304
|
+
在文件末尾追加:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
test("parseAutoStateModelOutput keeps done when auto-done flag is off", () => {
|
|
308
|
+
const c = parseAutoStateModelOutput('{"state":"done","confidence":"high","reason":"tests passed","question":null}', {
|
|
309
|
+
latestAssistantText: "Done. Tests passed.",
|
|
310
|
+
lastAgentActivityAt: 42,
|
|
311
|
+
env: { AGENT_BOARD_AUTO_STATE_NO_DONE: "0" },
|
|
312
|
+
});
|
|
313
|
+
assert.equal(c.kind, "done");
|
|
314
|
+
assert.equal(c.semanticState, "completed");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("buildAutoStatePrompt omits done option by default and restores it when flag off", () => {
|
|
318
|
+
assert.ok(!/^- done:/m.test(buildAutoStatePrompt("Fix the bug.")));
|
|
319
|
+
assert.ok(/^- done:/m.test(buildAutoStatePrompt("Fix the bug.", { AGENT_BOARD_AUTO_STATE_NO_DONE: "0" })));
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
(import 行追加 `buildAutoStatePrompt`。)
|
|
324
|
+
|
|
325
|
+
- [ ] **Step 2: 运行测试验证失败**
|
|
326
|
+
|
|
327
|
+
Run: `node --test test/auto-state.test.mjs`
|
|
328
|
+
Expected: 3 个用例 FAIL(kind 为 `"done"` 而非 `"in_progress"`;prompt 仍含 done 行)。
|
|
329
|
+
|
|
330
|
+
- [ ] **Step 3: 最小实现**
|
|
331
|
+
|
|
332
|
+
`buildAutoStatePrompt` 现有实现:
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
export function buildAutoStatePrompt(latestAssistantText) {
|
|
336
|
+
const text = truncate(String(latestAssistantText || "").trim(), 6000);
|
|
337
|
+
return `Classify the LAST assistant response for a coding-agent dashboard.\n\nChoose exactly one state:\n- needs_input: the assistant asks the user for a decision, clarification, approval, credentials, or is blocked waiting for the user.\n- in_progress: work is partial, next steps remain, verification is pending/failed, or the assistant says it will continue later.\n- done: the requested work is complete, final answer given, no user input required.\n\nReturn ONLY minified JSON with this shape:\n{"state":"needs_input|in_progress|done","confidence":"high|medium|low","reason":"short reason <=18 words","question":"user-facing question or null"}\n\nLast assistant response:\n${text}`;
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
改为:
|
|
342
|
+
|
|
343
|
+
```js
|
|
344
|
+
export function buildAutoStatePrompt(latestAssistantText, env = process.env) {
|
|
345
|
+
const text = truncate(String(latestAssistantText || "").trim(), 6000);
|
|
346
|
+
const doneLine = autoStateDoneDisabled(env)
|
|
347
|
+
? ""
|
|
348
|
+
: "- done: the requested work is complete, final answer given, no user input required.\n";
|
|
349
|
+
const doneNote = autoStateDoneDisabled(env)
|
|
350
|
+
? "The user marks completed manually in the dashboard, so completion signals (done/completed/finished) must be classified as in_progress.\n"
|
|
351
|
+
: "";
|
|
352
|
+
const states = autoStateDoneDisabled(env) ? "needs_input|in_progress" : "needs_input|in_progress|done";
|
|
353
|
+
return `Classify the LAST assistant response for a coding-agent dashboard.\n\nChoose exactly one state:\n- needs_input: the assistant asks the user for a decision, clarification, approval, credentials, or is blocked waiting for the user.\n- in_progress: work is partial, next steps remain, verification is pending/failed, or the assistant says it will continue later.\n${doneLine}${doneNote}\nReturn ONLY minified JSON with this shape:\n{"state":"${states}","confidence":"high|medium|low","reason":"short reason <=18 words","question":"user-facing question or null"}\n\nLast assistant response:\n${text}`;
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`parseAutoStateModelOutput` 现有代码:
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
const obj = extractJsonObject(raw);
|
|
361
|
+
if (!obj) return null;
|
|
362
|
+
const kind = normalizeKind(obj.state ?? obj.kind ?? obj.status);
|
|
363
|
+
if (!kind) return null;
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
改为:
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
const obj = extractJsonObject(raw);
|
|
370
|
+
if (!obj) return null;
|
|
371
|
+
let kind = normalizeKind(obj.state ?? obj.kind ?? obj.status);
|
|
372
|
+
if (!kind) return null;
|
|
373
|
+
if (kind === "done" && autoStateDoneDisabled(opts.env ?? process.env)) {
|
|
374
|
+
kind = "in_progress";
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
并在 `makeClassification(kind, {...})` 的调用中把 reason 兜底交给 `defaultReason(kind)`(已有逻辑),另在 opts 里透传降级提示:把
|
|
379
|
+
|
|
380
|
+
```js
|
|
381
|
+
return makeClassification(kind, {
|
|
382
|
+
source: "model",
|
|
383
|
+
confidence,
|
|
384
|
+
reason: cleanReason(obj.reason) || defaultReason(kind),
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
改为:
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
return makeClassification(kind, {
|
|
391
|
+
source: "model",
|
|
392
|
+
confidence,
|
|
393
|
+
reason: kind === "in_progress" && autoStateDoneDisabled(opts.env ?? process.env)
|
|
394
|
+
? "Model reported done but auto-done is disabled"
|
|
395
|
+
: cleanReason(obj.reason) || defaultReason(kind),
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
并更新 `parseAutoStateModelOutput` 的 JSDoc:`opts` 增加 `env?: NodeJS.ProcessEnv|Record<string,string|undefined>`。
|
|
399
|
+
|
|
400
|
+
- [ ] **Step 4: 运行测试验证通过**
|
|
401
|
+
|
|
402
|
+
Run: `node --test test/auto-state.test.mjs`
|
|
403
|
+
Expected: 全部 PASS。
|
|
404
|
+
|
|
405
|
+
- [ ] **Step 5: Commit**
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
git add test/auto-state.test.mjs src/core/auto-state.mjs
|
|
409
|
+
git commit -m "feat: downgrade model done output and two-state prompt when auto-done disabled (issue #14)"
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### Task 4: applyAutoState* completed guard(保护手动标记)
|
|
415
|
+
|
|
416
|
+
**Files:**
|
|
417
|
+
- Modify: `src/core/auto-state.mjs`(`applyAutoStateToStatus()`、`applyAutoStateToViewState()`)
|
|
418
|
+
- Test: `test/auto-state.test.mjs`(新增 1 用例)
|
|
419
|
+
|
|
420
|
+
**Interfaces:**
|
|
421
|
+
- 不变更签名。行为:`semanticState === "completed"` 时直接 `return false`(与 failed/stopped 同列)。
|
|
422
|
+
|
|
423
|
+
- [ ] **Step 1: 写失败测试**
|
|
424
|
+
|
|
425
|
+
追加:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
test("applyAutoStateToStatus never overwrites a manually completed row", () => {
|
|
429
|
+
const status = {
|
|
430
|
+
processState: "exited",
|
|
431
|
+
semanticState: "completed",
|
|
432
|
+
currentTool: null,
|
|
433
|
+
question: null,
|
|
434
|
+
error: null,
|
|
435
|
+
latestAssistantPreview: "Done. Fixed the bug and tests pass.",
|
|
436
|
+
summary: "Done.",
|
|
437
|
+
};
|
|
438
|
+
const changed = applyAutoStateToStatus(status, heuristicAutoState(status.latestAssistantPreview), 100);
|
|
439
|
+
assert.equal(changed, false);
|
|
440
|
+
assert.equal(status.semanticState, "completed");
|
|
441
|
+
assert.equal(status.autoState, undefined);
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
- [ ] **Step 2: 运行测试验证失败**
|
|
446
|
+
|
|
447
|
+
Run: `node --test test/auto-state.test.mjs`
|
|
448
|
+
Expected: 新用例 FAIL(`changed` 为 `true`,semanticState 被改为 `"idle"`)。
|
|
449
|
+
|
|
450
|
+
- [ ] **Step 3: 最小实现**
|
|
451
|
+
|
|
452
|
+
`applyAutoStateToStatus` 的 guard 行:
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
if (!classification || status.processState === "alive") return false;
|
|
456
|
+
if (status.semanticState === "failed" || status.semanticState === "stopped") return false;
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
改为:
|
|
460
|
+
|
|
461
|
+
```js
|
|
462
|
+
if (!classification || status.processState === "alive") return false;
|
|
463
|
+
if (status.semanticState === "failed" || status.semanticState === "stopped" || status.semanticState === "completed") return false;
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
`applyAutoStateToViewState` 的 guard 行做同样修改(`failed`/`stopped` 之后追加 `|| state.semanticState === "completed"`)。
|
|
467
|
+
|
|
468
|
+
- [ ] **Step 4: 运行测试验证通过**
|
|
469
|
+
|
|
470
|
+
Run: `node --test test/auto-state.test.mjs`
|
|
471
|
+
Expected: 全部 PASS。
|
|
472
|
+
|
|
473
|
+
- [ ] **Step 5: Commit**
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
git add test/auto-state.test.mjs src/core/auto-state.mjs
|
|
477
|
+
git commit -m "fix: never overwrite manual completed state in auto-state (issue #14)"
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
### Task 5: 集成测试同步 + 全量验证 + 收尾 commit
|
|
483
|
+
|
|
484
|
+
**Files:**
|
|
485
|
+
- Modify: `test/runner.integration.test.mjs`(L45-80、L114-126 两处 completed 断言)
|
|
486
|
+
- Test: 同文件新增 1 用例(默认无自动 done)
|
|
487
|
+
|
|
488
|
+
**Interfaces:**
|
|
489
|
+
- fake worker 由 `process.env.FAKE_PI_MODE = "completed"` 驱动;job-runner 子进程继承父进程 env,所以 `AGENT_BOARD_AUTO_STATE_NO_DONE` 在 spawn 前设置即可生效。
|
|
490
|
+
|
|
491
|
+
- [ ] **Step 1: 更新既有断言(两处)**
|
|
492
|
+
|
|
493
|
+
`test("runner auto-classifies a completed fake worker and writes durable artifacts"` 用例(L45-80)的 env 设置块:
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
const root = mkdtempSync(join(tmpdir(), "agentview-run-"));
|
|
497
|
+
const env = { ...process.env };
|
|
498
|
+
process.env.FAKE_PI_MODE = "completed";
|
|
499
|
+
process.env.AGENT_BOARD_SUMMARY_MODEL = "off";
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
改为:
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
const root = mkdtempSync(join(tmpdir(), "agentview-run-"));
|
|
506
|
+
const env = { ...process.env };
|
|
507
|
+
process.env.FAKE_PI_MODE = "completed";
|
|
508
|
+
process.env.AGENT_BOARD_SUMMARY_MODEL = "off";
|
|
509
|
+
process.env.AGENT_BOARD_AUTO_STATE_NO_DONE = "0";
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
其 finally 块在 `delete process.env.AGENT_BOARD_SUMMARY_MODEL;` 之后追加:
|
|
513
|
+
|
|
514
|
+
```js
|
|
515
|
+
delete process.env.AGENT_BOARD_AUTO_STATE_NO_DONE;
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`test("runner protects dash-prefixed prompts passed via argv"` 用例(L114-126)同样:`process.env.AGENT_BOARD_SUMMARY_MODEL = "off";` 之后追加 `process.env.AGENT_BOARD_AUTO_STATE_NO_DONE = "0";`,finally 块同步 delete。
|
|
519
|
+
|
|
520
|
+
- [ ] **Step 2: 新增默认行为用例(在 "runner protects dash-prefixed prompts" 用例之后追加)**
|
|
521
|
+
|
|
522
|
+
```js
|
|
523
|
+
test("runner keeps a completed fake worker idle when auto-done is disabled", { timeout: 20000 }, async () => {
|
|
524
|
+
const root = mkdtempSync(join(tmpdir(), "agentview-run-nodone-"));
|
|
525
|
+
process.env.FAKE_PI_MODE = "completed";
|
|
526
|
+
process.env.AGENT_BOARD_SUMMARY_MODEL = "off";
|
|
527
|
+
delete process.env.AGENT_BOARD_AUTO_STATE_NO_DONE;
|
|
528
|
+
try {
|
|
529
|
+
const meta = createView(root, { id: "view_1", name: "fix", cwd: root });
|
|
530
|
+
const config = makeConfig(root, "view_1", "run_1", meta.sessionFile, root, "fix the bug");
|
|
531
|
+
const st = readState(root, "view_1");
|
|
532
|
+
st.currentRunId = "run_1";
|
|
533
|
+
const { writeState } = await import("../src/core/store.mjs");
|
|
534
|
+
writeState(root, st);
|
|
535
|
+
const { pid } = launchRun(root, config, { runnerScript: RUNNER });
|
|
536
|
+
assert.ok(pid && pid > 0, "runner spawned");
|
|
537
|
+
const status = await waitFor(() => {
|
|
538
|
+
const s = readStatus(root, "view_1", "run_1");
|
|
539
|
+
return s && s.endedAt ? s : null;
|
|
540
|
+
});
|
|
541
|
+
assert.ok(status, "status reached terminal state");
|
|
542
|
+
assert.equal(status.semanticState, "idle");
|
|
543
|
+
assert.equal(status.processState, "exited");
|
|
544
|
+
assert.equal(status.autoState?.kind, "in_progress");
|
|
545
|
+
} finally {
|
|
546
|
+
delete process.env.FAKE_PI_MODE;
|
|
547
|
+
delete process.env.AGENT_BOARD_SUMMARY_MODEL;
|
|
548
|
+
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
- [ ] **Step 3: 运行集成测试**
|
|
554
|
+
|
|
555
|
+
Run: `node --test test/runner.integration.test.mjs`
|
|
556
|
+
Expected: 全部 PASS。若个别用例因 pre-existing flaky(并行全量偶发)失败,单文件重跑确认。
|
|
557
|
+
|
|
558
|
+
- [ ] **Step 4: 全量验证**
|
|
559
|
+
|
|
560
|
+
Run: `npm run verify`(= typecheck + test + pack:dry)
|
|
561
|
+
Expected: 全绿。
|
|
562
|
+
|
|
563
|
+
- [ ] **Step 5: Commit**
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
git add test/runner.integration.test.mjs
|
|
567
|
+
git commit -m "test: keep auto-done off by default in runner integration (issue #14)"
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Self-Review(对照 spec)
|
|
573
|
+
|
|
574
|
+
1. **Spec coverage**:变更 1(events.mjs)→ Task 1;变更 2 启发式 → Task 2;模型降级+prompt → Task 3;completed guard → Task 4;测试同步+全量验证 → Task 5;开关回退 → Task 2/3 测试均有 env-off 断言。✅
|
|
575
|
+
2. **Placeholder scan**:已把 Task 5 的省略注释落实为具体代码(Step 1/2 完整代码块)。其余任务无 TBD/TODO/省略。✅
|
|
576
|
+
3. **Type consistency**:`autoStateDoneDisabled(env)` 在各任务签名一致;`heuristicAutoState` opts.env、`parseAutoStateModelOutput` opts.env、`buildAutoStatePrompt(text, env)` 一致。✅
|