agentflowctl 0.9.0 → 0.10.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 +9 -5
- package/dist/cli.js +3 -1
- package/dist/engine.js +138 -34
- package/dist/roles.js +6 -3
- package/dist/schemas.js +4 -1
- package/package.json +1 -1
- package/prompts/task-review.md +77 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
讓 Claude Code、Codex、Gemini CLI(或任何 agent CLI)在同一條流程裡輪流寫規格、寫計畫、寫測試、寫實作、互相審查、互相修正,一路做到開 PR。
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
需求 → spec → plan ⇄ plan_review ⇄ plan_fix →(僵持時)仲裁 → implement
|
|
6
|
+
需求 → spec → plan ⇄ plan_review ⇄ plan_fix →(僵持時)仲裁 → implement(每個任務:測試 A → 實作 B → 任務審查 ⇄ 修正 → 任務驗證 ⇄ 修正)→ verify ⇄ fix → review ⇄ fix → pr
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
從需求到 PR 全程由程式推進。每個步驟都是一次獨立的 agent 執行,用 `.flow/` 裡的檔案交接。是否通過一律由程式檢查:跑測試、比對 git diff、驗證 JSON。兩家模型就能完整運作;有第三家時,仲裁會交給沒參與討論的那一家。
|
|
@@ -120,7 +120,7 @@ agentflowctl clean --all # 清掉所有已結束的 run 與中斷留
|
|
|
120
120
|
| `--manual-plan` | 計畫通過審查後進入 `awaiting_approval`,等 `approve` 才開始實作 |
|
|
121
121
|
| `-v` / `--verbose` | 執行時印出 agent 的文字、工具呼叫與專案指令;`run`、`resume`、`approve` 都適用,也可設 `AGENTFLOWCTL_VERBOSE=1` |
|
|
122
122
|
|
|
123
|
-
`status`
|
|
123
|
+
`status` 會列出任務。進行中的任務會標出目前的步驟:🧪 寫測試、🛠️ 寫實作、👀 任務審查、🔍 任務驗證、🩹 任務修正。
|
|
124
124
|
|
|
125
125
|
### 清除 worktree
|
|
126
126
|
|
|
@@ -268,7 +268,7 @@ run 因 Ctrl-C、失敗、額度暫停或等待核准而停下時,終端機會
|
|
|
268
268
|
| `defaultModels` | `{}` | 依 `claude`、`codex`、`gemini` adapter 指定全域預設 model;agent 的 `model` 優先,兩者都沒設時使用各 CLI 的預設。`command` adapter 不套用 |
|
|
269
269
|
| `fixStrategy` | `ring` | `ring`:審查意見隨機交給審查者以外的一家;`author`:交回最後作者 |
|
|
270
270
|
| `tddSplit` | `true` | 測試與實作是否分開 |
|
|
271
|
-
| `reviewQuorum` | `1` | 程式碼需要幾位不同審查者都 `approve
|
|
271
|
+
| `reviewQuorum` | `1` | 程式碼需要幾位不同審查者都 `approve`(任務審查與最後的程式碼審查都適用) |
|
|
272
272
|
| `planReviewQuorum` | `1` | 計畫需要幾位不同審查者都 `approve` |
|
|
273
273
|
| `planArbiter` | `true` | 計畫審查僵持時交付仲裁。關掉之後,僵持會直接讓 run 失敗 |
|
|
274
274
|
| `tieBreak` | `proceed` | 兩家仲裁意見分歧時:`proceed` 繼續並記錄爭議;`stop` 停下 |
|
|
@@ -337,7 +337,7 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
337
337
|
| 兩家 | 兩家各自在全新 context 裡判斷 | 都核准就繼續;都不核准就依裁決意見修訂並重新審查;分歧依 `tieBreak` |
|
|
338
338
|
| 一家 | 同一家 | 由它自己仲裁 |
|
|
339
339
|
|
|
340
|
-
兩家時的仲裁是雙盲的。仲裁者只看計畫,以及一份不含審查者名稱的爭議清單(`.flow/dispute.md`)。爭議清單用 `<issue>` 包住每則意見;給修訂者的 `.flow/feedback.md` 則用 `<opinion author="…">` 包住每位審查者的意見,避免意見內文與外層結構混淆。帶有名稱的審查紀錄移到 worktree 以外。`tieBreak` 預設 `proceed
|
|
340
|
+
兩家時的仲裁是雙盲的。仲裁者只看計畫,以及一份不含審查者名稱的爭議清單(`.flow/dispute.md`)。爭議清單用 `<issue>` 包住每則意見;給修訂者的 `.flow/feedback.md` 則用 `<opinion author="…">` 包住每位審查者的意見,避免意見內文與外層結構混淆。帶有名稱的審查紀錄移到 worktree 以外。`tieBreak` 預設 `proceed`,因為後面還有測試紅燈、綠燈、任務審查、verify 與程式碼審查。
|
|
341
341
|
|
|
342
342
|
計畫定案或仲裁最終停止時,裁決與每位仲裁者的理由附在 `plan.md` 最後的「仲裁紀錄」。需再修訂時,裁決理由寫進 `.flow/feedback.md`,供修訂者處理;重新審查會從第一輪計數。原始審查與每輪仲裁紀錄在 `.agentflowctl/runs/<id>/reviews/`。兩家都要求修改時不因仲裁輪數而直接失敗;整個 run 仍受 `maxAgentRuns` 限制。
|
|
343
343
|
|
|
@@ -355,6 +355,9 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
355
355
|
| 人工確認 | 你(只有 `--manual-plan`) | `agentflowctl approve` | — |
|
|
356
356
|
| implement 紅燈 | 洗牌輪流,每家各一次 | 有測試變更,而且測試執行後失敗;規格與計畫檔沒有被修改 | 還原並重試 |
|
|
357
357
|
| implement 綠燈 | 測試作者以外隨機一位 | 測試檔與規格、計畫檔都沒有被修改,而且測試通過 | 還原,或帶著輸出重試 |
|
|
358
|
+
| implement 任務審查 | 作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve` 這個任務的變更 | 進入任務修正 |
|
|
359
|
+
| implement 任務驗證 | — | `install` 與所有 `checks` 通過,才換下一個任務 | 進入任務修正 |
|
|
360
|
+
| implement 任務修正 | 與 fix 相同 | 與 fix 相同;修完重新任務審查、任務驗證 | 還原並重試 |
|
|
358
361
|
| verify | — | `install` 與所有 `checks` 通過 | 交回作者修正 |
|
|
359
362
|
| fix | verify 失敗交回作者;審查意見依 `fixStrategy` | 沒有刪除測試檔,也沒有修改規格與計畫檔 | 還原並重試 |
|
|
360
363
|
| review | 作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve`(審查者對程式碼與規格、計畫檔的修改一律還原) | 依 `fixStrategy` 交給他人修正 |
|
|
@@ -363,11 +366,12 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
363
366
|
驗收條件寫在 `.flow/acceptance.json`(`AC-1`…),任務寫在 `.flow/tasks.json`(`T-1`…)。
|
|
364
367
|
每個任務的寫測試與寫實作 prompt 只帶入該任務對應的驗收條件;agent 優先讀任務與相關程式碼,遇到資訊不足或矛盾才查規格、計畫的相關段落。agent 可先跑相關測試,紅燈與完整測試仍由外部流程執行與判定,減少重複讀取文件和全套測試輸出所用的 token。
|
|
365
368
|
計畫定案後(實作、修正、程式碼審查)不可修改 `.flow/` 裡的規格與計畫檔(`spec.md`、`acceptance.json`、`plan.md`、`tasks.json`、`tasks.ordered.json`);實作與修正時被改就還原並重試,因為寫出的程式碼可能依賴被改過的規格,必須重寫;審查者只交出審查結果,修改直接還原即可,不必重跑審查。對規格有疑慮要寫進交接事項。
|
|
369
|
+
每個任務綠燈後先做任務審查:審查者只看這個任務寫測試前到目前的 diff(`.flow/diff.patch`)與這個任務的驗收條件,審查紀錄存成 `.flow/review-<任務>-<審查者>.json`。審查者避開最後實作者;有第三家可選時,也優先避開測試作者,只有兩家或審查人數不足時才由測試作者審查。任務審查不要求結清所有交接事項(可能屬於後面的任務),最後的程式碼審查才會擋。審查通過後執行任務驗證(與 verify 相同的 `install` 與 `checks`)。審查要求修改或驗證失敗都進入任務修正,修正者的挑法與 fix 相同,修完回到任務審查再驗證。審查執行或任務修正若先失敗、後成功,會清除各自的連續失敗次數。所有任務完成後,仍照原本流程對整份變更跑一次 verify 與程式碼審查。
|
|
366
370
|
程式碼審查仍逐條核對所有驗收條件,但只在 `review.json` 列出未通過或其他重要問題;先看 diff 與相關檔案,驗收條件不清楚時才查規格。修正階段先依 `feedback.md` 定位問題並執行相關檢查,完整檢查仍由後續 verify 執行,以減少反覆讀取完整文件與測試輸出。
|
|
367
371
|
|
|
368
372
|
## Prompt 結構
|
|
369
373
|
|
|
370
|
-
每個階段的 prompt
|
|
374
|
+
每個階段的 prompt 都有專屬角色:需求分析師、軟體架構師、計畫審查者、計畫修訂者、中立仲裁者、測試工程師、實作工程師、任務審查者、除錯工程師、程式碼審查者。內容用 XML 標籤分段:`<role>`、`<context>`、`<inputs>`、`<steps>`、`<constraints>`、`<output_format>`、`<reply_format>`。
|
|
371
375
|
|
|
372
376
|
Agent 的最後回覆要附上 XML 中繼資料:
|
|
373
377
|
|
package/dist/cli.js
CHANGED
|
@@ -88,6 +88,8 @@ async function resolveCycle(flag) {
|
|
|
88
88
|
throw new Error(`設定的 agent(${defined.join("、")})都沒有偵測到已安裝的 CLI,可用 agentflowctl doctor 檢查`);
|
|
89
89
|
return found;
|
|
90
90
|
}
|
|
91
|
+
/** status 任務清單中,進行中任務的標記 */
|
|
92
|
+
const TASK_PHASE_MARK = { tests: "🧪", code: "🛠️ ", review: "👀", verify: "🔍", fix: "🩹" };
|
|
91
93
|
const program = new Command()
|
|
92
94
|
.name("agentflowctl")
|
|
93
95
|
.description("在專案資料夾內執行的 Agent 開發流程:規格 → 計畫 → TDD 實作 → 驗證 → 審查 → PR")
|
|
@@ -228,7 +230,7 @@ program
|
|
|
228
230
|
console.log("\n任務");
|
|
229
231
|
tasks.data.forEach((t, i) => {
|
|
230
232
|
const active = i === run.taskIndex && run.stage === "implement";
|
|
231
|
-
const mark = i < run.taskIndex ? "✅" : active ?
|
|
233
|
+
const mark = i < run.taskIndex ? "✅" : active ? TASK_PHASE_MARK[run.taskPhase] : "⬜";
|
|
232
234
|
console.log(` ${mark} ${t.id} ${t.title}`);
|
|
233
235
|
});
|
|
234
236
|
});
|
package/dist/engine.js
CHANGED
|
@@ -433,6 +433,12 @@ async function implementStage(run) {
|
|
|
433
433
|
if (!acceptance.ok)
|
|
434
434
|
throw new Error(acceptance.error);
|
|
435
435
|
const acceptanceJson = JSON.stringify(taskAcceptance(task, acceptance.data), null, 2);
|
|
436
|
+
if (run.taskPhase === "review")
|
|
437
|
+
return taskReviewStep(run, task, progress, taskJson, acceptanceJson);
|
|
438
|
+
if (run.taskPhase === "verify")
|
|
439
|
+
return taskVerifyStep(run, task, progress);
|
|
440
|
+
if (run.taskPhase === "fix")
|
|
441
|
+
return taskFixStep(run, task, progress);
|
|
436
442
|
const agents = taskAgents(run.cycle, run.taskIndex, cfg.tddSplit, run.id);
|
|
437
443
|
// ── 紅燈:只寫測試,而且測試必須失敗 ──
|
|
438
444
|
if (run.taskPhase === "tests") {
|
|
@@ -471,7 +477,7 @@ async function implementStage(run) {
|
|
|
471
477
|
}
|
|
472
478
|
writeFileSync(flowFile(run, "red-output.txt"), red.output);
|
|
473
479
|
info(run, `🔴 [${progress}] 測試如預期失敗`);
|
|
474
|
-
return { ...succeed(run, key, "implement"), taskPhase: "code", testsCommit: commit, lastTestsAuthor: testsAuthor };
|
|
480
|
+
return { ...succeed(run, key, "implement"), taskPhase: "code", taskBase: before, testsCommit: commit, lastTestsAuthor: testsAuthor };
|
|
475
481
|
}
|
|
476
482
|
// ── 綠燈:實作到測試通過,而且不可動測試 ──
|
|
477
483
|
const key = `${task.id}:code`;
|
|
@@ -506,28 +512,83 @@ async function implementStage(run) {
|
|
|
506
512
|
await resetTo(repo, testsCommit);
|
|
507
513
|
return retry(run, key, handoffError, "implement");
|
|
508
514
|
}
|
|
509
|
-
info(run, `🟢 [${progress}]
|
|
515
|
+
info(run, `🟢 [${progress}] 測試通過`);
|
|
516
|
+
return { ...succeed(run, key, "implement"), taskPhase: "review", lastWriter: codeAuthor };
|
|
517
|
+
}
|
|
518
|
+
// ── 任務審查:只看這個任務的變更與驗收條件 ──
|
|
519
|
+
async function taskReviewStep(run, task, progress, taskJson, acceptanceJson) {
|
|
520
|
+
const key = `${task.id}:review`;
|
|
521
|
+
const base = run.taskBase ?? (run.testsCommit && `${run.testsCommit}~1`);
|
|
522
|
+
if (!base)
|
|
523
|
+
throw new Error("缺少 taskBase,狀態不一致");
|
|
524
|
+
const result = await codeReview(run, {
|
|
525
|
+
base,
|
|
526
|
+
seed: `${run.id}:review:${task.id}:${run.attempts[key] ?? 0}`,
|
|
527
|
+
label: `[${progress}] 任務審查`,
|
|
528
|
+
step: `${task.id}-review`,
|
|
529
|
+
prompt: (reviewer, authors) => renderPrompt("task-review", { reviewer, authors, task: taskJson, acceptance: acceptanceJson }),
|
|
530
|
+
saveAs: (reviewer) => `review-${task.id}-${reviewer}.json`,
|
|
531
|
+
testAuthor: run.lastTestsAuthor,
|
|
532
|
+
// 未結交接事項可能屬於後面的任務,由最後的整體審查把關
|
|
533
|
+
gate: false,
|
|
534
|
+
runKey: `${task.id}:review-run`,
|
|
535
|
+
backTo: "implement",
|
|
536
|
+
});
|
|
537
|
+
if ("run" in result)
|
|
538
|
+
return result.run;
|
|
539
|
+
const reviewed = succeed(run, `${task.id}:review-run`, "implement");
|
|
540
|
+
if (!result.objector)
|
|
541
|
+
return { ...succeed(reviewed, key, "implement"), taskPhase: "verify" };
|
|
542
|
+
return {
|
|
543
|
+
...retry(reviewed, key, `任務審查要求修改:\n\n${result.issues.join("\n\n")}`, "implement"),
|
|
544
|
+
taskPhase: "fix",
|
|
545
|
+
fixSource: "review",
|
|
546
|
+
lastReviewer: result.objector,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
// ── 任務驗證:通過才進入下一個任務 ──
|
|
550
|
+
async function taskVerifyStep(run, task, progress) {
|
|
551
|
+
const key = `${task.id}:verify`;
|
|
552
|
+
info(run, `🔍 [${progress}] 執行驗證`);
|
|
553
|
+
const report = await runChecks(run, `${task.id}-`);
|
|
554
|
+
if (report)
|
|
555
|
+
return { ...retry(run, key, report, "implement"), taskPhase: "fix", fixSource: "verify" };
|
|
556
|
+
info(run, `✅ [${progress}] 完成`);
|
|
510
557
|
return {
|
|
511
558
|
...succeed(run, key, "implement"),
|
|
512
559
|
taskIndex: run.taskIndex + 1,
|
|
513
560
|
taskPhase: "tests",
|
|
561
|
+
taskBase: undefined,
|
|
514
562
|
testsCommit: undefined,
|
|
515
563
|
lastTestsAuthor: undefined,
|
|
516
|
-
lastWriter: codeAuthor,
|
|
517
564
|
};
|
|
518
565
|
}
|
|
519
|
-
|
|
520
|
-
|
|
566
|
+
// ── 任務修正:修完重新審查、驗證 ──
|
|
567
|
+
async function taskFixStep(run, task, progress) {
|
|
568
|
+
const result = await applyFix(run, {
|
|
569
|
+
seed: `${run.id}:fix:${task.id}:${run.attempts[`${task.id}:review`] ?? 0}`,
|
|
570
|
+
label: `[${progress}] `,
|
|
571
|
+
step: `${task.id}-fix`,
|
|
572
|
+
commitScope: `fix(${task.id})`,
|
|
573
|
+
key: `${task.id}:fix`,
|
|
574
|
+
backTo: "implement",
|
|
575
|
+
});
|
|
576
|
+
if ("run" in result)
|
|
577
|
+
return result.run;
|
|
578
|
+
return { ...succeed(run, `${task.id}:fix`, "implement"), taskPhase: "review", lastWriter: result.agent };
|
|
579
|
+
}
|
|
580
|
+
/** 執行 install 與所有 checks,結果寫入 verify.json;有失敗時回傳給修正者的報告 */
|
|
581
|
+
async function runChecks(run, stepPrefix = "") {
|
|
521
582
|
const cfg = loadRepoConfig();
|
|
522
583
|
const results = [];
|
|
523
|
-
const install = await runCommand(target(run,
|
|
584
|
+
const install = await runCommand(target(run, `${stepPrefix}install`, CMD_AGENT), cfg.install);
|
|
524
585
|
if (!install.ok) {
|
|
525
586
|
info(run, ` ✗ install${logHint(run, install.seq)}`);
|
|
526
587
|
results.push({ name: "install", ok: false, output: tail(install.output) });
|
|
527
588
|
}
|
|
528
589
|
else {
|
|
529
590
|
for (const check of cfg.checks) {
|
|
530
|
-
const r = await runCommand(target(run, check.name
|
|
591
|
+
const r = await runCommand(target(run, `${stepPrefix}${check.name}`, CMD_AGENT), check.cmd);
|
|
531
592
|
info(run, ` ${r.ok ? "✓" : "✗"} ${check.name}${r.ok ? "" : logHint(run, r.seq)}`);
|
|
532
593
|
results.push({ name: check.name, ok: r.ok, output: tail(r.output, 3000) });
|
|
533
594
|
}
|
|
@@ -535,11 +596,18 @@ async function verifyStage(run) {
|
|
|
535
596
|
writeFileSync(flowFile(run, "verify.json"), JSON.stringify(results, null, 2));
|
|
536
597
|
const failed = results.filter((r) => !r.ok);
|
|
537
598
|
if (failed.length === 0)
|
|
599
|
+
return undefined;
|
|
600
|
+
return failed.map((f) => `## ${f.name} 失敗\n\n\`\`\`\n${f.output}\n\`\`\``).join("\n\n");
|
|
601
|
+
}
|
|
602
|
+
async function verifyStage(run) {
|
|
603
|
+
info(run, "🔍 執行驗證");
|
|
604
|
+
const report = await runChecks(run);
|
|
605
|
+
if (!report)
|
|
538
606
|
return succeed(run, "verify", "review");
|
|
539
|
-
const report = failed.map((f) => `## ${f.name} 失敗\n\n\`\`\`\n${f.output}\n\`\`\``).join("\n\n");
|
|
540
607
|
return { ...retry(run, "verify", report, "fix"), fixSource: "verify" };
|
|
541
608
|
}
|
|
542
|
-
|
|
609
|
+
/** 修正驗證錯誤或審查意見;成功時回傳實際修正者,未通過時回傳重試後的 run */
|
|
610
|
+
async function applyFix(run, opts) {
|
|
543
611
|
const cfg = loadRepoConfig();
|
|
544
612
|
const source = run.fixSource ?? "verify";
|
|
545
613
|
const agent = fixAgent(run.cycle, {
|
|
@@ -547,55 +615,74 @@ async function fixStage(run) {
|
|
|
547
615
|
strategy: cfg.fixStrategy,
|
|
548
616
|
lastWriter: run.lastWriter,
|
|
549
617
|
lastReviewer: run.lastReviewer,
|
|
550
|
-
seed:
|
|
618
|
+
seed: opts.seed,
|
|
551
619
|
});
|
|
552
620
|
const why = source === "review" ? `依 ${run.lastReviewer ?? "reviewer"} 的審查意見` : "修正驗證錯誤";
|
|
553
|
-
info(run, `🩹 ${why}(${agent})`);
|
|
621
|
+
info(run, `🩹 ${opts.label}${why}(${agent})`);
|
|
554
622
|
const repo = worktreeDir(run.id);
|
|
555
623
|
const feedback = readFeedback(run);
|
|
556
624
|
const before = await headCommit(repo);
|
|
557
625
|
const snap = snapshotPlan(run, LOCKED_FILES);
|
|
558
|
-
const outcome = await agentStep(run, agent,
|
|
626
|
+
const outcome = await agentStep(run, agent, opts.step, renderPrompt("fix", { testPattern: cfg.testPattern }), {
|
|
559
627
|
kind: "write",
|
|
560
628
|
reset: async () => { await resetTo(repo, before); restorePlan(run, snap); },
|
|
561
629
|
});
|
|
562
630
|
const { r, agent: actual } = outcome;
|
|
563
631
|
const tampered = restorePlan(run, snap);
|
|
632
|
+
const again = (reason) => ({ run: retry(run, opts.key, reason, opts.backTo) });
|
|
564
633
|
if (!r.ok)
|
|
565
|
-
return
|
|
634
|
+
return again(`${feedback}\n\n(上次修正時 Agent 執行失敗:${r.summary})`);
|
|
566
635
|
if (tampered.length) {
|
|
567
636
|
await resetTo(repo, before);
|
|
568
|
-
return
|
|
637
|
+
return again(`${feedback}\n\n另外:${planTamperedMessage(tampered)}`);
|
|
569
638
|
}
|
|
570
|
-
await commitAll(repo,
|
|
639
|
+
await commitAll(repo, `${opts.commitScope}: ${why} [${actual}]`);
|
|
571
640
|
const testRe = new RegExp(cfg.testPattern);
|
|
572
641
|
const deleted = (await changedFiles(repo, before, await headCommit(repo), "D")).filter((f) => testRe.test(f));
|
|
573
642
|
if (deleted.length) {
|
|
574
643
|
await resetTo(repo, before);
|
|
575
|
-
return
|
|
644
|
+
return again(`${feedback}\n\n另外:不可刪除測試檔來讓檢查通過,已還原:${deleted.join(", ")}`);
|
|
576
645
|
}
|
|
577
646
|
const handoffError = finishHandoff(run, outcome, "writer");
|
|
578
647
|
if (handoffError) {
|
|
579
648
|
await resetTo(repo, before);
|
|
580
|
-
return
|
|
649
|
+
return again(handoffError);
|
|
581
650
|
}
|
|
651
|
+
return { agent: actual };
|
|
652
|
+
}
|
|
653
|
+
async function fixStage(run) {
|
|
654
|
+
const result = await applyFix(run, {
|
|
655
|
+
seed: `${run.id}:fix:${run.attempts.review ?? 0}`,
|
|
656
|
+
label: "",
|
|
657
|
+
step: "fix",
|
|
658
|
+
commitScope: "fix",
|
|
659
|
+
key: "fix",
|
|
660
|
+
backTo: "fix",
|
|
661
|
+
});
|
|
662
|
+
if ("run" in result)
|
|
663
|
+
return result.run;
|
|
582
664
|
// 修正者成為新的作者,下一輪審查會換成別人
|
|
583
|
-
return { ...to(run, "verify"), lastWriter:
|
|
665
|
+
return { ...to(run, "verify"), lastWriter: result.agent };
|
|
584
666
|
}
|
|
585
|
-
|
|
667
|
+
/**
|
|
668
|
+
* 由作者以外的審查小組審查 base 之後的變更;回傳第一位要求修改的審查者與所有意見,
|
|
669
|
+
* 審查本身未完成(執行失敗、格式錯誤、交接不合格)時回傳重試後的 run。
|
|
670
|
+
* gate 為 true 時,核准前必須結清所有程式碼類的未結交接事項。
|
|
671
|
+
*/
|
|
672
|
+
async function codeReview(run, opts) {
|
|
586
673
|
const cfg = loadRepoConfig();
|
|
587
674
|
const repo = worktreeDir(run.id);
|
|
588
|
-
const panel = reviewers(run.cycle, run.lastWriter, cfg.reviewQuorum,
|
|
589
|
-
writeFileSync(flowFile(run, "diff.patch"), await git(repo, "diff", `${
|
|
590
|
-
const authors = [...new Set((await git(repo, "log", "--format=%s", `${
|
|
675
|
+
const panel = reviewers(run.cycle, run.lastWriter, cfg.reviewQuorum, opts.seed, opts.testAuthor);
|
|
676
|
+
writeFileSync(flowFile(run, "diff.patch"), await git(repo, "diff", `${opts.base}...HEAD`));
|
|
677
|
+
const authors = [...new Set((await git(repo, "log", "--format=%s", `${opts.base}..HEAD`)).match(/\[[^\]]+\]$/gm) ?? [])]
|
|
591
678
|
.map((s) => s.slice(1, -1));
|
|
592
679
|
const issues = [];
|
|
593
|
-
let
|
|
680
|
+
let objector;
|
|
594
681
|
for (const [slot, reviewer] of panel.entries()) {
|
|
595
|
-
info(run, `👀
|
|
682
|
+
info(run, `👀 ${opts.label}(${reviewer})`);
|
|
596
683
|
rmSync(flowFile(run, "review.json"), { force: true });
|
|
597
684
|
const snap = snapshotPlan(run, LOCKED_FILES);
|
|
598
|
-
const outcome = await agentStep(run, reviewer,
|
|
685
|
+
const outcome = await agentStep(run, reviewer, opts.step, opts.prompt(reviewer, authors.join("、") || "未知"), {
|
|
599
686
|
kind: "review", slot, reset: async () => { await discardChanges(repo); restorePlan(run, snap); },
|
|
600
687
|
});
|
|
601
688
|
const { r } = outcome;
|
|
@@ -604,30 +691,47 @@ async function reviewStage(run) {
|
|
|
604
691
|
if (tampered.length)
|
|
605
692
|
info(run, ` ↩️ 已還原審查者修改的檔案:${tampered.join(", ")}`);
|
|
606
693
|
if (!r.ok)
|
|
607
|
-
return retry(run,
|
|
694
|
+
return { run: retry(run, opts.runKey, `Agent 執行失敗:${r.summary}`, opts.backTo) };
|
|
608
695
|
const review = readJsonFile(flowFile(run, "review.json"), ConsistentReviewResult);
|
|
609
696
|
if (!review.ok)
|
|
610
|
-
return retry(run,
|
|
611
|
-
const
|
|
697
|
+
return { run: retry(run, opts.runKey, review.error, opts.backTo) };
|
|
698
|
+
const gate = opts.gate ? { target: "code", verdict: review.data.verdict } : undefined;
|
|
699
|
+
const handoffError = finishHandoff(run, outcome, "reviewer", gate);
|
|
612
700
|
if (handoffError)
|
|
613
|
-
return retry(run,
|
|
614
|
-
renameSync(flowFile(run, "review.json"), flowFile(run,
|
|
701
|
+
return { run: retry(run, opts.runKey, handoffError, opts.backTo) };
|
|
702
|
+
renameSync(flowFile(run, "review.json"), flowFile(run, opts.saveAs(reviewer)));
|
|
615
703
|
if (review.data.verdict === "approve") {
|
|
616
704
|
info(run, ` ✓ ${reviewer} 核准`);
|
|
617
705
|
continue;
|
|
618
706
|
}
|
|
619
707
|
info(run, ` ✗ ${reviewer} 要求修改`);
|
|
620
|
-
|
|
708
|
+
objector ??= reviewer;
|
|
621
709
|
issues.push(opinion(reviewer, review.data.items
|
|
622
710
|
.filter((i) => i.status !== "met")
|
|
623
711
|
.map((i) => reviewIssue(i.criterion, i.status, i.note))));
|
|
624
712
|
}
|
|
625
|
-
|
|
713
|
+
return { objector, issues };
|
|
714
|
+
}
|
|
715
|
+
async function reviewStage(run) {
|
|
716
|
+
const result = await codeReview(run, {
|
|
717
|
+
base: run.baseBranch,
|
|
718
|
+
seed: `${run.id}:review:${run.attempts.review ?? 0}`,
|
|
719
|
+
label: "程式碼審查",
|
|
720
|
+
step: "review",
|
|
721
|
+
prompt: (reviewer, authors) => renderPrompt("review", { reviewer, authors }),
|
|
722
|
+
saveAs: (reviewer) => `review-${reviewer}.json`,
|
|
723
|
+
gate: true,
|
|
724
|
+
runKey: "review-run",
|
|
725
|
+
backTo: "review",
|
|
726
|
+
});
|
|
727
|
+
if ("run" in result)
|
|
728
|
+
return result.run;
|
|
729
|
+
if (!result.objector)
|
|
626
730
|
return succeed(run, "review", "pr");
|
|
627
731
|
return {
|
|
628
|
-
...retry(run, "review", `程式碼審查要求修改:\n\n${issues.join("\n\n")}`, "fix"),
|
|
732
|
+
...retry(run, "review", `程式碼審查要求修改:\n\n${result.issues.join("\n\n")}`, "fix"),
|
|
629
733
|
fixSource: "review",
|
|
630
|
-
lastReviewer:
|
|
734
|
+
lastReviewer: result.objector,
|
|
631
735
|
};
|
|
632
736
|
}
|
|
633
737
|
async function prStage(run) {
|
package/dist/roles.js
CHANGED
|
@@ -64,9 +64,12 @@ export function taskAgents(cycle, taskIndex, tddSplit, seed) {
|
|
|
64
64
|
const tests = taskBag(cycle, seed, Math.floor(taskIndex / n))[taskIndex % n];
|
|
65
65
|
return { tests, code: tddSplit ? pick(cycle, `${seed}:code:${taskIndex}`, [tests]) : tests };
|
|
66
66
|
}
|
|
67
|
-
/** 隨機挑出 quorum
|
|
68
|
-
export function reviewers(cycle, lastWriter, quorum, seed) {
|
|
69
|
-
const
|
|
67
|
+
/** 隨機挑出 quorum 位彼此不重複、而且不是最後作者的 reviewer;任務審查優先避開測試作者 */
|
|
68
|
+
export function reviewers(cycle, lastWriter, quorum, seed, testAuthor) {
|
|
69
|
+
const candidates = shuffled(cycle.filter((c) => c !== lastWriter), seed);
|
|
70
|
+
const preferred = testAuthor ? candidates.filter((c) => c !== testAuthor) : candidates;
|
|
71
|
+
const fallback = testAuthor ? candidates.filter((c) => c === testAuthor) : [];
|
|
72
|
+
const picked = [...preferred, ...fallback].slice(0, quorum);
|
|
70
73
|
return picked.length ? picked : [lastWriter ?? cycle[0]];
|
|
71
74
|
}
|
|
72
75
|
/**
|
package/dist/schemas.js
CHANGED
|
@@ -172,7 +172,10 @@ export const FlowRun = z.object({
|
|
|
172
172
|
/** 各關卡的連續失敗次數 */
|
|
173
173
|
attempts: z.record(z.string(), z.number()),
|
|
174
174
|
taskIndex: z.number().int().nonnegative(),
|
|
175
|
-
|
|
175
|
+
/** 目前任務進行到哪一步:寫測試 → 實作 → 審查 → 驗證,審查或驗證未通過時進入修正 */
|
|
176
|
+
taskPhase: z.enum(["tests", "code", "review", "verify", "fix"]),
|
|
177
|
+
/** 目前任務寫測試前的 commit,任務審查只看這之後的變更 */
|
|
178
|
+
taskBase: z.string().optional(),
|
|
176
179
|
testsCommit: z.string().optional(),
|
|
177
180
|
/** 目前任務的測試實際由誰撰寫(可能是代打) */
|
|
178
181
|
lastTestsAuthor: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<role>
|
|
2
|
+
你是任務審查者({{reviewer}}),負責在單一任務完成後、進入下一個任務前,獨立審查這個任務的變更。本任務的作者:{{authors}}。若你曾撰寫本任務的測試,仍須重新檢查測試是否真正驗證驗收條件;不要預設測試或實作正確。
|
|
3
|
+
</role>
|
|
4
|
+
|
|
5
|
+
<context>
|
|
6
|
+
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。這個任務的測試已由外部流程確認通過;完整的自動化檢查會在你審查之後才執行,所有任務完成後還有一次整體審查。
|
|
7
|
+
</context>
|
|
8
|
+
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本任務有關的待辦事項;與本任務無關的事項留給後續任務或整體審查。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
20
|
+
<inputs>
|
|
21
|
+
<task>
|
|
22
|
+
{{task}}
|
|
23
|
+
</task>
|
|
24
|
+
|
|
25
|
+
<acceptance>
|
|
26
|
+
{{acceptance}}
|
|
27
|
+
</acceptance>
|
|
28
|
+
|
|
29
|
+
- 本任務的變更:.flow/diff.patch(先看變更,再按需讀相關程式碼與測試)
|
|
30
|
+
- 規格與計畫:.flow/spec.md、.flow/plan.md(任務或驗收條件不清楚、互相矛盾時,才查相關段落)
|
|
31
|
+
</inputs>
|
|
32
|
+
|
|
33
|
+
<review_focus>
|
|
34
|
+
1. 逐條確認上面每個驗收條件是否真的被實作,而且有對應的測試真正驗證它(不是空洞的測試)。
|
|
35
|
+
2. 是否有明顯的錯誤、邊界情況遺漏、安全問題或效能問題。
|
|
36
|
+
3. 是否符合專案既有的架構與慣例,以及任務說明的範圍(沒有做到一半,也沒有做了其他任務的事)。
|
|
37
|
+
|
|
38
|
+
只審查本任務的變更;其他任務的驗收條件不在這次審查範圍內。先根據 diff 與驗收條件定位需要查閱的檔案,只在證據不足時讀取其他檔案。不必為了審查重跑全套檢查。
|
|
39
|
+
|
|
40
|
+
風格偏好與無關緊要的小問題不需要要求修改。
|
|
41
|
+
</review_focus>
|
|
42
|
+
|
|
43
|
+
<output_format>
|
|
44
|
+
寫入 .flow/review.json:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"verdict": "changes_requested",
|
|
49
|
+
"items": [
|
|
50
|
+
{ "criterion": "AC-1", "status": "not_met", "note": "src/form.tsx 缺少 API 失敗時的錯誤訊息,請在 catch 中顯示錯誤並補測試" }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `verdict`:逐條核對本任務的驗收條件後,全部通過且沒有嚴重問題時為 `approve`,否則為 `changes_requested`。
|
|
56
|
+
- `items` 只列未通過的驗收條件與額外發現的重要問題,每筆的 `status` 為 `not_met` 或 `partial`,`note` 請寫出具體位置與修正方向。
|
|
57
|
+
- `approve` 時 `items` 為空陣列;`changes_requested` 時至少要有一筆。兩者不一致會被視為格式錯誤並重新審查。
|
|
58
|
+
</output_format>
|
|
59
|
+
|
|
60
|
+
<constraints>
|
|
61
|
+
- 只能寫入 .flow/review.json 與 .flow/handoff-response.json,不可修改任何程式碼,其他變更都會被捨棄。
|
|
62
|
+
</constraints>
|
|
63
|
+
|
|
64
|
+
<reply_format>
|
|
65
|
+
完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
|
|
66
|
+
|
|
67
|
+
```xml
|
|
68
|
+
<result>
|
|
69
|
+
<status>done 或 blocked</status>
|
|
70
|
+
<summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
|
|
71
|
+
<files_changed>
|
|
72
|
+
<file>每個新增或修改的檔案路徑各一行</file>
|
|
73
|
+
</files_changed>
|
|
74
|
+
<concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
|
|
75
|
+
</result>
|
|
76
|
+
```
|
|
77
|
+
</reply_format>
|