agentflowctl 0.6.0 → 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.
- package/README.md +36 -10
- package/dist/cli.js +18 -15
- package/dist/engine.js +17 -14
- package/dist/feedback.js +15 -0
- package/dist/logs.js +85 -23
- package/dist/resultMeta.js +24 -0
- package/dist/runner.js +2 -23
- package/dist/stopReport.js +79 -0
- package/dist/tasks.js +5 -0
- package/package.json +1 -1
- package/prompts/plan-fix.md +1 -0
- package/prompts/plan-review.md +2 -2
- package/prompts/plan.md +4 -1
- package/prompts/spec.md +7 -0
package/README.md
CHANGED
|
@@ -99,10 +99,11 @@ agentflowctl run --req-file ./req.md --cycle codex,claude --max-agent-runs 40
|
|
|
99
99
|
agentflowctl run --req "..." --manual-plan # 計畫通過 AI 審查後,仍停下來等你確認
|
|
100
100
|
|
|
101
101
|
agentflowctl approve f-xxxx # 搭配 --manual-plan
|
|
102
|
-
agentflowctl status f-xxxx #
|
|
102
|
+
agentflowctl status f-xxxx # 階段、上一步結果、未結交接事項、下一步指令、任務進度、各 agent 用量、代打紀錄
|
|
103
103
|
agentflowctl list
|
|
104
104
|
agentflowctl logs f-xxxx # 列出每一份 log 的編號、結果、階段、步驟、agent
|
|
105
105
|
agentflowctl logs f-xxxx 7 # 解析第 7 份 log,最後附上錯誤整理(--latest 看最新一份)
|
|
106
|
+
agentflowctl logs f-xxxx 7 --full # 逐條顯示 shell 指令,完整顯示多行內容與絕對路徑
|
|
106
107
|
agentflowctl logs f-xxxx 7 --raw # 原始內容(agent 的 JSON 行)
|
|
107
108
|
agentflowctl resume f-xxxx # 從暫停、Ctrl-C 或失敗處接續
|
|
108
109
|
agentflowctl cancel f-xxxx
|
|
@@ -182,14 +183,14 @@ agentflowctl clean --all # 清掉所有已結束的 run 與中斷留
|
|
|
182
183
|
| 標記 | 內容 |
|
|
183
184
|
|---|---|
|
|
184
185
|
| 💬 | agent 的完整文字,不截斷 |
|
|
185
|
-
| 🔧 |
|
|
186
|
+
| 🔧 | 工具呼叫。連續的 shell 指令(多半是讀檔、搜尋)收成一行 `🔧 shell 指令 ×N`;其他工具只顯示第一行,多行時註明共幾行,worktree 內的絕對路徑改成相對路徑 |
|
|
186
187
|
| 📊 | token 用量 |
|
|
187
|
-
| 🏁 |
|
|
188
|
+
| 🏁 | 最後結果。與最後一則 💬 相同時不再重印 |
|
|
188
189
|
| ⚠️ | 工具回報的錯誤。agent 通常會自己換方法繼續,所以不列進錯誤整理 |
|
|
189
190
|
| ❌ | adapter 不認得的錯誤事件 |
|
|
190
191
|
| 📄 | 不是 JSON 的輸出行 |
|
|
191
192
|
|
|
192
|
-
adapter 不認得、也看不出錯誤跡象的 JSON 行不會顯示,只列出行數,要看全部請加 `--raw`。專案指令的 log 本來就是純文字,會原樣顯示。
|
|
193
|
+
要逐條看 shell 指令、完整的工具內容與重複的最後結果,加 `--full`。adapter 不認得、也看不出錯誤跡象的 JSON 行不會顯示,只列出行數,要看全部請加 `--raw`。專案指令的 log 本來就是純文字,會原樣顯示。
|
|
193
194
|
|
|
194
195
|
最後一段「錯誤」整理出結束碼、agent 回報的失敗、錯誤事件與 stderr:
|
|
195
196
|
|
|
@@ -199,7 +200,7 @@ adapter 不認得、也看不出錯誤跡象的 JSON 行不會顯示,只列出
|
|
|
199
200
|
檔案 /repo/.agentflowctl/runs/f-xxxx/logs/003-plan-plan-codex.log
|
|
200
201
|
|
|
201
202
|
💬 先讀 spec.md 與 acceptance.json
|
|
202
|
-
🔧 shell
|
|
203
|
+
🔧 shell 指令 ×1(--full 查看)
|
|
203
204
|
🏁 失敗:stream disconnected before completion
|
|
204
205
|
|
|
205
206
|
── 錯誤 ──
|
|
@@ -211,9 +212,33 @@ stderr:
|
|
|
211
212
|
|
|
212
213
|
執行成功時,stderr 會放在「其他輸出」段落,不算錯誤。
|
|
213
214
|
|
|
215
|
+
### 停下來時的結果與下一步
|
|
216
|
+
|
|
217
|
+
run 因 Ctrl-C、失敗、額度暫停或等待核准而停下時,終端機會直接印出三段;`agentflowctl status <id>` 也會印同樣的內容:
|
|
218
|
+
|
|
219
|
+
- **結果**:被中斷、還沒有結束紀錄的步驟,以及上一步的結果。agent 的步驟取回覆 `<result>` 的摘要與疑慮;失敗時優先顯示失敗的那一步,並附上結束碼、錯誤事件、stderr 或指令輸出的最後幾行。
|
|
220
|
+
- **未結交接事項**:交接紀錄裡還沒結案的 action 事項(open 或 proposed_resolved)。
|
|
221
|
+
- **下一步**:依狀態列出可以執行的指令,例如 `logs`、`cd` 到 worktree、`resume`、`cancel`、`approve`。
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
── 結果 ──
|
|
225
|
+
中斷於 #20 plan_review / plan-review / codex(沒有結束紀錄,resume 時會重跑這一步)
|
|
226
|
+
#19 plan_fix / plan-fix / claude ✓
|
|
227
|
+
摘要:接受三條審查意見,拆分 T-3、T-5
|
|
228
|
+
疑慮:T-15 可能仍太大
|
|
229
|
+
|
|
230
|
+
── 未結交接事項 ──
|
|
231
|
+
[plan] c9c730b280e87178 T-3、T-5 各混合多個獨立行為(proposed_resolved)
|
|
232
|
+
|
|
233
|
+
── 下一步 ──
|
|
234
|
+
agentflowctl logs f-xxxx 19 看上一步的完整 log
|
|
235
|
+
agentflowctl resume f-xxxx 從 plan_review 接續
|
|
236
|
+
agentflowctl cancel f-xxxx 放棄這個 run
|
|
237
|
+
```
|
|
238
|
+
|
|
214
239
|
### 出錯時怎麼查
|
|
215
240
|
|
|
216
|
-
1. run
|
|
241
|
+
1. 先看 run 停下時印出的「結果」與「下一步」,或執行 `agentflowctl status <id>`。
|
|
217
242
|
2. `agentflowctl logs <id> <編號>` 看那份 log 的錯誤段落。
|
|
218
243
|
3. 解析結果看不出原因時,加 `--raw` 看原始輸出。
|
|
219
244
|
4. 必要時直接在 worktree(`.agentflowctl/worktrees/<id>`)裡修正,再執行 `agentflowctl resume <id>`。
|
|
@@ -298,9 +323,9 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
298
323
|
|
|
299
324
|
人工確認計畫是為了擋住方向錯了還一路做下去。預設用三層機制取代它;加上 `--manual-plan` 時,三層都過了仍會停下來等你。
|
|
300
325
|
|
|
301
|
-
**格式與覆蓋率。** 每次撰寫或修改計畫之後,都要重新通過 zod
|
|
326
|
+
**格式與覆蓋率。** 每次撰寫或修改計畫之後,都要重新通過 zod、任務相依、無循環、每條驗收條件都有任務負責,而且每個任務最多對應兩條驗收條件(一次只做一件事,最多兩件)。沒過就還原。
|
|
302
327
|
|
|
303
|
-
**跨模型審查。**
|
|
328
|
+
**跨模型審查。** 審查看需求覆蓋、驗收條件能不能測且一條只寫一個行為、任務是否只做一件事(最多兩件)與技術方向。審查者只能寫意見。若改了規格或計畫,檔案會被還原。修改者要在 `plan.md` 的「審查回應」逐條回覆;不同意要寫理由。
|
|
304
329
|
|
|
305
330
|
**僵持時仲裁。** 兩種情況會觸發:這輪審查意見和上一輪一樣,或已達重試上限。仲裁者只判斷一件事:照這份計畫實作,能不能滿足需求。
|
|
306
331
|
|
|
@@ -310,7 +335,7 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
310
335
|
| 兩家 | 兩家各自在全新 context 裡判斷 | 都核准就繼續;都不核准就依裁決意見修訂並重新審查;分歧依 `tieBreak` |
|
|
311
336
|
| 一家 | 同一家 | 由它自己仲裁 |
|
|
312
337
|
|
|
313
|
-
|
|
338
|
+
兩家時的仲裁是雙盲的。仲裁者只看計畫,以及一份不含審查者名稱的爭議清單(`.flow/dispute.md`)。爭議清單用 `<issue>` 包住每則意見;給修訂者的 `.flow/feedback.md` 則用 `<opinion author="…">` 包住每位審查者的意見,避免意見內文與外層結構混淆。帶有名稱的審查紀錄移到 worktree 以外。`tieBreak` 預設 `proceed`,因為後面還有測試紅燈、綠燈、verify 與程式碼審查。
|
|
314
339
|
|
|
315
340
|
計畫定案或仲裁最終停止時,裁決與每位仲裁者的理由附在 `plan.md` 最後的「仲裁紀錄」。需再修訂時,裁決理由寫進 `.flow/feedback.md`,供修訂者處理;重新審查會從第一輪計數。原始審查與每輪仲裁紀錄在 `.agentflowctl/runs/<id>/reviews/`。兩家都要求修改時不因仲裁輪數而直接失敗;整個 run 仍受 `maxAgentRuns` 限制。
|
|
316
341
|
|
|
@@ -321,7 +346,7 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
321
346
|
| 階段 | 負責的 agent | 程式認定通過的條件 | 失敗時 |
|
|
322
347
|
|---|---|---|---|
|
|
323
348
|
| spec | 隨機一位 | 檔案存在、zod 驗證、id 不重複 | 重試 |
|
|
324
|
-
| plan | 與 spec 同一位 | zod
|
|
349
|
+
| plan | 與 spec 同一位 | zod、相依存在、無循環、每條驗收條件都有任務、每個任務最多兩條驗收條件 | 重試 |
|
|
325
350
|
| plan_review | 計畫作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve` | 進入 plan_fix |
|
|
326
351
|
| plan_fix | 依 `fixStrategy` | 修改後仍通過 plan 的格式與 DAG 檢查 | 還原並重試 |
|
|
327
352
|
| 仲裁 | 見上一節 | 一致核准;分歧依 `tieBreak` | 兩家都不核准時進入 plan_fix 再審查;第三方不核准或 `tieBreak: stop` 時失敗 |
|
|
@@ -411,6 +436,7 @@ src/
|
|
|
411
436
|
roles.ts 角色分配規則(含計畫修正者與仲裁者)
|
|
412
437
|
runner.ts 執行 agent、正規化結果、執行專案指令
|
|
413
438
|
logs.ts log 檔名、檔頭檔尾、列表與解析
|
|
439
|
+
stopReport.ts run 停下時的結果、未結交接事項與下一步指令
|
|
414
440
|
agents/ claude、codex、gemini、command
|
|
415
441
|
setup.ts agent setup 互動精靈
|
|
416
442
|
git.ts worktree 與 git 操作
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,8 @@ import { flowDir, logDir, projectRoot, worktreeDir } from "./paths.js";
|
|
|
15
15
|
import { TaskList } from "./schemas.js";
|
|
16
16
|
import { agentRuns, getRun, listRuns, listSubstitutions, saveRun, usageByAgent } from "./store.js";
|
|
17
17
|
import { readJsonFile } from "./util.js";
|
|
18
|
+
import { openActions, readHandoff } from "./handoff.js";
|
|
19
|
+
import { stopReport } from "./stopReport.js";
|
|
18
20
|
import { runSetup, SETUP_ADAPTERS } from "./setup.js";
|
|
19
21
|
import { addAgent, readRawConfig, removeAgent, setAgent, setCycle, writeRawConfig } from "./agentConfig.js";
|
|
20
22
|
function mustGetRun(id) {
|
|
@@ -26,12 +28,13 @@ function mustGetRun(id) {
|
|
|
26
28
|
async function drive(run) {
|
|
27
29
|
// Ctrl-C 會同時送給子程序(claude、測試指令),狀態已經寫在 state.json,之後可用 resume 接續
|
|
28
30
|
process.once("SIGINT", () => {
|
|
29
|
-
console.log(
|
|
31
|
+
console.log("\n已中斷");
|
|
32
|
+
printSummary(getRun(run.id) ?? run, true);
|
|
30
33
|
process.exit(130);
|
|
31
34
|
});
|
|
32
35
|
printSummary(await advance(run));
|
|
33
36
|
}
|
|
34
|
-
function printSummary(run) {
|
|
37
|
+
function printSummary(run, interrupted = false) {
|
|
35
38
|
console.log("");
|
|
36
39
|
console.log(`run ${run.id}`);
|
|
37
40
|
console.log(`階段 ${run.stage}`);
|
|
@@ -46,22 +49,21 @@ function printSummary(run) {
|
|
|
46
49
|
if (run.stage === "failed") {
|
|
47
50
|
console.log(`失敗於 ${run.failedStage ?? "?"}`);
|
|
48
51
|
console.log(`原因 ${run.failureReason ?? "?"}`);
|
|
49
|
-
const logs = listLogs(logDir(run.id));
|
|
50
|
-
const last = logs.at(-1);
|
|
51
|
-
const lastFailed = logs.filter((e) => e.footer && !e.footer.ok).at(-1);
|
|
52
|
-
if (last)
|
|
53
|
-
console.log(`最後的 log #${last.seq} ${logMark(last)}(agentflowctl logs ${run.id} ${last.seq})`);
|
|
54
|
-
if (lastFailed && lastFailed !== last)
|
|
55
|
-
console.log(`最近失敗的 log #${lastFailed.seq}(agentflowctl logs ${run.id} ${lastFailed.seq})`);
|
|
56
|
-
console.log(`\n必要時直接在 worktree 裡修正,再執行 agentflowctl resume ${run.id}`);
|
|
57
52
|
}
|
|
58
53
|
if (run.stage === "paused") {
|
|
59
54
|
console.log(`暫停於 ${run.pausedStage ?? "?"}`);
|
|
60
55
|
console.log(`原因 ${run.pauseReason ?? "?"}`);
|
|
61
|
-
console.log(`\n額度恢復後執行 agentflowctl resume ${run.id}`);
|
|
62
56
|
}
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const report = stopReport({
|
|
58
|
+
run,
|
|
59
|
+
interrupted,
|
|
60
|
+
logs: listLogs(logDir(run.id)),
|
|
61
|
+
read: (file) => readFileSync(file, "utf8"),
|
|
62
|
+
open: openActions(readHandoff(run.id)),
|
|
63
|
+
worktree: worktreeDir(run.id),
|
|
64
|
+
});
|
|
65
|
+
for (const line of report)
|
|
66
|
+
console.log(line);
|
|
65
67
|
}
|
|
66
68
|
/** 決定參與的 agent:指令參數 > flow.config.json > 自動偵測已安裝的 CLI */
|
|
67
69
|
async function resolveCycle(flag) {
|
|
@@ -367,6 +369,7 @@ program
|
|
|
367
369
|
.command("logs <id> [seq]")
|
|
368
370
|
.description("列出 log;指定編號(或 --latest)時顯示解析後的內容,最後附上錯誤整理")
|
|
369
371
|
.option("--latest", "顯示最新一份 log", false)
|
|
372
|
+
.option("--full", "逐條顯示 shell 指令,完整顯示工具內容(多行指令、絕對路徑)與重複的最後回覆", false)
|
|
370
373
|
.option("--raw", "顯示原始內容(agent 的 JSON 行)", false)
|
|
371
374
|
.action((id, seq, opts) => {
|
|
372
375
|
mustGetRun(id);
|
|
@@ -379,14 +382,14 @@ program
|
|
|
379
382
|
const h = e.header;
|
|
380
383
|
console.log(`${String(e.seq).padStart(3)} ${logMark(e).padEnd(4)} ${(h?.stage ?? "?").padEnd(12)} ${(h?.step ?? "?").padEnd(19)} ${(h?.agent ?? "?").padEnd(10)} ${localTime(h?.startedAt)}`);
|
|
381
384
|
}
|
|
382
|
-
console.log(`\n查看內容:agentflowctl logs ${id} <編號>(加 --raw 看原始 JSON)`);
|
|
385
|
+
console.log(`\n查看內容:agentflowctl logs ${id} <編號>(加 --full 看完整工具內容、--raw 看原始 JSON)`);
|
|
383
386
|
return;
|
|
384
387
|
}
|
|
385
388
|
const entry = seq ? logs.find((e) => e.seq === Number(seq)) : logs.at(-1);
|
|
386
389
|
if (!entry)
|
|
387
390
|
throw new Error(`找不到 log #${seq}(共 ${logs.length} 份,可用 agentflowctl logs ${id} 列出)`);
|
|
388
391
|
const text = readFileSync(entry.file, "utf8");
|
|
389
|
-
console.log(opts.raw ? text : renderLog(text, entry.file));
|
|
392
|
+
console.log(opts.raw ? text : renderLog(text, entry.file, { full: opts.full }));
|
|
390
393
|
});
|
|
391
394
|
program.parseAsync().catch((err) => {
|
|
392
395
|
console.error(`錯誤:${err.message}`);
|
package/dist/engine.js
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { config } from "./config.js";
|
|
5
5
|
import { arbitrationDecision } from "./arbitration.js";
|
|
6
6
|
import { detectProjectDefaults, withProjectDefaults } from "./detect.js";
|
|
7
|
+
import { escapeXml, opinion, reviewIssue } from "./feedback.js";
|
|
7
8
|
import { changedFiles, commitAll, discardChanges, git, headCommit, resetTo } from "./git.js";
|
|
8
9
|
import { acceptHandoff, openActions, prepareHandoff, previewHandoff, readHandoff, recoverHandoff, reviewHandoffGate, validateHandoffResponse } from "./handoff.js";
|
|
9
10
|
import { flowDir, logDir, projectRoot, runDir, worktreeDir } from "./paths.js";
|
|
@@ -34,10 +35,13 @@ const exhausted = new Set();
|
|
|
34
35
|
function handoffTarget(run) {
|
|
35
36
|
return ["spec", "plan", "plan_review", "plan_fix"].includes(run.stage) ? "plan" : "code";
|
|
36
37
|
}
|
|
37
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* 每次執行 agent 都用新的 key。重試次數會在後面的輪次重複出現,只靠它組 key 會撞到先前已套用的呼叫,
|
|
40
|
+
* 讓這次的處置被當成重播而略過,所以加上這個 run 已執行 agent 的次數。
|
|
41
|
+
*/
|
|
38
42
|
function handoffKey(run, step, slot, agent) {
|
|
39
43
|
const attempts = Object.entries(run.attempts).sort(([a], [b]) => a.localeCompare(b));
|
|
40
|
-
return JSON.stringify([run.id, run.stage, step, run.taskIndex, run.taskPhase, attempts, slot, agent]);
|
|
44
|
+
return JSON.stringify([run.id, run.stage, step, run.taskIndex, run.taskPhase, attempts, slot, agent, agentRuns(run.id)]);
|
|
41
45
|
}
|
|
42
46
|
async function agentStep(run, planned, step, prompt, mode) {
|
|
43
47
|
const cfg = loadRepoConfig();
|
|
@@ -284,9 +288,9 @@ async function planReviewStage(run) {
|
|
|
284
288
|
firstObjector ??= reviewer;
|
|
285
289
|
const lines = review.data.items
|
|
286
290
|
.filter((i) => i.status !== "met")
|
|
287
|
-
.map((i) =>
|
|
291
|
+
.map((i) => reviewIssue(i.criterion, i.status, i.note));
|
|
288
292
|
issueLines.push(...lines);
|
|
289
|
-
issues.push(
|
|
293
|
+
issues.push(opinion(reviewer, lines));
|
|
290
294
|
}
|
|
291
295
|
if (!firstObjector)
|
|
292
296
|
return planSettled(run, "plan-review");
|
|
@@ -375,9 +379,10 @@ async function arbitratePlan(run) {
|
|
|
375
379
|
mkdirSync(join(runDir(run.id), "reviews"), { recursive: true });
|
|
376
380
|
renameSync(flowFile(run, "plan-arbiter.json"), join(runDir(run.id), "reviews", `plan-arbiter-${arbitrationRound}-${arbiter}.json`));
|
|
377
381
|
const verdict = result.data.verdict;
|
|
378
|
-
const notes = result.data.items.map((i) =>
|
|
382
|
+
const notes = result.data.items.map((i) => `<issue criterion="${escapeXml(i.criterion)}">${escapeXml(i.note)}</issue>`);
|
|
383
|
+
const markdownNotes = result.data.items.map((i) => `- ${i.criterion}:${i.note}`);
|
|
379
384
|
info(run, ` ${verdict === "approve" ? "✓" : "✗"} ${arbiter}:${verdict === "approve" ? "可以執行" : "不可執行"}`);
|
|
380
|
-
verdicts.push({ arbiter, verdict, notes });
|
|
385
|
+
verdicts.push({ arbiter, verdict, notes, markdownNotes });
|
|
381
386
|
}
|
|
382
387
|
rmSync(flowFile(run, "dispute.md"), { force: true });
|
|
383
388
|
const approvals = verdicts.filter((v) => v.verdict === "approve").length;
|
|
@@ -390,16 +395,16 @@ async function arbitratePlan(run) {
|
|
|
390
395
|
: approvals === 0
|
|
391
396
|
? `${mode}沒有任何一方核准${decision === "revise" ? ",交回計畫修訂" : ""}`
|
|
392
397
|
: `${mode}意見分歧,依 tieBreak=${cfg.tieBreak} ${cfg.tieBreak === "proceed" ? "繼續實作" : "停止"}`;
|
|
393
|
-
const
|
|
398
|
+
const feedbackRecord = verdicts.map((v) => opinion(v.arbiter, v.notes, v.verdict)).join("\n\n");
|
|
394
399
|
if (decision === "revise") {
|
|
395
400
|
const attempts = { ...run.attempts, "plan-arbitration": arbitrationRound };
|
|
396
401
|
delete attempts["plan-review"];
|
|
397
402
|
rmSync(flowFile(run, "plan-review-last.txt"), { force: true });
|
|
398
|
-
writeFileSync(flowFile(run, "feedback.md"), `# 仲裁要求修訂(第 ${arbitrationRound} 次)\n\n${summary}\n\n${
|
|
403
|
+
writeFileSync(flowFile(run, "feedback.md"), `# 仲裁要求修訂(第 ${arbitrationRound} 次)\n\n${summary}\n\n${feedbackRecord}\n`);
|
|
399
404
|
info(run, ` → ${summary}`);
|
|
400
405
|
return { ...run, attempts, stage: "plan_fix" };
|
|
401
406
|
}
|
|
402
|
-
writeFileSync(flowFile(run, "plan.md"), `${readFileSync(flowFile(run, "plan.md"), "utf8")}\n\n## 仲裁紀錄\n\n**結果:${summary}**\n\n${
|
|
407
|
+
writeFileSync(flowFile(run, "plan.md"), `${readFileSync(flowFile(run, "plan.md"), "utf8")}\n\n## 仲裁紀錄\n\n**結果:${summary}**\n\n${verdicts.map((v) => `### ${v.arbiter}(${v.verdict})\n\n${v.markdownNotes.join("\n")}`).join("\n\n")}\n`);
|
|
403
408
|
if (decision === "proceed") {
|
|
404
409
|
info(run, ` → ${summary}`);
|
|
405
410
|
return planSettled(run, "plan-review");
|
|
@@ -582,11 +587,9 @@ async function reviewStage(run) {
|
|
|
582
587
|
}
|
|
583
588
|
info(run, ` ✗ ${reviewer} 要求修改`);
|
|
584
589
|
firstObjector ??= reviewer;
|
|
585
|
-
issues.push(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
.map((i) => `- **${i.criterion}**(${i.status}):${i.note}`)
|
|
589
|
-
.join("\n"));
|
|
590
|
+
issues.push(opinion(reviewer, review.data.items
|
|
591
|
+
.filter((i) => i.status !== "met")
|
|
592
|
+
.map((i) => reviewIssue(i.criterion, i.status, i.note))));
|
|
590
593
|
}
|
|
591
594
|
if (!firstObjector)
|
|
592
595
|
return succeed(run, "review", "pr");
|
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** agent 提供的文字必須跳脫,才不會打斷意見的 XML 邊界。 */
|
|
2
|
+
export function escapeXml(value) {
|
|
3
|
+
return value.replace(/[&<>"']/g, (char) => ({
|
|
4
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
5
|
+
})[char]);
|
|
6
|
+
}
|
|
7
|
+
export function reviewIssue(criterion, status, note) {
|
|
8
|
+
return `<issue criterion="${escapeXml(criterion)}" status="${escapeXml(status)}">${escapeXml(note)}</issue>`;
|
|
9
|
+
}
|
|
10
|
+
export function opinion(author, issues, verdict) {
|
|
11
|
+
const name = escapeXml(author);
|
|
12
|
+
const outcome = verdict ? ` verdict="${escapeXml(verdict)}"` : "";
|
|
13
|
+
return `<opinion author="${name}"${outcome}>\n${issues.join("\n")}\n</opinion>`;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=feedback.js.map
|
package/dist/logs.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { ADAPTERS } from "./agents/index.js";
|
|
4
4
|
import { str, tryJson } from "./agents/types.js";
|
|
5
|
+
import { parseResultMeta } from "./resultMeta.js";
|
|
5
6
|
/**
|
|
6
7
|
* 每次執行 agent 或專案指令都寫成一份 log:
|
|
7
8
|
* - 檔名:<三位數序號>-<階段>-<步驟>-<agent>.log,專案指令的 agent 欄位是 cmd
|
|
@@ -96,47 +97,76 @@ function findError(ev) {
|
|
|
96
97
|
const hit = walk(ev, 0);
|
|
97
98
|
return hit ? { message: clip(pick(hit)), fatal: false } : undefined;
|
|
98
99
|
}
|
|
99
|
-
|
|
100
|
+
/** worktree 絕對路徑改成相對路徑:`<worktree>/x` → `x`,單獨的 `<worktree>` → `.` */
|
|
101
|
+
const relativeToWorktree = (s) => s.replace(/[^\s'"=]*\/\.agentflowctl\/worktrees\/[^/\s'"]+(\/)?/g, (_m, slash) => (slash ? "" : "."));
|
|
102
|
+
/** 精簡顯示的工具內容:只留第一行,並註明原本有幾行 */
|
|
103
|
+
function compactDetail(detail) {
|
|
104
|
+
const lines = relativeToWorktree(detail.trim()).split("\n");
|
|
105
|
+
const first = clip(lines[0], 200);
|
|
106
|
+
return lines.length > 1 ? `${first} …(共 ${lines.length} 行)` : first;
|
|
107
|
+
}
|
|
108
|
+
/** 各家 CLI 執行 shell 指令的工具名稱:codex 的 shell、claude 的 Bash、gemini 的 run_shell_command */
|
|
109
|
+
const SHELL_TOOLS = new Set(["shell", "bash", "run_shell_command"]);
|
|
110
|
+
const isShellTool = (ev) => ev.kind === "tool" && SHELL_TOOLS.has(ev.name.toLowerCase());
|
|
111
|
+
function renderEvent(ev, full, lastText) {
|
|
100
112
|
switch (ev.kind) {
|
|
101
113
|
case "text":
|
|
102
114
|
return `💬 ${indent(ev.text.trim())}`;
|
|
103
|
-
case "tool":
|
|
104
|
-
|
|
115
|
+
case "tool": {
|
|
116
|
+
const detail = ev.detail?.trim();
|
|
117
|
+
if (!detail)
|
|
118
|
+
return `🔧 ${ev.name}`;
|
|
119
|
+
return `🔧 ${ev.name}: ${full ? indent(detail) : compactDetail(detail)}`;
|
|
120
|
+
}
|
|
105
121
|
case "usage":
|
|
106
122
|
return `📊 用量 input ${ev.inputTokens ?? "?"} / output ${ev.outputTokens ?? "?"} tokens`;
|
|
107
|
-
case "done":
|
|
108
|
-
|
|
123
|
+
case "done": {
|
|
124
|
+
const summary = ev.summary?.trim();
|
|
125
|
+
// 最後一則回覆通常就是 summary,精簡模式不再重印一次
|
|
126
|
+
const show = summary && (full || summary !== lastText);
|
|
127
|
+
return `🏁 ${ev.ok ? "完成" : "失敗"}${show ? `:${indent(summary)}` : ""}`;
|
|
128
|
+
}
|
|
109
129
|
}
|
|
110
130
|
}
|
|
111
|
-
|
|
112
|
-
export function renderLog(text, file = "") {
|
|
131
|
+
function analyzeLog(text, full) {
|
|
113
132
|
const { header, footer, body, stderr } = parseLog(text);
|
|
114
|
-
const
|
|
133
|
+
const lines = [];
|
|
115
134
|
const errors = [];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
: "(沒有檔頭)";
|
|
119
|
-
out.push(`${logSeq(file) ? `#${logSeq(file)} ` : ""}${title}`);
|
|
120
|
-
out.push(`開始 ${localTime(header?.startedAt)} ` +
|
|
121
|
-
(footer ? `結束 ${localTime(footer.endedAt)} 結束碼 ${footer.code} ${footer.ok ? "✓ 成功" : "✗ 失敗"}` : "… 沒有結束紀錄(還在執行或被中斷)"));
|
|
122
|
-
if (file)
|
|
123
|
-
out.push(`檔案 ${file}`);
|
|
124
|
-
out.push("");
|
|
135
|
+
let lastText;
|
|
136
|
+
let doneSummary;
|
|
125
137
|
const adapter = header?.adapter ? ADAPTERS[header.adapter] : undefined;
|
|
126
138
|
if (!adapter || header?.agent === CMD_AGENT) {
|
|
127
|
-
|
|
139
|
+
lines.push(...body);
|
|
140
|
+
lastText = body.slice(-15).join("\n").trim() || undefined;
|
|
128
141
|
}
|
|
129
142
|
else {
|
|
130
143
|
let skipped = 0;
|
|
144
|
+
// 精簡模式下連續的 shell 指令(多半是讀檔、搜尋)收成一行,只留數量
|
|
145
|
+
let shells = 0;
|
|
146
|
+
const flushShells = () => {
|
|
147
|
+
if (shells)
|
|
148
|
+
lines.push(`🔧 shell 指令 ×${shells}(--full 查看)`);
|
|
149
|
+
shells = 0;
|
|
150
|
+
};
|
|
131
151
|
for (const line of body) {
|
|
132
152
|
if (!line.trim())
|
|
133
153
|
continue;
|
|
134
154
|
const events = adapter.parse(line);
|
|
135
155
|
if (events.length) {
|
|
136
156
|
for (const ev of events) {
|
|
137
|
-
|
|
157
|
+
if (!full && isShellTool(ev)) {
|
|
158
|
+
shells++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (ev.kind !== "usage")
|
|
162
|
+
flushShells();
|
|
163
|
+
const s = renderEvent(ev, full, lastText);
|
|
138
164
|
if (s)
|
|
139
|
-
|
|
165
|
+
lines.push(s);
|
|
166
|
+
if (ev.kind === "text")
|
|
167
|
+
lastText = ev.text.trim();
|
|
168
|
+
if (ev.kind === "done")
|
|
169
|
+
doneSummary = ev.summary?.trim() || undefined;
|
|
140
170
|
if (ev.kind === "done" && !ev.ok)
|
|
141
171
|
errors.push(`agent 回報失敗${ev.summary ? `:${indent(ev.summary.trim())}` : ""}`);
|
|
142
172
|
}
|
|
@@ -144,7 +174,8 @@ export function renderLog(text, file = "") {
|
|
|
144
174
|
}
|
|
145
175
|
const json = tryJson(line);
|
|
146
176
|
if (!json) {
|
|
147
|
-
|
|
177
|
+
flushShells();
|
|
178
|
+
lines.push(`📄 ${line}`);
|
|
148
179
|
continue;
|
|
149
180
|
}
|
|
150
181
|
const err = findError(json);
|
|
@@ -152,22 +183,53 @@ export function renderLog(text, file = "") {
|
|
|
152
183
|
skipped++;
|
|
153
184
|
continue;
|
|
154
185
|
}
|
|
155
|
-
|
|
186
|
+
flushShells();
|
|
187
|
+
lines.push(`${err.fatal ? "❌" : "⚠️ "} ${indent(err.message)}`);
|
|
156
188
|
if (err.fatal)
|
|
157
189
|
errors.push(`錯誤事件:${indent(err.message)}`);
|
|
158
190
|
}
|
|
191
|
+
flushShells();
|
|
159
192
|
if (skipped)
|
|
160
|
-
|
|
193
|
+
lines.push(`(另有 ${skipped} 行其他事件未顯示,可用 --raw 查看)`);
|
|
161
194
|
}
|
|
162
195
|
if (footer && footer.code !== 0)
|
|
163
196
|
errors.unshift(`結束碼 ${footer.code}`);
|
|
164
197
|
if (footer && !footer.ok && !errors.length)
|
|
165
198
|
errors.push("執行未通過(沒有更多錯誤訊息)");
|
|
166
199
|
const stderrText = stderr.length ? `stderr:\n ${indent(clip(stderr.join("\n"), 4000))}` : undefined;
|
|
200
|
+
return { header, footer, lines, errors, stderrText, lastText, doneSummary };
|
|
201
|
+
}
|
|
202
|
+
const logTitle = (header) => header
|
|
203
|
+
? `${header.stage} / ${header.step} / ${header.agent}${header.adapter && header.adapter !== header.agent ? `(adapter ${header.adapter})` : ""}`
|
|
204
|
+
: "(沒有檔頭)";
|
|
205
|
+
/** 把一份 log 轉成人看得懂的版本;最後附上錯誤整理,讓失敗原因一眼可見。預設精簡工具內容,opts.full 時完整顯示 */
|
|
206
|
+
export function renderLog(text, file = "", opts = {}) {
|
|
207
|
+
const { header, footer, lines, errors, stderrText } = analyzeLog(text, opts.full ?? false);
|
|
208
|
+
const out = [];
|
|
209
|
+
out.push(`${logSeq(file) ? `#${logSeq(file)} ` : ""}${logTitle(header)}`);
|
|
210
|
+
out.push(`開始 ${localTime(header?.startedAt)} ` +
|
|
211
|
+
(footer ? `結束 ${localTime(footer.endedAt)} 結束碼 ${footer.code} ${footer.ok ? "✓ 成功" : "✗ 失敗"}` : "… 沒有結束紀錄(還在執行或被中斷)"));
|
|
212
|
+
if (file)
|
|
213
|
+
out.push(`檔案 ${file}`);
|
|
214
|
+
out.push("", ...lines);
|
|
167
215
|
if (errors.length)
|
|
168
216
|
out.push("", "── 錯誤 ──", ...errors, ...(stderrText ? [stderrText] : []));
|
|
169
217
|
else if (stderrText)
|
|
170
218
|
out.push("", "── 其他輸出 ──", stderrText);
|
|
171
219
|
return out.join("\n");
|
|
172
220
|
}
|
|
221
|
+
/** 一份 log 的結果摘要:給 run 停下時直接印在終端機,不用再另外查 log */
|
|
222
|
+
export function summarizeLog(text) {
|
|
223
|
+
const a = analyzeLog(text, false);
|
|
224
|
+
const reply = a.doneSummary ?? a.lastText;
|
|
225
|
+
const meta = reply ? parseResultMeta(reply) ?? (a.lastText ? parseResultMeta(a.lastText) : undefined) : undefined;
|
|
226
|
+
const failed = a.footer ? !a.footer.ok : false;
|
|
227
|
+
return {
|
|
228
|
+
title: logTitle(a.header),
|
|
229
|
+
ok: a.footer?.ok,
|
|
230
|
+
meta,
|
|
231
|
+
text: meta ? undefined : reply && clip(reply, 600),
|
|
232
|
+
errors: failed ? [...a.errors, ...(a.stderrText ? [a.stderrText] : [])] : [],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
173
235
|
//# sourceMappingURL=logs.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Agent 回覆結尾的 <result> 中繼資料(格式定義在各 prompt 的 <reply_format>) */
|
|
3
|
+
export const ResultMeta = z.object({
|
|
4
|
+
status: z.enum(["done", "blocked"]),
|
|
5
|
+
summary: z.string().default(""),
|
|
6
|
+
filesChanged: z.array(z.string()).default([]),
|
|
7
|
+
concerns: z.string().default(""),
|
|
8
|
+
});
|
|
9
|
+
const tagText = (xml, tag) => xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim();
|
|
10
|
+
/** 取回覆中最後一個 <result> 區塊;沒有或格式不合時回傳 undefined,不影響關卡判斷 */
|
|
11
|
+
export function parseResultMeta(text) {
|
|
12
|
+
const block = [...text.matchAll(/<result>([\s\S]*?)<\/result>/g)].at(-1)?.[1];
|
|
13
|
+
if (block === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
const files = tagText(block, "files_changed") ?? "";
|
|
16
|
+
const parsed = ResultMeta.safeParse({
|
|
17
|
+
status: tagText(block, "status"),
|
|
18
|
+
summary: tagText(block, "summary"),
|
|
19
|
+
filesChanged: [...files.matchAll(/<file>([\s\S]*?)<\/file>/g)].map((m) => m[1].trim()).filter(Boolean),
|
|
20
|
+
concerns: tagText(block, "concerns"),
|
|
21
|
+
});
|
|
22
|
+
return parsed.success ? parsed.data : undefined;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=resultMeta.js.map
|
package/dist/runner.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
3
|
import { ADAPTERS } from "./agents/index.js";
|
|
5
4
|
import { projectRoot, runDir } from "./paths.js";
|
|
6
5
|
import { config } from "./config.js";
|
|
7
6
|
import { CMD_AGENT, footerLine, headerLine, logSeq, STDERR_MARK } from "./logs.js";
|
|
8
7
|
import { exec, execShell } from "./proc.js";
|
|
9
8
|
import { tail } from "./util.js";
|
|
9
|
+
import { parseResultMeta } from "./resultMeta.js";
|
|
10
10
|
/**
|
|
11
11
|
* 判斷 agent 失敗是否因為方案額度或速率限制。
|
|
12
12
|
* 各家的錯誤訊息會隨版本變動,這裡用寬鬆的樣式比對,且只在執行失敗時才檢查,避免誤判正常輸出。
|
|
@@ -22,28 +22,7 @@ const QUOTA_PATTERNS = [
|
|
|
22
22
|
/\b429\b/,
|
|
23
23
|
];
|
|
24
24
|
export const isQuotaError = (text) => QUOTA_PATTERNS.some((p) => p.test(text));
|
|
25
|
-
|
|
26
|
-
export const ResultMeta = z.object({
|
|
27
|
-
status: z.enum(["done", "blocked"]),
|
|
28
|
-
summary: z.string().default(""),
|
|
29
|
-
filesChanged: z.array(z.string()).default([]),
|
|
30
|
-
concerns: z.string().default(""),
|
|
31
|
-
});
|
|
32
|
-
const tagText = (xml, tag) => xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim();
|
|
33
|
-
/** 取回覆中最後一個 <result> 區塊;沒有或格式不合時回傳 undefined,不影響關卡判斷 */
|
|
34
|
-
export function parseResultMeta(text) {
|
|
35
|
-
const block = [...text.matchAll(/<result>([\s\S]*?)<\/result>/g)].at(-1)?.[1];
|
|
36
|
-
if (block === undefined)
|
|
37
|
-
return undefined;
|
|
38
|
-
const files = tagText(block, "files_changed") ?? "";
|
|
39
|
-
const parsed = ResultMeta.safeParse({
|
|
40
|
-
status: tagText(block, "status"),
|
|
41
|
-
summary: tagText(block, "summary"),
|
|
42
|
-
filesChanged: [...files.matchAll(/<file>([\s\S]*?)<\/file>/g)].map((m) => m[1].trim()).filter(Boolean),
|
|
43
|
-
concerns: tagText(block, "concerns"),
|
|
44
|
-
});
|
|
45
|
-
return parsed.success ? parsed.data : undefined;
|
|
46
|
-
}
|
|
25
|
+
export { parseResultMeta, ResultMeta } from "./resultMeta.js";
|
|
47
26
|
function appendLog(file, text) {
|
|
48
27
|
mkdirSync(dirname(file), { recursive: true });
|
|
49
28
|
appendFileSync(file, text.endsWith("\n") ? text : `${text}\n`);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { summarizeLog } from "./logs.js";
|
|
3
|
+
const pad = (s) => s.split("\n").join("\n ");
|
|
4
|
+
const clip = (s, max) => (s.length <= max ? s : `${s.slice(0, max)}…`);
|
|
5
|
+
/** 一份 log 的結果:標題、<result> 的摘要與疑慮;失敗時附上錯誤整理 */
|
|
6
|
+
function outcomeLines(entry, text) {
|
|
7
|
+
const s = summarizeLog(text);
|
|
8
|
+
const mark = s.ok === undefined ? "…" : s.ok ? "✓" : "✗";
|
|
9
|
+
const out = [` #${entry.seq} ${s.title} ${mark}`];
|
|
10
|
+
if (s.meta) {
|
|
11
|
+
if (s.meta.status === "blocked")
|
|
12
|
+
out.push(" 狀態:blocked");
|
|
13
|
+
if (s.meta.summary)
|
|
14
|
+
out.push(` 摘要:${pad(s.meta.summary)}`);
|
|
15
|
+
if (s.meta.concerns)
|
|
16
|
+
out.push(` 疑慮:${pad(s.meta.concerns)}`);
|
|
17
|
+
}
|
|
18
|
+
else if (s.text) {
|
|
19
|
+
out.push(` ${pad(s.text)}`);
|
|
20
|
+
}
|
|
21
|
+
for (const e of s.errors)
|
|
22
|
+
out.push(` ${pad(clip(e, 1500))}`);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* run 停下來(Ctrl-C、失敗、暫停、等待核准)時要直接印在終端機的內容:
|
|
27
|
+
* 最近一步的結果、未結交接事項,以及接下來可以執行的指令。完成的 run 不印。
|
|
28
|
+
*/
|
|
29
|
+
export function stopReport(i) {
|
|
30
|
+
const { run, logs } = i;
|
|
31
|
+
if (run.stage === "done")
|
|
32
|
+
return [];
|
|
33
|
+
const out = [];
|
|
34
|
+
const last = logs.at(-1);
|
|
35
|
+
const running = last && !last.footer ? last : undefined;
|
|
36
|
+
const finished = logs.filter((e) => e.footer);
|
|
37
|
+
const lastFailed = finished.filter((e) => !e.footer.ok).at(-1);
|
|
38
|
+
// 失敗時優先看失敗的那一步,其餘情況看最後完成的一步
|
|
39
|
+
const shown = run.stage === "failed" && lastFailed ? lastFailed : finished.at(-1);
|
|
40
|
+
if (running || shown) {
|
|
41
|
+
out.push("", "── 結果 ──");
|
|
42
|
+
if (running) {
|
|
43
|
+
const title = summarizeLog(i.read(running.file)).title;
|
|
44
|
+
out.push(` ${i.interrupted ? "中斷於" : "進行中"} #${running.seq} ${title}(沒有結束紀錄,resume 時會重跑這一步)`);
|
|
45
|
+
}
|
|
46
|
+
if (shown)
|
|
47
|
+
out.push(...outcomeLines(shown, i.read(shown.file)));
|
|
48
|
+
}
|
|
49
|
+
if (i.open.length) {
|
|
50
|
+
out.push("", "── 未結交接事項 ──");
|
|
51
|
+
for (const item of i.open)
|
|
52
|
+
out.push(` [${item.targetStage}] ${item.id} ${item.summary}(${item.status})`);
|
|
53
|
+
}
|
|
54
|
+
const cmd = (c, why) => ` ${c.padEnd(40)} ${why}`;
|
|
55
|
+
const actions = [];
|
|
56
|
+
if (run.stage === "failed") {
|
|
57
|
+
if (lastFailed)
|
|
58
|
+
actions.push(cmd(`agentflowctl logs ${run.id} ${lastFailed.seq}`, "看失敗步驟的完整 log"));
|
|
59
|
+
actions.push(cmd(`cd ${i.worktree}`, "需要時手動修正"));
|
|
60
|
+
actions.push(cmd(`agentflowctl resume ${run.id}`, `從 ${run.failedStage ?? "失敗的階段"} 重試`));
|
|
61
|
+
actions.push(cmd(`agentflowctl cancel ${run.id}`, "放棄這個 run"));
|
|
62
|
+
}
|
|
63
|
+
else if (run.stage === "paused") {
|
|
64
|
+
actions.push(cmd(`agentflowctl resume ${run.id}`, "額度恢復後接續"));
|
|
65
|
+
}
|
|
66
|
+
else if (run.stage === "awaiting_approval") {
|
|
67
|
+
actions.push(cmd(`less ${join(i.worktree, ".flow", "plan.md")}`, "檢視計畫"));
|
|
68
|
+
actions.push(cmd(`agentflowctl approve ${run.id}`, "核准並開始實作"));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (shown)
|
|
72
|
+
actions.push(cmd(`agentflowctl logs ${run.id} ${shown.seq}`, "看上一步的完整 log"));
|
|
73
|
+
actions.push(cmd(`agentflowctl resume ${run.id}`, `從 ${run.stage} 接續`));
|
|
74
|
+
actions.push(cmd(`agentflowctl cancel ${run.id}`, "放棄這個 run"));
|
|
75
|
+
}
|
|
76
|
+
out.push("", "── 下一步 ──", ...actions);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=stopReport.js.map
|
package/dist/tasks.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** 一個任務最多做兩件事:對應的驗收條件超過這個數量就要再拆 */
|
|
2
|
+
export const MAX_TASK_ACCEPTANCE = 2;
|
|
1
3
|
/**
|
|
2
4
|
* 檢查任務清單並依相依關係排序(Kahn 演算法)。
|
|
3
5
|
* 回傳排序後的任務,或回傳一段可以直接回饋給 Agent 的錯誤說明。
|
|
@@ -12,6 +14,9 @@ export function orderTasks(tasks, acceptanceIds) {
|
|
|
12
14
|
}
|
|
13
15
|
const covered = new Set();
|
|
14
16
|
for (const t of tasks) {
|
|
17
|
+
if (t.acceptance.length > MAX_TASK_ACCEPTANCE) {
|
|
18
|
+
errors.push(`${t.id} 對應 ${t.acceptance.length} 條驗收條件,一個任務最多 ${MAX_TASK_ACCEPTANCE} 條,請拆成更小的任務`);
|
|
19
|
+
}
|
|
15
20
|
for (const d of t.dependsOn)
|
|
16
21
|
if (!ids.has(d))
|
|
17
22
|
errors.push(`${t.id} 相依的 ${d} 不存在`);
|
package/package.json
CHANGED
package/prompts/plan-fix.md
CHANGED
package/prompts/plan-review.md
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
<review_focus>
|
|
34
34
|
1. **需求覆蓋**:規格是否完整涵蓋原始需求?有沒有遺漏、誤解,或加入需求沒要求的範圍?
|
|
35
|
-
2.
|
|
36
|
-
3.
|
|
35
|
+
2. **驗收條件**:每一條是否具體、可以用自動化測試驗證,而且只描述一個行為?把多個行為寫在同一條的,要求拆開。有沒有重要的邊界情況或錯誤處理沒被列入?
|
|
36
|
+
3. **任務拆解**:每個任務是否只做一件事(最多兩件),小到一次 TDD 循環就能完成,而且能寫出「實作前會失敗」的測試?任務太大、一次要動很多檔案或驗證很多行為的,要求拆成更小的任務。相依順序是否合理?
|
|
37
37
|
4. **技術方向**:是否符合專案既有的架構與慣例?有沒有更簡單的做法,或明顯的風險?
|
|
38
38
|
|
|
39
39
|
措辭、格式這類不影響實作結果的小問題,不需要要求修改。
|
package/prompts/plan.md
CHANGED
|
@@ -46,7 +46,10 @@
|
|
|
46
46
|
</output_format>
|
|
47
47
|
|
|
48
48
|
<guidelines>
|
|
49
|
-
-
|
|
49
|
+
- 一個任務只做一件事,最多兩件:`acceptance` 最多列兩條驗收條件,超過就拆成多個任務(程式會檢查,超過會被退回)。
|
|
50
|
+
- 每個任務是一個可獨立測試的垂直切片,小到一次 TDD 循環就能完成;只動少數幾個檔案,測試只驗證一兩個行為。
|
|
51
|
+
- `title` 用一句話說出這件事;需要用「並且」「以及」串起來的,就是兩個任務。
|
|
52
|
+
- `description` 寫清楚要動哪些檔案、測試要驗證哪個行為,以及這個任務不做什麼。
|
|
50
53
|
- 每個任務都必須能寫出「在實作前會失敗」的測試;純設定或重構類工作請併入相關任務。
|
|
51
54
|
- 測試檔名必須符合正規表示式 `{{testPattern}}`。
|
|
52
55
|
- 每一條驗收條件都至少要有一個任務負責;`dependsOn` 不可有循環。
|
package/prompts/spec.md
CHANGED
|
@@ -28,6 +28,13 @@
|
|
|
28
28
|
4. 撰寫 .flow/acceptance.json,每一條驗收條件都必須能用自動化測試驗證。
|
|
29
29
|
</steps>
|
|
30
30
|
|
|
31
|
+
<guidelines>
|
|
32
|
+
- 驗收條件要寫細:一條只描述一個可觀察的行為(一個輸入或情境,對應一個預期結果)。
|
|
33
|
+
- 描述裡出現「並且」「同時」「以及」,或同時涵蓋成功與失敗路徑時,拆成多條。
|
|
34
|
+
- 邊界情況與錯誤處理各自獨立成一條,不要附在正常路徑那一條裡。
|
|
35
|
+
- 寧可多幾條小的,也不要少數幾條大的;後面每個任務最多只能對應兩條驗收條件。
|
|
36
|
+
</guidelines>
|
|
37
|
+
|
|
31
38
|
<output_format>
|
|
32
39
|
.flow/acceptance.json 的格式:
|
|
33
40
|
|