agentflowctl 0.5.0 → 0.7.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 +46 -10
- package/dist/arbitration.js +5 -3
- package/dist/cli.js +18 -15
- package/dist/engine.js +122 -34
- package/dist/feedback.js +15 -0
- package/dist/handoff.js +109 -0
- package/dist/logs.js +79 -23
- package/dist/paths.js +1 -0
- package/dist/resultMeta.js +24 -0
- package/dist/runner.js +2 -23
- package/dist/schemas.js +49 -0
- package/dist/stopReport.js +79 -0
- package/package.json +1 -1
- package/prompts/fix.md +11 -0
- package/prompts/implement-code.md +11 -0
- package/prompts/implement-tests.md +11 -0
- package/prompts/plan-arbiter.md +13 -1
- package/prompts/plan-fix.md +11 -0
- package/prompts/plan-review.md +12 -1
- package/prompts/plan.md +11 -0
- package/prompts/review.md +12 -1
- package/prompts/spec.md +11 -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 # 完整顯示多行指令與絕對路徑
|
|
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
|
+
| 🔧 | 工具呼叫。只顯示第一行,多行時註明共幾行;去掉 `/bin/zsh -lc '…'` 這類 shell 包裝,worktree 內的絕對路徑改成相對路徑 |
|
|
186
187
|
| 📊 | token 用量 |
|
|
187
|
-
| 🏁 |
|
|
188
|
+
| 🏁 | 最後結果。與最後一則 💬 相同時不再重印 |
|
|
188
189
|
| ⚠️ | 工具回報的錯誤。agent 通常會自己換方法繼續,所以不列進錯誤整理 |
|
|
189
190
|
| ❌ | adapter 不認得的錯誤事件 |
|
|
190
191
|
| 📄 | 不是 JSON 的輸出行 |
|
|
191
192
|
|
|
192
|
-
adapter 不認得、也看不出錯誤跡象的 JSON 行不會顯示,只列出行數,要看全部請加 `--raw`。專案指令的 log 本來就是純文字,會原樣顯示。
|
|
193
|
+
要看完整的工具內容與重複的最後結果,加 `--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: cat .flow/spec.md
|
|
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>`。
|
|
@@ -307,12 +332,14 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
307
332
|
| 有幾家 | 誰來仲裁 | 結果 |
|
|
308
333
|
| --- | --- | --- |
|
|
309
334
|
| 三家以上 | 沒參與這次討論的那一家 | 核准就繼續,否則 run 失敗 |
|
|
310
|
-
| 兩家 | 兩家各自在全新 context 裡判斷 |
|
|
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
|
-
計畫定案或仲裁最終停止時,裁決與每位仲裁者的理由附在 `plan.md` 最後的「仲裁紀錄」。需再修訂時,裁決理由寫進 `.flow/feedback.md`,供修訂者處理;重新審查會從第一輪計數。原始審查與每輪仲裁紀錄在 `.agentflowctl/runs/<id>/reviews
|
|
340
|
+
計畫定案或仲裁最終停止時,裁決與每位仲裁者的理由附在 `plan.md` 最後的「仲裁紀錄」。需再修訂時,裁決理由寫進 `.flow/feedback.md`,供修訂者處理;重新審查會從第一輪計數。原始審查與每輪仲裁紀錄在 `.agentflowctl/runs/<id>/reviews/`。兩家都要求修改時不因仲裁輪數而直接失敗;整個 run 仍受 `maxAgentRuns` 限制。
|
|
341
|
+
|
|
342
|
+
仲裁 JSON 的 `verdict` 應為 `approve` 或 `changes_requested`;若模型寫成 `reject`,程式會當成 `changes_requested` 並保留理由。缺少檔案、JSON 格式錯誤或其他不合法輸出不算反對票,run 會暫停並顯示驗證錯誤;檢查 `agentflowctl logs <id>` 與 `.flow/plan-arbiter.json` 後可用 `resume` 重新執行。
|
|
316
343
|
|
|
317
344
|
## 階段與通過條件
|
|
318
345
|
|
|
@@ -322,7 +349,7 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
322
349
|
| plan | 與 spec 同一位 | zod、相依存在、無循環、每條驗收條件都有任務 | 重試 |
|
|
323
350
|
| plan_review | 計畫作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve` | 進入 plan_fix |
|
|
324
351
|
| plan_fix | 依 `fixStrategy` | 修改後仍通過 plan 的格式與 DAG 檢查 | 還原並重試 |
|
|
325
|
-
| 仲裁 | 見上一節 | 一致核准;分歧依 `tieBreak` |
|
|
352
|
+
| 仲裁 | 見上一節 | 一致核准;分歧依 `tieBreak` | 兩家都不核准時進入 plan_fix 再審查;第三方不核准或 `tieBreak: stop` 時失敗 |
|
|
326
353
|
| 人工確認 | 你(只有 `--manual-plan`) | `agentflowctl approve` | — |
|
|
327
354
|
| implement 紅燈 | 洗牌輪流,每家各一次 | 有測試變更,而且測試執行後失敗 | 還原並重試 |
|
|
328
355
|
| implement 綠燈 | 測試作者以外隨機一位 | 測試檔沒有任何修改,而且測試通過 | 還原,或帶著輸出重試 |
|
|
@@ -349,6 +376,14 @@ Agent 的最後回覆要附上 XML 中繼資料:
|
|
|
349
376
|
|
|
350
377
|
`blocked` 與 `concerns` 會印在終端機上,完整回覆留在 log,可用 `agentflowctl logs` 查看。這份中繼資料只給人看;缺少或格式錯誤都不影響流程,是否通過仍由上表的程式檢查決定。
|
|
351
378
|
|
|
379
|
+
### Agent 交接紀錄
|
|
380
|
+
|
|
381
|
+
每次 agent 執行前,程式會把與當前階段有關的未結事項寫入 `.flow/handoff-context.md`。agent 完成時必須寫 `.flow/handoff-response.json`,包含 `newIssues` 和 `dispositions` 兩個陣列;沒有事項也要明確寫成 `{ "newIssues": [], "dispositions": [] }`。缺少檔案或 JSON 格式不合法,會依該步驟的重試規則處理。
|
|
382
|
+
|
|
383
|
+
程式只在原有關卡通過後接收交接回覆,並將正式紀錄原子儲存於 `.agentflowctl/runs/<id>/handoff.json`。`action` 是需要後續處理的事項;`info` 只供參考。作者只能提出已修正並附證據,審查者才能確認結案或附理由接受。XML `<concerns>` 可以供人閱讀,但重要疑慮必須寫進交接 JSON,才能交給下一位 agent。額度代打與重試不會接收失敗呼叫的交接內容;中斷後可用 `resume` 接續。
|
|
384
|
+
|
|
385
|
+
計畫審查或程式碼審查若核准,但該階段仍有未結的 `action`,程式會視為互相矛盾的審查結果並重試。計畫定案和開 PR 前也會再檢查一次;`info` 會提供給目標階段閱讀,但不阻擋通關。
|
|
386
|
+
|
|
352
387
|
## Adapter
|
|
353
388
|
|
|
354
389
|
| adapter | 執行方式 | 權限 |
|
|
@@ -401,6 +436,7 @@ src/
|
|
|
401
436
|
roles.ts 角色分配規則(含計畫修正者與仲裁者)
|
|
402
437
|
runner.ts 執行 agent、正規化結果、執行專案指令
|
|
403
438
|
logs.ts log 檔名、檔頭檔尾、列表與解析
|
|
439
|
+
stopReport.ts run 停下時的結果、未結交接事項與下一步指令
|
|
404
440
|
agents/ claude、codex、gemini、command
|
|
405
441
|
setup.ts agent setup 互動精靈
|
|
406
442
|
git.ts worktree 與 git 操作
|
package/dist/arbitration.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export function arbitrationDecision(verdicts, tieBreak
|
|
1
|
+
/** 兩家仲裁都要求修改時交回計畫修訂;整體執行次數由 maxAgentRuns 限制。 */
|
|
2
|
+
export function arbitrationDecision(verdicts, tieBreak) {
|
|
3
|
+
if (verdicts.includes("abstain"))
|
|
4
|
+
return "invalid";
|
|
3
5
|
const approvals = verdicts.filter((v) => v === "approve").length;
|
|
4
6
|
if (approvals === verdicts.length)
|
|
5
7
|
return "proceed";
|
|
6
8
|
if (approvals > 0)
|
|
7
9
|
return tieBreak;
|
|
8
|
-
return verdicts.length === 2
|
|
10
|
+
return verdicts.length === 2 ? "revise" : "stop";
|
|
9
11
|
}
|
|
10
12
|
//# sourceMappingURL=arbitration.js.map
|
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", "完整顯示工具內容(多行指令、絕對路徑)與重複的最後回覆", 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,13 +4,15 @@ 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";
|
|
9
|
+
import { acceptHandoff, openActions, prepareHandoff, previewHandoff, readHandoff, recoverHandoff, reviewHandoffGate, validateHandoffResponse } from "./handoff.js";
|
|
8
10
|
import { flowDir, logDir, projectRoot, runDir, worktreeDir } from "./paths.js";
|
|
9
11
|
import { CMD_AGENT, nextLogFile } from "./logs.js";
|
|
10
12
|
import { exec } from "./proc.js";
|
|
11
13
|
import { arbiterPanel, availableAgent, fixAgent, planAgent, planFixAgent, reviewers, specAgent, taskAgents } from "./roles.js";
|
|
12
14
|
import { resolveAgent, runAgent, runCommand } from "./runner.js";
|
|
13
|
-
import { AcceptanceList, RepoConfig, ReviewResult, TaskList, } from "./schemas.js";
|
|
15
|
+
import { AcceptanceList, ArbiterResult, RepoConfig, ReviewResult, TaskList, } from "./schemas.js";
|
|
14
16
|
import { addSubstitution, addUsage, agentRuns, saveRun } from "./store.js";
|
|
15
17
|
import { orderTasks } from "./tasks.js";
|
|
16
18
|
import { readJsonFile, renderPrompt, tail } from "./util.js";
|
|
@@ -30,6 +32,14 @@ export class QuotaPause extends Error {
|
|
|
30
32
|
}
|
|
31
33
|
/** 這次執行中已確認額度用完的 agent;程序結束即清空,resume 時會重新嘗試 */
|
|
32
34
|
const exhausted = new Set();
|
|
35
|
+
function handoffTarget(run) {
|
|
36
|
+
return ["spec", "plan", "plan_review", "plan_fix"].includes(run.stage) ? "plan" : "code";
|
|
37
|
+
}
|
|
38
|
+
/** 同一輪重跑使用相同 key;重試次數或 panel 位置改變時使用新 key。 */
|
|
39
|
+
function handoffKey(run, step, slot, agent) {
|
|
40
|
+
const attempts = Object.entries(run.attempts).sort(([a], [b]) => a.localeCompare(b));
|
|
41
|
+
return JSON.stringify([run.id, run.stage, step, run.taskIndex, run.taskPhase, attempts, slot, agent]);
|
|
42
|
+
}
|
|
33
43
|
async function agentStep(run, planned, step, prompt, mode) {
|
|
34
44
|
const cfg = loadRepoConfig();
|
|
35
45
|
const reset = mode.reset ?? (() => discardChanges(worktreeDir(run.id)));
|
|
@@ -47,11 +57,13 @@ async function agentStep(run, planned, step, prompt, mode) {
|
|
|
47
57
|
info(run, `🔁 ${agent} 額度已用完,${step} 由 ${sub} 代打${note ? `(注意:${note})` : ""}`);
|
|
48
58
|
agent = sub;
|
|
49
59
|
}
|
|
60
|
+
const callKey = handoffKey(run, step, mode.slot ?? 0, agent);
|
|
61
|
+
prepareHandoff(run.id, callKey, handoffTarget(run), mode.blind ?? false);
|
|
50
62
|
const r = await runAgent(agent, resolveAgent(cfg, agent), target(run, step, agent), prompt);
|
|
51
63
|
addUsage(run.id, { stage: step, agent, inputTokens: r.inputTokens, outputTokens: r.outputTokens });
|
|
52
64
|
if (!r.quotaExhausted) {
|
|
53
65
|
reportMeta(run, agent, r);
|
|
54
|
-
return { r, agent };
|
|
66
|
+
return { r, agent, step, callKey };
|
|
55
67
|
}
|
|
56
68
|
info(run, `⛽ ${agent} 的額度已用完`);
|
|
57
69
|
exhausted.add(agent);
|
|
@@ -59,6 +71,28 @@ async function agentStep(run, planned, step, prompt, mode) {
|
|
|
59
71
|
// 迴圈回到開頭:review 會停下,write 會找代打
|
|
60
72
|
}
|
|
61
73
|
}
|
|
74
|
+
/** 原有關卡已通過後才接受交接;失敗回覆不進入正式紀錄。 */
|
|
75
|
+
function finishHandoff(run, outcome, role, gate) {
|
|
76
|
+
const parsed = validateHandoffResponse(run.id);
|
|
77
|
+
if (!parsed.ok)
|
|
78
|
+
return parsed.error;
|
|
79
|
+
try {
|
|
80
|
+
const source = {
|
|
81
|
+
stage: run.stage, step: outcome.step, agent: outcome.agent, callKey: outcome.callKey,
|
|
82
|
+
};
|
|
83
|
+
const preview = previewHandoff(readHandoff(run.id), outcome.callKey, source, parsed.data, role);
|
|
84
|
+
if (gate) {
|
|
85
|
+
const error = reviewHandoffGate(preview, gate.target, gate.verdict);
|
|
86
|
+
if (error)
|
|
87
|
+
return error;
|
|
88
|
+
}
|
|
89
|
+
acceptHandoff(run.id, outcome.callKey, source, parsed.data, role);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return error.message;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
62
96
|
/** 印出回覆裡的 XML 中繼資料;只供人檢視,關卡仍由程式檢查決定 */
|
|
63
97
|
function reportMeta(run, agent, r) {
|
|
64
98
|
if (!r.meta)
|
|
@@ -113,7 +147,8 @@ function loadOrderedTasks(run) {
|
|
|
113
147
|
async function specStage(run) {
|
|
114
148
|
const agent = specAgent(run.cycle, run.id);
|
|
115
149
|
info(run, `📝 產生規格(${agent})`);
|
|
116
|
-
const
|
|
150
|
+
const outcome = await agentStep(run, agent, "spec", renderPrompt("spec", { requirement: run.requirement }), { kind: "write" });
|
|
151
|
+
const { r } = outcome;
|
|
117
152
|
await discardChanges(worktreeDir(run.id)); // 這個階段只允許寫 .flow/
|
|
118
153
|
if (!r.ok)
|
|
119
154
|
return retry(run, "spec", `Agent 執行失敗:${r.summary}`, "spec");
|
|
@@ -125,6 +160,9 @@ async function specStage(run) {
|
|
|
125
160
|
const ids = ac.data.map((a) => a.id);
|
|
126
161
|
if (new Set(ids).size !== ids.length)
|
|
127
162
|
return retry(run, "spec", "驗收條件 id 有重複", "spec");
|
|
163
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
164
|
+
if (handoffError)
|
|
165
|
+
return retry(run, "spec", handoffError, "spec");
|
|
128
166
|
return succeed(run, "spec", "plan");
|
|
129
167
|
}
|
|
130
168
|
// ── 規格與計畫檔案:審查者與仲裁者只能讀,不能改 ──
|
|
@@ -177,6 +215,9 @@ function announceTasks(run, ordered) {
|
|
|
177
215
|
}
|
|
178
216
|
/** 計畫定案後:預設直接開始實作;--manual-plan 時才停下來等人 */
|
|
179
217
|
function planSettled(run, key) {
|
|
218
|
+
const pending = openActions(readHandoff(run.id), "plan");
|
|
219
|
+
if (pending.length)
|
|
220
|
+
return retry(run, "plan-handoff", `計畫仍有未結交接事項:${pending.map((item) => item.id).join("、")}`, "plan_fix");
|
|
180
221
|
const next = run.autopilot ? "implement" : "awaiting_approval";
|
|
181
222
|
const ordered = loadOrderedTasks(run);
|
|
182
223
|
announceTasks(run, ordered);
|
|
@@ -192,13 +233,17 @@ async function planStage(run) {
|
|
|
192
233
|
const agent = planAgent(run.cycle, run.id);
|
|
193
234
|
info(run, `🗺️ 拆解任務(${agent})`);
|
|
194
235
|
const cfg = loadRepoConfig();
|
|
195
|
-
const
|
|
236
|
+
const outcome = await agentStep(run, agent, "plan", renderPrompt("plan", { testPattern: cfg.testPattern }), { kind: "write" });
|
|
237
|
+
const { r, agent: actual } = outcome;
|
|
196
238
|
await discardChanges(worktreeDir(run.id));
|
|
197
239
|
if (!r.ok)
|
|
198
240
|
return retry(run, "plan", `Agent 執行失敗:${r.summary}`, "plan");
|
|
199
241
|
const ordered = validatePlan(run);
|
|
200
242
|
if (typeof ordered === "string")
|
|
201
243
|
return retry(run, "plan", ordered, "plan");
|
|
244
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
245
|
+
if (handoffError)
|
|
246
|
+
return retry(run, "plan", handoffError, "plan");
|
|
202
247
|
acceptPlan(run, ordered);
|
|
203
248
|
rmSync(flowFile(run, "plan-review-last.txt"), { force: true });
|
|
204
249
|
return { ...succeed(run, "plan", "plan_review"), planWriter: actual };
|
|
@@ -215,7 +260,8 @@ async function planReviewStage(run) {
|
|
|
215
260
|
info(run, `🧐 計畫審查第 ${round} 輪(${reviewer},作者 ${author})`);
|
|
216
261
|
const snap = snapshotPlan(run);
|
|
217
262
|
rmSync(flowFile(run, "plan-review.json"), { force: true });
|
|
218
|
-
const
|
|
263
|
+
const outcome = await agentStep(run, reviewer, "plan-review", renderPrompt("plan-review", { reviewer, author, requirement: run.requirement }), { kind: "review", slot: panel.indexOf(reviewer), reset: async () => { await discardChanges(worktreeDir(run.id)); restorePlan(run, snap); } });
|
|
264
|
+
const { r } = outcome;
|
|
219
265
|
await discardChanges(worktreeDir(run.id));
|
|
220
266
|
const tampered = restorePlan(run, snap);
|
|
221
267
|
if (tampered.length)
|
|
@@ -225,6 +271,9 @@ async function planReviewStage(run) {
|
|
|
225
271
|
const review = readJsonFile(flowFile(run, "plan-review.json"), ReviewResult);
|
|
226
272
|
if (!review.ok)
|
|
227
273
|
return retry(run, "plan-review-run", review.error, "plan_review");
|
|
274
|
+
const handoffError = finishHandoff(run, outcome, "reviewer", { target: "plan", verdict: review.data.verdict });
|
|
275
|
+
if (handoffError)
|
|
276
|
+
return retry(run, "plan-review-run", handoffError, "plan_review");
|
|
228
277
|
// 審查紀錄移到 worktree 外面:之後的仲裁者看不到是哪一家提的意見
|
|
229
278
|
mkdirSync(join(runDir(run.id), "reviews"), { recursive: true });
|
|
230
279
|
renameSync(flowFile(run, "plan-review.json"), join(runDir(run.id), "reviews", `plan-review-${round}-${reviewer}.json`));
|
|
@@ -236,9 +285,9 @@ async function planReviewStage(run) {
|
|
|
236
285
|
firstObjector ??= reviewer;
|
|
237
286
|
const lines = review.data.items
|
|
238
287
|
.filter((i) => i.status !== "met")
|
|
239
|
-
.map((i) =>
|
|
288
|
+
.map((i) => reviewIssue(i.criterion, i.status, i.note));
|
|
240
289
|
issueLines.push(...lines);
|
|
241
|
-
issues.push(
|
|
290
|
+
issues.push(opinion(reviewer, lines));
|
|
242
291
|
}
|
|
243
292
|
if (!firstObjector)
|
|
244
293
|
return planSettled(run, "plan-review");
|
|
@@ -272,7 +321,8 @@ async function planFixStage(run) {
|
|
|
272
321
|
info(run, `✏️ 依 ${run.planReviewer ?? "審查者"} 的意見修改計畫(${agent})`);
|
|
273
322
|
const feedback = readFeedback(run);
|
|
274
323
|
const snap = snapshotPlan(run);
|
|
275
|
-
const
|
|
324
|
+
const outcome = await agentStep(run, agent, "plan-fix", renderPrompt("plan-fix", { requirement: run.requirement, testPattern: cfg.testPattern }), { kind: "write", reset: async () => { await discardChanges(worktreeDir(run.id)); restorePlan(run, snap); } });
|
|
325
|
+
const { r, agent: actual } = outcome;
|
|
276
326
|
await discardChanges(worktreeDir(run.id)); // 只允許改 .flow/
|
|
277
327
|
if (!r.ok) {
|
|
278
328
|
restorePlan(run, snap);
|
|
@@ -283,6 +333,11 @@ async function planFixStage(run) {
|
|
|
283
333
|
restorePlan(run, snap);
|
|
284
334
|
return retry(run, "plan-fix", `${feedback}\n\n另外,修改後的計畫沒有通過格式檢查,已還原:\n${ordered}`, "plan_fix");
|
|
285
335
|
}
|
|
336
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
337
|
+
if (handoffError) {
|
|
338
|
+
restorePlan(run, snap);
|
|
339
|
+
return retry(run, "plan-fix", handoffError, "plan_fix");
|
|
340
|
+
}
|
|
286
341
|
acceptPlan(run, ordered);
|
|
287
342
|
writeFileSync(flowFile(run, "feedback.md"), feedback); // 保留審查意見,讓下一輪審查者知道上次提了什麼
|
|
288
343
|
const attempts = { ...run.attempts };
|
|
@@ -299,45 +354,54 @@ async function arbitratePlan(run) {
|
|
|
299
354
|
const panel = arbiterPanel(run.cycle, `${run.id}:arbiter`, run.planWriter, run.planReviewer);
|
|
300
355
|
const mode = panel.length > 1 ? "雙盲交叉仲裁" : "第三方仲裁";
|
|
301
356
|
const verdicts = [];
|
|
302
|
-
for (const arbiter of panel) {
|
|
357
|
+
for (const [slot, arbiter] of panel.entries()) {
|
|
303
358
|
info(run, `⚖️ ${mode}(${arbiter})`);
|
|
304
359
|
const snap = snapshotPlan(run);
|
|
305
360
|
rmSync(flowFile(run, "plan-arbiter.json"), { force: true });
|
|
306
|
-
const
|
|
307
|
-
kind: "review",
|
|
361
|
+
const outcome = await agentStep(run, arbiter, "plan-arbiter", renderPrompt("plan-arbiter", { requirement: run.requirement }), {
|
|
362
|
+
kind: "review", slot, blind: true,
|
|
308
363
|
reset: async () => { await discardChanges(worktreeDir(run.id)); restorePlan(run, snap); },
|
|
309
364
|
});
|
|
365
|
+
const { r } = outcome;
|
|
310
366
|
await discardChanges(worktreeDir(run.id));
|
|
311
367
|
restorePlan(run, snap);
|
|
312
|
-
const result = r.ok ? readJsonFile(flowFile(run, "plan-arbiter.json"),
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
368
|
+
const result = r.ok ? readJsonFile(flowFile(run, "plan-arbiter.json"), ArbiterResult) : undefined;
|
|
369
|
+
const handoffError = result?.ok ? finishHandoff(run, outcome, "reviewer", { target: "plan", verdict: result.data.verdict }) : undefined;
|
|
370
|
+
if (!result?.ok || handoffError) {
|
|
371
|
+
const outputError = !r.ok ? `Agent 執行失敗:${r.summary}` : !result ? "未產生有效裁決" : result.ok ? "未產生有效裁決" : result.error;
|
|
372
|
+
const reason = handoffError ?? outputError;
|
|
373
|
+
info(run, ` ⏸️ ${arbiter} 未產生有效裁決:${reason}`);
|
|
374
|
+
return { ...run, stage: "paused", pausedStage: "plan_review", pauseReason: `${arbiter} 仲裁未產生有效裁決:${reason}` };
|
|
316
375
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
376
|
+
mkdirSync(join(runDir(run.id), "reviews"), { recursive: true });
|
|
377
|
+
renameSync(flowFile(run, "plan-arbiter.json"), join(runDir(run.id), "reviews", `plan-arbiter-${arbitrationRound}-${arbiter}.json`));
|
|
378
|
+
const verdict = result.data.verdict;
|
|
379
|
+
const notes = result.data.items.map((i) => `<issue criterion="${escapeXml(i.criterion)}">${escapeXml(i.note)}</issue>`);
|
|
380
|
+
const markdownNotes = result.data.items.map((i) => `- ${i.criterion}:${i.note}`);
|
|
381
|
+
info(run, ` ${verdict === "approve" ? "✓" : "✗"} ${arbiter}:${verdict === "approve" ? "可以執行" : "不可執行"}`);
|
|
382
|
+
verdicts.push({ arbiter, verdict, notes, markdownNotes });
|
|
321
383
|
}
|
|
322
384
|
rmSync(flowFile(run, "dispute.md"), { force: true });
|
|
323
385
|
const approvals = verdicts.filter((v) => v.verdict === "approve").length;
|
|
324
386
|
const unanimous = approvals === verdicts.length;
|
|
325
|
-
const decision = arbitrationDecision(verdicts.map((v) => v.verdict), cfg.tieBreak
|
|
387
|
+
const decision = arbitrationDecision(verdicts.map((v) => v.verdict), cfg.tieBreak);
|
|
388
|
+
if (decision === "invalid")
|
|
389
|
+
throw new Error("仲裁結果缺少有效裁決");
|
|
326
390
|
const summary = unanimous
|
|
327
391
|
? `${mode}一致核准`
|
|
328
392
|
: approvals === 0
|
|
329
393
|
? `${mode}沒有任何一方核准${decision === "revise" ? ",交回計畫修訂" : ""}`
|
|
330
394
|
: `${mode}意見分歧,依 tieBreak=${cfg.tieBreak} ${cfg.tieBreak === "proceed" ? "繼續實作" : "停止"}`;
|
|
331
|
-
const
|
|
395
|
+
const feedbackRecord = verdicts.map((v) => opinion(v.arbiter, v.notes, v.verdict)).join("\n\n");
|
|
332
396
|
if (decision === "revise") {
|
|
333
397
|
const attempts = { ...run.attempts, "plan-arbitration": arbitrationRound };
|
|
334
398
|
delete attempts["plan-review"];
|
|
335
399
|
rmSync(flowFile(run, "plan-review-last.txt"), { force: true });
|
|
336
|
-
writeFileSync(flowFile(run, "feedback.md"), `# 仲裁要求修訂(第 ${arbitrationRound} 次)\n\n${summary}\n\n${
|
|
400
|
+
writeFileSync(flowFile(run, "feedback.md"), `# 仲裁要求修訂(第 ${arbitrationRound} 次)\n\n${summary}\n\n${feedbackRecord}\n`);
|
|
337
401
|
info(run, ` → ${summary}`);
|
|
338
402
|
return { ...run, attempts, stage: "plan_fix" };
|
|
339
403
|
}
|
|
340
|
-
writeFileSync(flowFile(run, "plan.md"), `${readFileSync(flowFile(run, "plan.md"), "utf8")}\n\n## 仲裁紀錄\n\n**結果:${summary}**\n\n${
|
|
404
|
+
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`);
|
|
341
405
|
if (decision === "proceed") {
|
|
342
406
|
info(run, ` → ${summary}`);
|
|
343
407
|
return planSettled(run, "plan-review");
|
|
@@ -363,7 +427,8 @@ async function implementStage(run) {
|
|
|
363
427
|
const key = `${task.id}:tests`;
|
|
364
428
|
info(run, `🧪 [${progress}] 撰寫測試(${agents.tests})`);
|
|
365
429
|
const before = await headCommit(repo);
|
|
366
|
-
const
|
|
430
|
+
const outcome = await agentStep(run, agents.tests, `${task.id}-tests`, renderPrompt("implement-tests", { task: taskJson, testPattern: cfg.testPattern, testCmd }), { kind: "write", reset: () => resetTo(repo, before) });
|
|
431
|
+
const { r, agent: testsAuthor } = outcome;
|
|
367
432
|
if (!r.ok) {
|
|
368
433
|
await resetTo(repo, before);
|
|
369
434
|
return retry(run, key, `Agent 執行失敗:${r.summary}`, "implement");
|
|
@@ -381,6 +446,11 @@ async function implementStage(run) {
|
|
|
381
446
|
await resetTo(repo, before);
|
|
382
447
|
return retry(run, key, "測試在功能尚未實作前就全部通過,代表測試沒有驗證到新行為。請撰寫會因功能尚未實作而失敗的測試。", "implement");
|
|
383
448
|
}
|
|
449
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
450
|
+
if (handoffError) {
|
|
451
|
+
await resetTo(repo, before);
|
|
452
|
+
return retry(run, key, handoffError, "implement");
|
|
453
|
+
}
|
|
384
454
|
writeFileSync(flowFile(run, "red-output.txt"), red.output);
|
|
385
455
|
info(run, `🔴 [${progress}] 測試如預期失敗`);
|
|
386
456
|
return { ...succeed(run, key, "implement"), taskPhase: "code", testsCommit: commit, lastTestsAuthor: testsAuthor };
|
|
@@ -392,7 +462,8 @@ async function implementStage(run) {
|
|
|
392
462
|
throw new Error("缺少 testsCommit,狀態不一致");
|
|
393
463
|
info(run, `🛠️ [${progress}] 實作(${agents.code},測試由 ${run.lastTestsAuthor ?? agents.tests} 撰寫)`);
|
|
394
464
|
const redOutput = existsSync(flowFile(run, "red-output.txt")) ? readFileSync(flowFile(run, "red-output.txt"), "utf8") : "";
|
|
395
|
-
const
|
|
465
|
+
const outcome = await agentStep(run, agents.code, `${task.id}-code`, renderPrompt("implement-code", { task: taskJson, testCmd, redOutput: tail(redOutput, 3000) }), { kind: "write", reset: () => resetTo(repo, testsCommit) });
|
|
466
|
+
const { r, agent: codeAuthor } = outcome;
|
|
396
467
|
if (!r.ok)
|
|
397
468
|
return retry(run, key, `Agent 執行失敗:${r.summary}`, "implement");
|
|
398
469
|
await commitAll(repo, `feat(${task.id}): ${task.title} [${codeAuthor}]`);
|
|
@@ -406,6 +477,11 @@ async function implementStage(run) {
|
|
|
406
477
|
info(run, ` ✗ 測試仍未通過${logHint(run, green.seq)}`);
|
|
407
478
|
return retry(run, key, `測試仍未通過:\n\n\`\`\`\n${tail(green.output)}\n\`\`\``, "implement");
|
|
408
479
|
}
|
|
480
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
481
|
+
if (handoffError) {
|
|
482
|
+
await resetTo(repo, testsCommit);
|
|
483
|
+
return retry(run, key, handoffError, "implement");
|
|
484
|
+
}
|
|
409
485
|
info(run, `🟢 [${progress}] 完成`);
|
|
410
486
|
return {
|
|
411
487
|
...succeed(run, key, "implement"),
|
|
@@ -454,10 +530,11 @@ async function fixStage(run) {
|
|
|
454
530
|
const repo = worktreeDir(run.id);
|
|
455
531
|
const feedback = readFeedback(run);
|
|
456
532
|
const before = await headCommit(repo);
|
|
457
|
-
const
|
|
533
|
+
const outcome = await agentStep(run, agent, "fix", renderPrompt("fix", { testPattern: cfg.testPattern }), {
|
|
458
534
|
kind: "write",
|
|
459
535
|
reset: () => resetTo(repo, before),
|
|
460
536
|
});
|
|
537
|
+
const { r, agent: actual } = outcome;
|
|
461
538
|
if (!r.ok)
|
|
462
539
|
return retry(run, "fix", `${feedback}\n\n(上次修正時 Agent 執行失敗:${r.summary})`, "fix");
|
|
463
540
|
await commitAll(repo, `fix: ${why} [${actual}]`);
|
|
@@ -467,6 +544,11 @@ async function fixStage(run) {
|
|
|
467
544
|
await resetTo(repo, before);
|
|
468
545
|
return retry(run, "fix", `${feedback}\n\n另外:不可刪除測試檔來讓檢查通過,已還原:${deleted.join(", ")}`, "fix");
|
|
469
546
|
}
|
|
547
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
548
|
+
if (handoffError) {
|
|
549
|
+
await resetTo(repo, before);
|
|
550
|
+
return retry(run, "fix", handoffError, "fix");
|
|
551
|
+
}
|
|
470
552
|
// 修正者成為新的作者,下一輪審查會換成別人
|
|
471
553
|
return { ...to(run, "verify"), lastWriter: actual };
|
|
472
554
|
}
|
|
@@ -479,18 +561,22 @@ async function reviewStage(run) {
|
|
|
479
561
|
.map((s) => s.slice(1, -1));
|
|
480
562
|
const issues = [];
|
|
481
563
|
let firstObjector;
|
|
482
|
-
for (const reviewer of panel) {
|
|
564
|
+
for (const [slot, reviewer] of panel.entries()) {
|
|
483
565
|
info(run, `👀 程式碼審查(${reviewer})`);
|
|
484
566
|
rmSync(flowFile(run, "review.json"), { force: true });
|
|
485
|
-
const
|
|
486
|
-
kind: "review",
|
|
567
|
+
const outcome = await agentStep(run, reviewer, "review", renderPrompt("review", { reviewer, authors: authors.join("、") || "未知" }), {
|
|
568
|
+
kind: "review", slot,
|
|
487
569
|
});
|
|
570
|
+
const { r } = outcome;
|
|
488
571
|
await discardChanges(repo); // 審查者不可改程式碼
|
|
489
572
|
if (!r.ok)
|
|
490
573
|
return retry(run, "review-run", `Agent 執行失敗:${r.summary}`, "review");
|
|
491
574
|
const review = readJsonFile(flowFile(run, "review.json"), ReviewResult);
|
|
492
575
|
if (!review.ok)
|
|
493
576
|
return retry(run, "review-run", review.error, "review");
|
|
577
|
+
const handoffError = finishHandoff(run, outcome, "reviewer", { target: "code", verdict: review.data.verdict });
|
|
578
|
+
if (handoffError)
|
|
579
|
+
return retry(run, "review-run", handoffError, "review");
|
|
494
580
|
renameSync(flowFile(run, "review.json"), flowFile(run, `review-${reviewer}.json`));
|
|
495
581
|
if (review.data.verdict === "approve") {
|
|
496
582
|
info(run, ` ✓ ${reviewer} 核准`);
|
|
@@ -498,11 +584,9 @@ async function reviewStage(run) {
|
|
|
498
584
|
}
|
|
499
585
|
info(run, ` ✗ ${reviewer} 要求修改`);
|
|
500
586
|
firstObjector ??= reviewer;
|
|
501
|
-
issues.push(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
.map((i) => `- **${i.criterion}**(${i.status}):${i.note}`)
|
|
505
|
-
.join("\n"));
|
|
587
|
+
issues.push(opinion(reviewer, review.data.items
|
|
588
|
+
.filter((i) => i.status !== "met")
|
|
589
|
+
.map((i) => reviewIssue(i.criterion, i.status, i.note))));
|
|
506
590
|
}
|
|
507
591
|
if (!firstObjector)
|
|
508
592
|
return succeed(run, "review", "pr");
|
|
@@ -513,6 +597,9 @@ async function reviewStage(run) {
|
|
|
513
597
|
};
|
|
514
598
|
}
|
|
515
599
|
async function prStage(run) {
|
|
600
|
+
const pending = openActions(readHandoff(run.id));
|
|
601
|
+
if (pending.length)
|
|
602
|
+
return { ...run, stage: "failed", failedStage: "pr", failureReason: `仍有未結交接事項:${pending.map((item) => item.id).join("、")}` };
|
|
516
603
|
const repo = worktreeDir(run.id);
|
|
517
604
|
const remotes = (await git(repo, "remote")).split("\n").filter(Boolean);
|
|
518
605
|
if (!remotes.includes("origin")) {
|
|
@@ -546,6 +633,7 @@ const STAGES = {
|
|
|
546
633
|
/** 一路推進到需要人介入(awaiting_approval、paused)或結束(done / failed)為止;每一步都寫回檔案,中斷後可接續 */
|
|
547
634
|
export async function advance(initial) {
|
|
548
635
|
let run = initial;
|
|
636
|
+
recoverHandoff(run.id);
|
|
549
637
|
for (;;) {
|
|
550
638
|
if (["done", "failed", "awaiting_approval", "paused"].includes(run.stage))
|
|
551
639
|
return run;
|
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
|