agentflowctl 0.4.0 → 0.5.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 +31 -27
- package/dist/agentConfig.js +4 -4
- package/dist/arbitration.js +10 -0
- package/dist/cli.js +17 -17
- package/dist/config.js +0 -2
- package/dist/engine.js +39 -15
- package/dist/roles.js +70 -39
- package/dist/runner.js +2 -1
- package/dist/schemas.js +9 -3
- package/dist/setup.js +2 -2
- package/examples/flow.config.json +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ claude # 完成登入
|
|
|
17
17
|
codex # 完成登入
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
沒有內建的 agent,只會使用 `flow.config.json` 的 `agents` 裡設定的。用 `agent setup` 互動設定:它會偵測本機的 `claude`、`codex`、`gemini`,逐一詢問要不要加入、名稱與 model
|
|
20
|
+
沒有內建的 agent,只會使用 `flow.config.json` 的 `agents` 裡設定的。用 `agent setup` 互動設定:它會偵測本機的 `claude`、`codex`、`gemini`,逐一詢問要不要加入、名稱與 model,再設定參與的 agent;確認後才一次寫入,最後自動跑一次 `doctor`:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npx agentflowctl agent setup
|
|
@@ -30,7 +30,7 @@ npx agentflowctl agent add claude --adapter claude
|
|
|
30
30
|
npx agentflowctl agent add codex --adapter codex
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
之後改了設定或換了環境,用 `doctor` 檢查。它會列出設定的 agent 的 CLI
|
|
33
|
+
之後改了設定或換了環境,用 `doctor` 檢查。它會列出設定的 agent 的 CLI 是否已安裝,並印出即將參與的 agent。沒有設定 `cycle` 時,依 `agents` 的順序取已安裝的;一個都沒偵測到就無法執行。
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
npx agentflowctl doctor
|
|
@@ -64,26 +64,28 @@ pnpm run build
|
|
|
64
64
|
pnpm link --global
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
##
|
|
67
|
+
## 角色分配怎麼打破同溫層
|
|
68
68
|
|
|
69
69
|
同一個模型審查自己的程式碼,容易放過自己的寫法;測試和實作出自同一個模型,也容易寫出剛好會過的測試。角色分配只有三條規則,定義在 `src/roles.ts`:
|
|
70
70
|
|
|
71
71
|
| 規則 | 效果 |
|
|
72
72
|
|---|---|
|
|
73
|
-
| 審查者永遠不是最後寫程式的 agent | 每一輪審查都由另一家看 |
|
|
73
|
+
| 審查者永遠不是最後寫程式的 agent,多位審查者彼此不重複 | 每一輪審查都由另一家看 |
|
|
74
74
|
| 同一個任務的測試與實作由不同 agent 負責(`tddSplit`) | A 寫的測試,B 實作到通過,而且不能改測試 |
|
|
75
|
-
|
|
|
75
|
+
| 人選隨機決定,不依 `cycle` 的順序 | 各家輪流當作者、審查者、修正者,不會固定由同一家起頭 |
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
`cycle` 只決定哪些 agent 參與。隨機以 run id 為種子,同一個 run 的同一步驟 `resume` 後仍是同一家。寫測試的人每 N 個任務(N 為參與的家數)洗一次牌,每家各輪一次,換輪時也不會連續兩個任務由同一家寫測試。
|
|
78
|
+
|
|
79
|
+
兩家時是乒乓,例如:
|
|
78
80
|
|
|
79
81
|
```
|
|
80
|
-
計畫:
|
|
82
|
+
計畫:codex 撰寫 → claude 審查(要求修改)→ codex 修改 → claude 審查(核准)
|
|
81
83
|
T-1 測試:claude 實作:codex
|
|
82
84
|
T-2 測試:codex 實作:claude
|
|
83
85
|
審查:codex(最後作者是 claude)→ 要求修改 → claude 修正 → codex 審查(核准)
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
三家時,每一輪的審查者從作者以外隨機挑,修正者從審查者以外隨機挑;仲裁交給沒寫過這份計畫、也沒審過它的那一家(有多家時隨機挑一家)。
|
|
87
89
|
|
|
88
90
|
每個 commit 訊息結尾會標上實際作者,例如 `feat(T-2): 匯出 [gemini]`。
|
|
89
91
|
|
|
@@ -112,7 +114,7 @@ agentflowctl clean --all # 清掉所有已結束的 run 與中斷留
|
|
|
112
114
|
|---|---|
|
|
113
115
|
| `--req` / `--req-file` | 需求文字,或從檔案讀取 |
|
|
114
116
|
| `--base` | 基底分支,預設為目前分支 |
|
|
115
|
-
| `--cycle` | 這次 run
|
|
117
|
+
| `--cycle` | 這次 run 參與的 agent,例如 `claude,codex,gemini`;順序不影響分工;建立後就固定,`resume` 沿用 |
|
|
116
118
|
| `--max-agent-runs` | 這次 run 的 agent 執行次數上限 |
|
|
117
119
|
| `--manual-plan` | 計畫通過審查後進入 `awaiting_approval`,等 `approve` 才開始實作 |
|
|
118
120
|
| `-v` / `--verbose` | 執行時印出 agent 的文字、工具呼叫與專案指令;`run`、`resume`、`approve` 都適用,也可設 `AGENTFLOWCTL_VERBOSE=1` |
|
|
@@ -226,6 +228,7 @@ stderr:
|
|
|
226
228
|
"fixStrategy": "ring",
|
|
227
229
|
"tddSplit": true,
|
|
228
230
|
"tieBreak": "proceed",
|
|
231
|
+
"defaultModels": { "claude": "Claude 模型名稱", "codex": "Codex 模型名稱", "gemini": "Gemini 模型名稱" },
|
|
229
232
|
"agents": {
|
|
230
233
|
"claude": { "adapter": "claude" },
|
|
231
234
|
"codex": { "adapter": "codex", "model": "你要用的模型" },
|
|
@@ -236,8 +239,9 @@ stderr:
|
|
|
236
239
|
|
|
237
240
|
| 設定 | 預設 | 說明 |
|
|
238
241
|
|---|---|---|
|
|
239
|
-
| `cycle` | 自動偵測 |
|
|
240
|
-
| `
|
|
242
|
+
| `cycle` | 自動偵測 | 參與的 agent,順序不影響分工(人選隨機決定)。未設定時依 `agents` 的順序取已安裝的 CLI。同一家 CLI 可以登記成不同 agent,例如 `claude-fast` 與 `claude-strong` |
|
|
243
|
+
| `defaultModels` | `{}` | 依 `claude`、`codex`、`gemini` adapter 指定全域預設 model;agent 的 `model` 優先,兩者都沒設時使用各 CLI 的預設。`command` adapter 不套用 |
|
|
244
|
+
| `fixStrategy` | `ring` | `ring`:審查意見隨機交給審查者以外的一家;`author`:交回最後作者 |
|
|
241
245
|
| `tddSplit` | `true` | 測試與實作是否分開 |
|
|
242
246
|
| `reviewQuorum` | `1` | 程式碼需要幾位不同審查者都 `approve` |
|
|
243
247
|
| `planReviewQuorum` | `1` | 計畫需要幾位不同審查者都 `approve` |
|
|
@@ -271,14 +275,14 @@ verify 失敗(型別、lint、建置)一律交回最後作者。審查意見
|
|
|
271
275
|
`agents` 與 `cycle` 也可以用 `agent` 指令修改,不必手動編輯 JSON。每次寫入前都會先驗證整份設定:
|
|
272
276
|
|
|
273
277
|
```bash
|
|
274
|
-
agentflowctl agent setup # 互動設定 claude、codex、gemini
|
|
275
|
-
agentflowctl agent list # 設定的 agent
|
|
278
|
+
agentflowctl agent setup # 互動設定 claude、codex、gemini 與參與的 agent
|
|
279
|
+
agentflowctl agent list # 設定的 agent、是否已安裝、是否參與
|
|
276
280
|
agentflowctl agent add claude-strong --adapter claude --model opus
|
|
277
281
|
agentflowctl agent add aider --adapter command -- aider --yes-always --message {prompt}
|
|
278
282
|
agentflowctl agent set codex --model 你要用的模型 --extra-arg=--search
|
|
279
283
|
agentflowctl agent set aider --adapter gemini # 換 adapter
|
|
280
284
|
agentflowctl agent remove aider
|
|
281
|
-
agentflowctl agent cycle claude-strong,codex,gemini #
|
|
285
|
+
agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯示目前參與的 agent
|
|
282
286
|
```
|
|
283
287
|
|
|
284
288
|
修改會連帶更新相關設定,並在終端機列出:
|
|
@@ -286,9 +290,9 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
286
290
|
- `set --adapter` 換 adapter 時,會清掉舊 adapter 的 `model`、`extraArgs`、`command`,這次有重新指定的除外。
|
|
287
291
|
- `remove` 會一併從 `cycle` 移除。`cycle` 變空就刪除這個欄位,改回從 `agents` 自動偵測。
|
|
288
292
|
- `--extra-arg` 可以重複指定,會整個取代原本的 `extraArgs`。參數以 `-` 開頭時,寫成 `--extra-arg=--sandbox`。
|
|
289
|
-
- `setup`
|
|
293
|
+
- `setup` 遇到已存在的名稱會先問要不要覆寫;不覆寫時保留原設定,但仍會參與。在非互動式環境(CI、管線)裡請改用 `agent add`。`command` adapter 要自己寫指令,不在 `setup` 裡。
|
|
290
294
|
|
|
291
|
-
已建立的 run
|
|
295
|
+
已建立的 run 會沿用建立時參與的 agent,不受這些修改影響。
|
|
292
296
|
|
|
293
297
|
## 計畫怎麼在沒有人的情況下通過
|
|
294
298
|
|
|
@@ -303,27 +307,27 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
303
307
|
| 有幾家 | 誰來仲裁 | 結果 |
|
|
304
308
|
| --- | --- | --- |
|
|
305
309
|
| 三家以上 | 沒參與這次討論的那一家 | 核准就繼續,否則 run 失敗 |
|
|
306
|
-
| 兩家 | 兩家各自在全新 context 裡判斷 |
|
|
310
|
+
| 兩家 | 兩家各自在全新 context 裡判斷 | 都核准就繼續;都不核准就依裁決意見修訂並重新審查,仲裁達重試上限才停下;分歧依 `tieBreak` |
|
|
307
311
|
| 一家 | 同一家 | 由它自己仲裁 |
|
|
308
312
|
|
|
309
313
|
兩家時的仲裁是雙盲的。仲裁者只看計畫,以及一份不含模型名稱的爭議清單(`.flow/dispute.md`)。帶有名稱的審查紀錄移到 worktree 以外。`tieBreak` 預設 `proceed`,因為後面還有測試紅燈、綠燈、verify 與程式碼審查。
|
|
310
314
|
|
|
311
|
-
|
|
315
|
+
計畫定案或仲裁最終停止時,裁決與每位仲裁者的理由附在 `plan.md` 最後的「仲裁紀錄」。需再修訂時,裁決理由寫進 `.flow/feedback.md`,供修訂者處理;重新審查會從第一輪計數。原始審查與每輪仲裁紀錄在 `.agentflowctl/runs/<id>/reviews/`。仲裁最多進行 `AGENTFLOWCTL_MAX_ATTEMPTS` 次,預設三次。
|
|
312
316
|
|
|
313
317
|
## 階段與通過條件
|
|
314
318
|
|
|
315
319
|
| 階段 | 負責的 agent | 程式認定通過的條件 | 失敗時 |
|
|
316
320
|
|---|---|---|---|
|
|
317
|
-
| spec |
|
|
318
|
-
| plan |
|
|
319
|
-
| plan_review |
|
|
321
|
+
| spec | 隨機一位 | 檔案存在、zod 驗證、id 不重複 | 重試 |
|
|
322
|
+
| plan | 與 spec 同一位 | zod、相依存在、無循環、每條驗收條件都有任務 | 重試 |
|
|
323
|
+
| plan_review | 計畫作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve` | 進入 plan_fix |
|
|
320
324
|
| plan_fix | 依 `fixStrategy` | 修改後仍通過 plan 的格式與 DAG 檢查 | 還原並重試 |
|
|
321
|
-
| 仲裁 | 見上一節 | 一致核准;分歧依 `tieBreak` |
|
|
325
|
+
| 仲裁 | 見上一節 | 一致核准;分歧依 `tieBreak` | 兩家都不核准時先進入 plan_fix,達仲裁上限才失敗;第三方不核准或 `tieBreak: stop` 時失敗 |
|
|
322
326
|
| 人工確認 | 你(只有 `--manual-plan`) | `agentflowctl approve` | — |
|
|
323
|
-
| implement 紅燈 |
|
|
324
|
-
| implement 綠燈 |
|
|
327
|
+
| implement 紅燈 | 洗牌輪流,每家各一次 | 有測試變更,而且測試執行後失敗 | 還原並重試 |
|
|
328
|
+
| implement 綠燈 | 測試作者以外隨機一位 | 測試檔沒有任何修改,而且測試通過 | 還原,或帶著輸出重試 |
|
|
325
329
|
| verify | — | `install` 與所有 `checks` 通過 | 交回作者修正 |
|
|
326
|
-
| review |
|
|
330
|
+
| review | 作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve` | 依 `fixStrategy` 交給他人修正 |
|
|
327
331
|
| pr | — | push 成功;有 `gh` 就開 PR | — |
|
|
328
332
|
|
|
329
333
|
驗收條件寫在 `.flow/acceptance.json`(`AC-1`…),任務寫在 `.flow/tasks.json`(`T-1`…)。
|
|
@@ -367,7 +371,7 @@ Codex 沙箱預設不能連網,所以建立 worktree 時會先跑 `install`。
|
|
|
367
371
|
| 步驟 | 行為 |
|
|
368
372
|
| --- | --- |
|
|
369
373
|
| 計畫審查、程式碼審查、仲裁 | 暫停。額度恢復後 `agentflowctl resume`。換人代審會變成作者審自己 |
|
|
370
|
-
| 規格、計畫、修改計畫、寫測試、寫實作、修正 |
|
|
374
|
+
| 規格、計畫、修改計畫、寫測試、寫實作、修正 | 隨機由另一家還有額度的 agent 代打 |
|
|
371
375
|
| 每家都用完 | 暫停 |
|
|
372
376
|
|
|
373
377
|
換人或暫停前,額度用完的 agent 留下的半成品會先清掉。代打寫在 `.agentflowctl/runs/<id>/substitutions.jsonl`,`status` 會列出。commit 結尾標的是實際執行的模型。
|
|
@@ -394,7 +398,7 @@ agentflowctl 本身只依賴 Node.js 與 git。專案指令透過系統 shell
|
|
|
394
398
|
src/
|
|
395
399
|
cli.ts 指令列(run、doctor、status……)
|
|
396
400
|
engine.ts 狀態機與各階段
|
|
397
|
-
roles.ts
|
|
401
|
+
roles.ts 角色分配規則(含計畫修正者與仲裁者)
|
|
398
402
|
runner.ts 執行 agent、正規化結果、執行專案指令
|
|
399
403
|
logs.ts log 檔名、檔頭檔尾、列表與解析
|
|
400
404
|
agents/ claude、codex、gemini、command
|
package/dist/agentConfig.js
CHANGED
|
@@ -59,21 +59,21 @@ export function removeAgent(cfg, name) {
|
|
|
59
59
|
const rest = cycle.filter((n) => n !== name);
|
|
60
60
|
if (rest.length) {
|
|
61
61
|
next.cycle = rest;
|
|
62
|
-
changes.push(
|
|
62
|
+
changes.push(`已從參與的 agent 移除,現在是 ${rest.join("、")}`);
|
|
63
63
|
}
|
|
64
64
|
else {
|
|
65
65
|
delete next.cycle;
|
|
66
|
-
changes.push("
|
|
66
|
+
changes.push("參與的 agent 因此變空,已刪除 cycle,改回從 agents 自動偵測已安裝的 CLI");
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
return { cfg: next, changes };
|
|
70
70
|
}
|
|
71
71
|
export function setCycle(cfg, names) {
|
|
72
72
|
if (!names.length)
|
|
73
|
-
throw new Error("
|
|
73
|
+
throw new Error("參與的 agent 至少要有一個");
|
|
74
74
|
const dup = names.find((n, i) => names.indexOf(n) !== i);
|
|
75
75
|
if (dup)
|
|
76
|
-
throw new Error(
|
|
76
|
+
throw new Error(`參與的 agent 裡 ${dup} 重複了`);
|
|
77
77
|
const missing = names.filter((n) => !isDefined(cfg, n));
|
|
78
78
|
if (missing.length)
|
|
79
79
|
throw new Error(`未定義的 agent:${missing.join("、")}(先用 agent add 新增)`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 仲裁沒有共識時,兩家先依裁決意見修訂計畫;重複仲裁仍受次數上限約束。 */
|
|
2
|
+
export function arbitrationDecision(verdicts, tieBreak, round, maxAttempts) {
|
|
3
|
+
const approvals = verdicts.filter((v) => v === "approve").length;
|
|
4
|
+
if (approvals === verdicts.length)
|
|
5
|
+
return "proceed";
|
|
6
|
+
if (approvals > 0)
|
|
7
|
+
return tieBreak;
|
|
8
|
+
return verdicts.length === 2 && round < maxAttempts ? "revise" : "stop";
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=arbitration.js.map
|
package/dist/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ function printSummary(run) {
|
|
|
36
36
|
console.log(`run ${run.id}`);
|
|
37
37
|
console.log(`階段 ${run.stage}`);
|
|
38
38
|
console.log(`用量 agent 執行 ${agentRuns(run.id)} / ${run.maxAgentRuns} 次`);
|
|
39
|
-
console.log(`agent ${run.cycle.join("
|
|
39
|
+
console.log(`agent ${run.cycle.join("、")}${run.lastWriter ? `(最後作者:${run.lastWriter})` : ""}`);
|
|
40
40
|
console.log(`分支 ${run.branch}`);
|
|
41
41
|
console.log(`worktree ${worktreeDir(run.id)}`);
|
|
42
42
|
if (run.prUrl)
|
|
@@ -63,7 +63,7 @@ function printSummary(run) {
|
|
|
63
63
|
if (run.stage === "awaiting_approval")
|
|
64
64
|
console.log(`\n確認計畫後執行 agentflowctl approve ${run.id}`);
|
|
65
65
|
}
|
|
66
|
-
/**
|
|
66
|
+
/** 決定參與的 agent:指令參數 > flow.config.json > 自動偵測已安裝的 CLI */
|
|
67
67
|
async function resolveCycle(flag) {
|
|
68
68
|
const cfg = loadRepoConfig();
|
|
69
69
|
const wanted = flag ? flag.split(",").map((s) => s.trim()).filter(Boolean) : cfg.cycle;
|
|
@@ -102,7 +102,7 @@ program
|
|
|
102
102
|
.option("--base <branch>", "基底分支(預設為目前的分支)")
|
|
103
103
|
.option("--max-agent-runs <n>", "單一 run 最多執行幾次 agent(預設取 flow.config.json 的 maxAgentRuns)")
|
|
104
104
|
.option("--manual-plan", "計畫通過 AI 審查後,仍停下來等你確認", false)
|
|
105
|
-
.option("--cycle <agents>", "agent
|
|
105
|
+
.option("--cycle <agents>", "參與的 agent,例如 claude,codex,gemini(順序不影響分工)")
|
|
106
106
|
.action(async (opts) => {
|
|
107
107
|
const requirement = opts.reqFile ? readFileSync(opts.reqFile, "utf8") : opts.req;
|
|
108
108
|
if (!requirement?.trim())
|
|
@@ -116,7 +116,7 @@ program
|
|
|
116
116
|
const branch = `flow/${id}`;
|
|
117
117
|
console.log(`[${id}] 🌿 從 ${base} 建立 worktree(分支 ${branch})`);
|
|
118
118
|
await addWorktree(root, worktreeDir(id), base, branch);
|
|
119
|
-
console.log(`[${id}] 🤝 agent
|
|
119
|
+
console.log(`[${id}] 🤝 參與的 agent:${cycle.join("、")}(角色隨機分配)`);
|
|
120
120
|
const cfg = loadRepoConfig();
|
|
121
121
|
const now = new Date().toISOString();
|
|
122
122
|
// worktree 一建好就寫入紀錄:之後在任何地方中斷,都能用 resume 接續或用 clean 清掉
|
|
@@ -241,13 +241,13 @@ function applyEdit(edit, done) {
|
|
|
241
241
|
for (const c of changes)
|
|
242
242
|
console.log(` ↳ ${c}`);
|
|
243
243
|
if (JSON.stringify(before.cycle) !== JSON.stringify(cfg.cycle)) {
|
|
244
|
-
console.log(" 已建立的 run
|
|
244
|
+
console.log(" 已建立的 run 會沿用建立時參與的 agent,不受影響");
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
const agent = program.command("agent").description("管理 agent 與 adapter 設定(寫入 flow.config.json)");
|
|
248
248
|
agent
|
|
249
249
|
.command("list")
|
|
250
|
-
.description("列出設定的 agent
|
|
250
|
+
.description("列出設定的 agent、是否已安裝、是否參與")
|
|
251
251
|
.action(async () => {
|
|
252
252
|
const cfg = loadRepoConfig();
|
|
253
253
|
const cycle = await resolveCycle().catch(() => cfg.cycle ?? []);
|
|
@@ -264,10 +264,10 @@ agent
|
|
|
264
264
|
def.extraArgs.length && `extraArgs=${def.extraArgs.join(" ")}`,
|
|
265
265
|
def.command && `command=${def.command.join(" ")}`,
|
|
266
266
|
].filter(Boolean);
|
|
267
|
-
console.log(`${ok ? "✅" : "❌"} ${name.padEnd(14)} ${pos >= 0 ?
|
|
267
|
+
console.log(`${ok ? "✅" : "❌"} ${name.padEnd(14)} ${pos >= 0 ? "參與" : "不參與"} ${detail.join(" ")}`);
|
|
268
268
|
}
|
|
269
269
|
console.log(`
|
|
270
|
-
|
|
270
|
+
參與的 agent:${cycle.length ? cycle.join("、") : "(沒有可用的 agent)"}${cfg.cycle ? "" : "(自動偵測)"}`);
|
|
271
271
|
});
|
|
272
272
|
agent
|
|
273
273
|
.command("add <name> [command...]")
|
|
@@ -276,7 +276,7 @@ agent
|
|
|
276
276
|
.option("--model <model>", "模型名稱")
|
|
277
277
|
.option("--extra-arg <arg>", "額外參數,可重複;以 - 開頭時寫成 --extra-arg=--sandbox", collect)
|
|
278
278
|
.action((name, command, opts) => {
|
|
279
|
-
applyEdit((cfg) => addAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已新增 ${name}
|
|
279
|
+
applyEdit((cfg) => addAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已新增 ${name};要讓它參與請用 agent cycle`);
|
|
280
280
|
});
|
|
281
281
|
agent
|
|
282
282
|
.command("set <name> [command...]")
|
|
@@ -289,19 +289,19 @@ agent
|
|
|
289
289
|
});
|
|
290
290
|
agent
|
|
291
291
|
.command("remove <name>")
|
|
292
|
-
.description("刪除 agent
|
|
292
|
+
.description("刪除 agent,一併從參與的 agent 移除")
|
|
293
293
|
.action((name) => applyEdit((cfg) => removeAgent(cfg, name), `已移除 ${name}`));
|
|
294
294
|
agent
|
|
295
295
|
.command("cycle [names]")
|
|
296
|
-
.description("
|
|
296
|
+
.description("顯示參與的 agent,或用逗號分隔設定新的名單")
|
|
297
297
|
.action(async (names) => {
|
|
298
298
|
if (!names) {
|
|
299
299
|
const cfg = loadRepoConfig();
|
|
300
|
-
console.log(`${(await resolveCycle()).join("
|
|
300
|
+
console.log(`${(await resolveCycle()).join("、")}${cfg.cycle ? "" : "(自動偵測)"}`);
|
|
301
301
|
return;
|
|
302
302
|
}
|
|
303
303
|
const list = names.split(",").map((s) => s.trim()).filter(Boolean);
|
|
304
|
-
applyEdit((cfg) => setCycle(cfg, list),
|
|
304
|
+
applyEdit((cfg) => setCycle(cfg, list), `參與的 agent 設為 ${list.join("、")}`);
|
|
305
305
|
const cfg = loadRepoConfig();
|
|
306
306
|
for (const n of list) {
|
|
307
307
|
if (!(await probeAgent(resolveAgent(cfg, n))))
|
|
@@ -310,7 +310,7 @@ agent
|
|
|
310
310
|
});
|
|
311
311
|
agent
|
|
312
312
|
.command("setup")
|
|
313
|
-
.description("互動式設定:偵測已安裝的 claude、codex、gemini
|
|
313
|
+
.description("互動式設定:偵測已安裝的 claude、codex、gemini,逐一選擇要不要加入並設定參與的 agent")
|
|
314
314
|
.action(async () => {
|
|
315
315
|
if (!stdin.isTTY)
|
|
316
316
|
throw new Error("agent setup 需要互動式終端機,請改用 agent add");
|
|
@@ -332,7 +332,7 @@ agent
|
|
|
332
332
|
console.log("");
|
|
333
333
|
await doctor();
|
|
334
334
|
});
|
|
335
|
-
/** 檢查設定的 agent
|
|
335
|
+
/** 檢查設定的 agent 是否已安裝,印出參與的 agent 與主要設定 */
|
|
336
336
|
async function doctor() {
|
|
337
337
|
const cfg = loadRepoConfig();
|
|
338
338
|
const names = Object.keys(cfg.agents);
|
|
@@ -344,7 +344,7 @@ async function doctor() {
|
|
|
344
344
|
console.log(`${ok ? "✅" : "❌"} ${name.padEnd(10)} adapter=${def.adapter}${def.model ? ` model=${def.model}` : ""}`);
|
|
345
345
|
}
|
|
346
346
|
try {
|
|
347
|
-
console.log(`\n
|
|
347
|
+
console.log(`\n參與的 agent:${(await resolveCycle()).join("、")}`);
|
|
348
348
|
}
|
|
349
349
|
catch (e) {
|
|
350
350
|
console.log(`\n${e.message}`);
|
|
@@ -353,7 +353,7 @@ async function doctor() {
|
|
|
353
353
|
console.log(`修正策略:${cfg.fixStrategy} 測試與實作分開:${cfg.tddSplit ? "是" : "否"}`);
|
|
354
354
|
console.log(`程式碼審查人數:${cfg.reviewQuorum} 計畫審查人數:${cfg.planReviewQuorum} 計畫仲裁:${cfg.planArbiter ? "開啟" : "關閉"}`);
|
|
355
355
|
}
|
|
356
|
-
program.command("doctor").description("檢查可用的 agent CLI
|
|
356
|
+
program.command("doctor").description("檢查可用的 agent CLI 與目前參與的 agent").action(doctor);
|
|
357
357
|
program
|
|
358
358
|
.command("list")
|
|
359
359
|
.description("列出這個專案的所有 run")
|
package/dist/config.js
CHANGED
package/dist/engine.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { config } from "./config.js";
|
|
5
|
+
import { arbitrationDecision } from "./arbitration.js";
|
|
5
6
|
import { detectProjectDefaults, withProjectDefaults } from "./detect.js";
|
|
6
7
|
import { changedFiles, commitAll, discardChanges, git, headCommit, resetTo } from "./git.js";
|
|
7
8
|
import { flowDir, logDir, projectRoot, runDir, worktreeDir } from "./paths.js";
|
|
@@ -38,7 +39,7 @@ async function agentStep(run, planned, step, prompt, mode) {
|
|
|
38
39
|
if (mode.kind === "review") {
|
|
39
40
|
throw new QuotaPause(`${agent} 的額度已用完;${step} 是審查步驟,不由另一家代打`);
|
|
40
41
|
}
|
|
41
|
-
const sub = availableAgent(run.cycle, agent, [...exhausted]);
|
|
42
|
+
const sub = availableAgent(run.cycle, agent, [...exhausted], `${run.id}:${step}:sub`);
|
|
42
43
|
if (!sub)
|
|
43
44
|
throw new QuotaPause(`所有 agent 的額度都已用完(${[...exhausted].join("、")})`);
|
|
44
45
|
const note = step.endsWith("-code") && sub === run.lastTestsAuthor ? "測試與實作由同一家負責" : undefined;
|
|
@@ -110,7 +111,7 @@ function loadOrderedTasks(run) {
|
|
|
110
111
|
}
|
|
111
112
|
// ───────────────────────── 各階段 ─────────────────────────
|
|
112
113
|
async function specStage(run) {
|
|
113
|
-
const agent = specAgent(run.cycle);
|
|
114
|
+
const agent = specAgent(run.cycle, run.id);
|
|
114
115
|
info(run, `📝 產生規格(${agent})`);
|
|
115
116
|
const { r } = await agentStep(run, agent, "spec", renderPrompt("spec", { requirement: run.requirement }), { kind: "write" });
|
|
116
117
|
await discardChanges(worktreeDir(run.id)); // 這個階段只允許寫 .flow/
|
|
@@ -170,7 +171,7 @@ function announceTasks(run, ordered) {
|
|
|
170
171
|
const cfg = loadRepoConfig();
|
|
171
172
|
info(run, `📋 共 ${ordered.length} 個任務:${ordered.map((t) => t.id).join(" → ")}`);
|
|
172
173
|
ordered.forEach((t, i) => {
|
|
173
|
-
const a = taskAgents(run.cycle, i, cfg.tddSplit);
|
|
174
|
+
const a = taskAgents(run.cycle, i, cfg.tddSplit, run.id);
|
|
174
175
|
info(run, ` ${t.id} 測試:${a.tests} 實作:${a.code}`);
|
|
175
176
|
});
|
|
176
177
|
}
|
|
@@ -182,10 +183,13 @@ function planSettled(run, key) {
|
|
|
182
183
|
if (!run.autopilot) {
|
|
183
184
|
info(run, `✋ 計畫已通過審查,請檢視 ${flowFile(run, "plan.md")},確認後執行 agentflowctl approve ${run.id}`);
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
+
const settled = succeed(run, key, next);
|
|
187
|
+
const attempts = { ...settled.attempts };
|
|
188
|
+
delete attempts["plan-arbitration"];
|
|
189
|
+
return { ...settled, attempts, taskIndex: 0, taskPhase: "tests" };
|
|
186
190
|
}
|
|
187
191
|
async function planStage(run) {
|
|
188
|
-
const agent = planAgent(run.cycle);
|
|
192
|
+
const agent = planAgent(run.cycle, run.id);
|
|
189
193
|
info(run, `🗺️ 拆解任務(${agent})`);
|
|
190
194
|
const cfg = loadRepoConfig();
|
|
191
195
|
const { r, agent: actual } = await agentStep(run, agent, "plan", renderPrompt("plan", { testPattern: cfg.testPattern }), { kind: "write" });
|
|
@@ -201,9 +205,9 @@ async function planStage(run) {
|
|
|
201
205
|
}
|
|
202
206
|
async function planReviewStage(run) {
|
|
203
207
|
const cfg = loadRepoConfig();
|
|
204
|
-
const author = run.planWriter ?? planAgent(run.cycle);
|
|
205
|
-
const panel = reviewers(run.cycle, author, cfg.planReviewQuorum);
|
|
208
|
+
const author = run.planWriter ?? planAgent(run.cycle, run.id);
|
|
206
209
|
const round = (run.attempts["plan-review"] ?? 0) + 1;
|
|
210
|
+
const panel = reviewers(run.cycle, author, cfg.planReviewQuorum, `${run.id}:plan-review:${round}`);
|
|
207
211
|
const issues = [];
|
|
208
212
|
const issueLines = []; // 只含意見內容、不含審查者名稱,用來偵測僵持
|
|
209
213
|
let firstObjector;
|
|
@@ -259,7 +263,12 @@ async function planReviewStage(run) {
|
|
|
259
263
|
}
|
|
260
264
|
async function planFixStage(run) {
|
|
261
265
|
const cfg = loadRepoConfig();
|
|
262
|
-
const agent = planFixAgent(run.cycle, {
|
|
266
|
+
const agent = planFixAgent(run.cycle, {
|
|
267
|
+
strategy: cfg.fixStrategy,
|
|
268
|
+
planWriter: run.planWriter,
|
|
269
|
+
planReviewer: run.planReviewer,
|
|
270
|
+
seed: `${run.id}:plan-fix:${run.attempts["plan-review"] ?? 0}`,
|
|
271
|
+
});
|
|
263
272
|
info(run, `✏️ 依 ${run.planReviewer ?? "審查者"} 的意見修改計畫(${agent})`);
|
|
264
273
|
const feedback = readFeedback(run);
|
|
265
274
|
const snap = snapshotPlan(run);
|
|
@@ -286,7 +295,8 @@ async function planFixStage(run) {
|
|
|
286
295
|
*/
|
|
287
296
|
async function arbitratePlan(run) {
|
|
288
297
|
const cfg = loadRepoConfig();
|
|
289
|
-
const
|
|
298
|
+
const arbitrationRound = (run.attempts["plan-arbitration"] ?? 0) + 1;
|
|
299
|
+
const panel = arbiterPanel(run.cycle, `${run.id}:arbiter`, run.planWriter, run.planReviewer);
|
|
290
300
|
const mode = panel.length > 1 ? "雙盲交叉仲裁" : "第三方仲裁";
|
|
291
301
|
const verdicts = [];
|
|
292
302
|
for (const arbiter of panel) {
|
|
@@ -302,7 +312,7 @@ async function arbitratePlan(run) {
|
|
|
302
312
|
const result = r.ok ? readJsonFile(flowFile(run, "plan-arbiter.json"), ReviewResult) : undefined;
|
|
303
313
|
if (result?.ok) {
|
|
304
314
|
mkdirSync(join(runDir(run.id), "reviews"), { recursive: true });
|
|
305
|
-
renameSync(flowFile(run, "plan-arbiter.json"), join(runDir(run.id), "reviews", `plan-arbiter-${arbiter}.json`));
|
|
315
|
+
renameSync(flowFile(run, "plan-arbiter.json"), join(runDir(run.id), "reviews", `plan-arbiter-${arbitrationRound}-${arbiter}.json`));
|
|
306
316
|
}
|
|
307
317
|
const verdict = result?.ok ? result.data.verdict : "abstain";
|
|
308
318
|
const notes = result?.ok ? result.data.items.map((i) => `- ${i.criterion}:${i.note}`) : [`- 未產生有效裁決(${r.ok ? "輸出格式錯誤" : r.summary})`];
|
|
@@ -312,13 +322,21 @@ async function arbitratePlan(run) {
|
|
|
312
322
|
rmSync(flowFile(run, "dispute.md"), { force: true });
|
|
313
323
|
const approvals = verdicts.filter((v) => v.verdict === "approve").length;
|
|
314
324
|
const unanimous = approvals === verdicts.length;
|
|
315
|
-
const decision =
|
|
325
|
+
const decision = arbitrationDecision(verdicts.map((v) => v.verdict), cfg.tieBreak, arbitrationRound, config.maxAttempts);
|
|
316
326
|
const summary = unanimous
|
|
317
327
|
? `${mode}一致核准`
|
|
318
328
|
: approvals === 0
|
|
319
|
-
? `${mode}
|
|
329
|
+
? `${mode}沒有任何一方核准${decision === "revise" ? ",交回計畫修訂" : ""}`
|
|
320
330
|
: `${mode}意見分歧,依 tieBreak=${cfg.tieBreak} ${cfg.tieBreak === "proceed" ? "繼續實作" : "停止"}`;
|
|
321
331
|
const record = verdicts.map((v) => `### ${v.arbiter}(${v.verdict})\n\n${v.notes.join("\n")}`).join("\n\n");
|
|
332
|
+
if (decision === "revise") {
|
|
333
|
+
const attempts = { ...run.attempts, "plan-arbitration": arbitrationRound };
|
|
334
|
+
delete attempts["plan-review"];
|
|
335
|
+
rmSync(flowFile(run, "plan-review-last.txt"), { force: true });
|
|
336
|
+
writeFileSync(flowFile(run, "feedback.md"), `# 仲裁要求修訂(第 ${arbitrationRound} 次)\n\n${summary}\n\n${record}\n`);
|
|
337
|
+
info(run, ` → ${summary}`);
|
|
338
|
+
return { ...run, attempts, stage: "plan_fix" };
|
|
339
|
+
}
|
|
322
340
|
writeFileSync(flowFile(run, "plan.md"), `${readFileSync(flowFile(run, "plan.md"), "utf8")}\n\n## 仲裁紀錄\n\n**結果:${summary}**\n\n${record}\n`);
|
|
323
341
|
if (decision === "proceed") {
|
|
324
342
|
info(run, ` → ${summary}`);
|
|
@@ -339,7 +357,7 @@ async function implementStage(run) {
|
|
|
339
357
|
const testCmd = `${cfg.install} && ${cfg.test}`;
|
|
340
358
|
const progress = `${run.taskIndex + 1}/${tasks.length} ${task.id} ${task.title}`;
|
|
341
359
|
const taskJson = JSON.stringify(task, null, 2);
|
|
342
|
-
const agents = taskAgents(run.cycle, run.taskIndex, cfg.tddSplit);
|
|
360
|
+
const agents = taskAgents(run.cycle, run.taskIndex, cfg.tddSplit, run.id);
|
|
343
361
|
// ── 紅燈:只寫測試,而且測試必須失敗 ──
|
|
344
362
|
if (run.taskPhase === "tests") {
|
|
345
363
|
const key = `${task.id}:tests`;
|
|
@@ -424,7 +442,13 @@ async function verifyStage(run) {
|
|
|
424
442
|
async function fixStage(run) {
|
|
425
443
|
const cfg = loadRepoConfig();
|
|
426
444
|
const source = run.fixSource ?? "verify";
|
|
427
|
-
const agent = fixAgent(run.cycle, {
|
|
445
|
+
const agent = fixAgent(run.cycle, {
|
|
446
|
+
source,
|
|
447
|
+
strategy: cfg.fixStrategy,
|
|
448
|
+
lastWriter: run.lastWriter,
|
|
449
|
+
lastReviewer: run.lastReviewer,
|
|
450
|
+
seed: `${run.id}:fix:${run.attempts.review ?? 0}`,
|
|
451
|
+
});
|
|
428
452
|
const why = source === "review" ? `依 ${run.lastReviewer ?? "reviewer"} 的審查意見` : "修正驗證錯誤";
|
|
429
453
|
info(run, `🩹 ${why}(${agent})`);
|
|
430
454
|
const repo = worktreeDir(run.id);
|
|
@@ -449,7 +473,7 @@ async function fixStage(run) {
|
|
|
449
473
|
async function reviewStage(run) {
|
|
450
474
|
const cfg = loadRepoConfig();
|
|
451
475
|
const repo = worktreeDir(run.id);
|
|
452
|
-
const panel = reviewers(run.cycle, run.lastWriter, cfg.reviewQuorum);
|
|
476
|
+
const panel = reviewers(run.cycle, run.lastWriter, cfg.reviewQuorum, `${run.id}:review:${run.attempts.review ?? 0}`);
|
|
453
477
|
writeFileSync(flowFile(run, "diff.patch"), await git(repo, "diff", `${run.baseBranch}...HEAD`));
|
|
454
478
|
const authors = [...new Set((await git(repo, "log", "--format=%s", `${run.baseBranch}..HEAD`)).match(/\[[^\]]+\]$/gm) ?? [])]
|
|
455
479
|
.map((s) => s.slice(1, -1));
|
package/dist/roles.js
CHANGED
|
@@ -2,78 +2,109 @@
|
|
|
2
2
|
* 決定每個步驟由哪個 agent 執行。
|
|
3
3
|
*
|
|
4
4
|
* 規則只有三條:
|
|
5
|
-
* 1. 審查者不能是最後寫程式的 agent
|
|
5
|
+
* 1. 審查者不能是最後寫程式的 agent,多位審查者彼此不重複。
|
|
6
6
|
* 2. 開啟 tddSplit 時,同一個任務的測試與實作由不同 agent 負責。
|
|
7
|
-
* 3.
|
|
7
|
+
* 3. 人選隨機決定,不依 cycle 的順序。cycle 只代表有哪些 agent 參與。
|
|
8
|
+
*
|
|
9
|
+
* 隨機以 seed(run id 加上步驟與輪次)決定:同一個 run 的同一步驟重算時會得到同樣的人,
|
|
10
|
+
* 所以 resume、或同一步驟在不同地方重算時結果一致。
|
|
8
11
|
*/
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (!exclude.includes(candidate))
|
|
16
|
-
return candidate;
|
|
12
|
+
/** FNV-1a 雜湊:把 seed 字串轉成 32 位元整數 */
|
|
13
|
+
function hash(seed) {
|
|
14
|
+
let h = 0x811c9dc5;
|
|
15
|
+
for (let i = 0; i < seed.length; i++) {
|
|
16
|
+
h ^= seed.charCodeAt(i);
|
|
17
|
+
h = Math.imul(h, 0x01000193);
|
|
17
18
|
}
|
|
18
|
-
return
|
|
19
|
+
return h >>> 0;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
/** mulberry32:由 seed 決定的亂數產生器 */
|
|
22
|
+
function rng(seed) {
|
|
23
|
+
let a = hash(seed);
|
|
24
|
+
return () => {
|
|
25
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
26
|
+
let t = a;
|
|
27
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
28
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
29
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
30
|
+
};
|
|
26
31
|
}
|
|
27
|
-
/**
|
|
28
|
-
export function
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
for (let i =
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
break;
|
|
35
|
-
picked.push(next);
|
|
36
|
-
cursor = next;
|
|
32
|
+
/** 由 seed 決定的洗牌,不改動原陣列 */
|
|
33
|
+
export function shuffled(items, seed) {
|
|
34
|
+
const out = [...items];
|
|
35
|
+
const rand = rng(seed);
|
|
36
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
37
|
+
const j = Math.floor(rand() * (i + 1));
|
|
38
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
37
39
|
}
|
|
38
|
-
return
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** 隨機挑一位不在 exclude 裡的 agent;全部被排除時退回任一位(單一 agent 也能運作) */
|
|
43
|
+
export function pick(cycle, seed, exclude = []) {
|
|
44
|
+
const order = shuffled(cycle, seed);
|
|
45
|
+
return order.find((c) => !exclude.includes(c)) ?? order[0];
|
|
46
|
+
}
|
|
47
|
+
/** 規格與計畫由同一位撰寫,計畫審查才能同時避開兩者的作者 */
|
|
48
|
+
export const specAgent = (cycle, seed) => pick(cycle, `${seed}:author`);
|
|
49
|
+
export const planAgent = specAgent;
|
|
50
|
+
/**
|
|
51
|
+
* 每 cycle.length 個任務為一輪,每輪洗一次牌,所以每家輪完一次才會再輪到;
|
|
52
|
+
* 換輪時若新一輪第一位和上一輪最後一位相同,就把它往後移,避免同一家連續撰寫測試。
|
|
53
|
+
*/
|
|
54
|
+
function taskBag(cycle, seed, bag) {
|
|
55
|
+
const order = shuffled(cycle, `${seed}:tasks:${bag}`);
|
|
56
|
+
if (bag === 0 || order.length < 2)
|
|
57
|
+
return order;
|
|
58
|
+
const prevLast = taskBag(cycle, seed, bag - 1).at(-1);
|
|
59
|
+
return order[0] === prevLast ? [...order.slice(1), order[0]] : order;
|
|
60
|
+
}
|
|
61
|
+
/** 第 i 個任務的測試作者從洗好的牌依序取;實作者隨機挑一位測試作者以外的 agent */
|
|
62
|
+
export function taskAgents(cycle, taskIndex, tddSplit, seed) {
|
|
63
|
+
const n = cycle.length;
|
|
64
|
+
const tests = taskBag(cycle, seed, Math.floor(taskIndex / n))[taskIndex % n];
|
|
65
|
+
return { tests, code: tddSplit ? pick(cycle, `${seed}:code:${taskIndex}`, [tests]) : tests };
|
|
66
|
+
}
|
|
67
|
+
/** 隨機挑出 quorum 位彼此不重複、而且不是作者的 reviewer */
|
|
68
|
+
export function reviewers(cycle, lastWriter, quorum, seed) {
|
|
69
|
+
const picked = shuffled(cycle.filter((c) => c !== lastWriter), seed).slice(0, quorum);
|
|
70
|
+
return picked.length ? picked : [lastWriter ?? cycle[0]];
|
|
39
71
|
}
|
|
40
72
|
/**
|
|
41
73
|
* 誰來修正:
|
|
42
74
|
* - verify 失敗(型別、lint、建置這類機械性錯誤):交給最後寫程式的 agent
|
|
43
|
-
* - review 要求修改:ring
|
|
75
|
+
* - review 要求修改:ring 策略隨機挑一位 reviewer 以外的 agent;author 策略交回作者
|
|
44
76
|
*/
|
|
45
77
|
export function fixAgent(cycle, opts) {
|
|
46
78
|
if (opts.source === "verify" || opts.strategy === "author")
|
|
47
79
|
return opts.lastWriter ?? cycle[0];
|
|
48
|
-
return
|
|
80
|
+
return pick(cycle, opts.seed, opts.lastReviewer ? [opts.lastReviewer] : []);
|
|
49
81
|
}
|
|
50
|
-
/** 計畫修正者:ring
|
|
82
|
+
/** 計畫修正者:ring 策略隨機挑一位審查者以外的 agent;author 策略交回計畫作者 */
|
|
51
83
|
export function planFixAgent(cycle, opts) {
|
|
52
84
|
if (opts.strategy === "author")
|
|
53
85
|
return opts.planWriter ?? cycle[0];
|
|
54
|
-
return
|
|
86
|
+
return pick(cycle, opts.seed, opts.planReviewer ? [opts.planReviewer] : []);
|
|
55
87
|
}
|
|
56
88
|
/**
|
|
57
89
|
* 仲裁小組:
|
|
58
|
-
* - 有第三方(作者與最後審查者以外的 agent)→
|
|
90
|
+
* - 有第三方(作者與最後審查者以外的 agent)→ 隨機挑一位第三方單獨仲裁
|
|
59
91
|
* - 只有兩家 → 兩家各自在全新 context 中雙盲仲裁,避免讓一直反對的審查者同時當裁判
|
|
60
92
|
* - 只有一家 → 只能由它自己仲裁
|
|
61
93
|
*/
|
|
62
|
-
export function arbiterPanel(cycle, writer, reviewer) {
|
|
94
|
+
export function arbiterPanel(cycle, seed, writer, reviewer) {
|
|
63
95
|
const involved = [writer, reviewer].filter((x) => Boolean(x));
|
|
64
|
-
const third = cycle.find((c) => !involved.includes(c));
|
|
96
|
+
const third = shuffled(cycle, seed).find((c) => !involved.includes(c));
|
|
65
97
|
if (third)
|
|
66
98
|
return [third];
|
|
67
99
|
return [...new Set(cycle)];
|
|
68
100
|
}
|
|
69
101
|
/**
|
|
70
|
-
* 代打人選:原本的 agent
|
|
102
|
+
* 代打人選:原本的 agent 可用就用它;額度用完時,隨機挑一位還有額度的 agent。
|
|
71
103
|
* 全部都用完時回傳 undefined。
|
|
72
104
|
*/
|
|
73
|
-
export function availableAgent(cycle, preferred, exhausted) {
|
|
105
|
+
export function availableAgent(cycle, preferred, exhausted, seed) {
|
|
74
106
|
if (!exhausted.includes(preferred))
|
|
75
107
|
return preferred;
|
|
76
|
-
|
|
77
|
-
return exhausted.includes(next) ? undefined : next;
|
|
108
|
+
return shuffled(cycle, seed).find((c) => !exhausted.includes(c));
|
|
78
109
|
}
|
|
79
110
|
//# sourceMappingURL=roles.js.map
|
package/dist/runner.js
CHANGED
|
@@ -59,7 +59,8 @@ export function resolveAgent(cfg, name) {
|
|
|
59
59
|
const def = cfg.agents[name];
|
|
60
60
|
if (!def)
|
|
61
61
|
throw new Error(`未定義的 agent:${name}(請先用 agent add ${name} --adapter <adapter> 新增)`);
|
|
62
|
-
|
|
62
|
+
const defaultModel = def.adapter === "command" ? undefined : cfg.defaultModels[def.adapter];
|
|
63
|
+
return { ...def, model: def.model ?? defaultModel };
|
|
63
64
|
}
|
|
64
65
|
/** 確認某個 agent 的 CLI 是否可以執行 */
|
|
65
66
|
export async function probeAgent(def) {
|
package/dist/schemas.js
CHANGED
|
@@ -39,7 +39,7 @@ export const ReviewResult = z.object({
|
|
|
39
39
|
note: z.string().default(""),
|
|
40
40
|
})),
|
|
41
41
|
});
|
|
42
|
-
/** 一個 agent 的定義;名稱(agents 的 key
|
|
42
|
+
/** 一個 agent 的定義;名稱(agents 的 key)用在 cycle 裡 */
|
|
43
43
|
export const AgentDef = z.object({
|
|
44
44
|
adapter: z.enum(["claude", "codex", "gemini", "command"]),
|
|
45
45
|
model: z.string().optional(),
|
|
@@ -51,7 +51,13 @@ export const AgentDef = z.object({
|
|
|
51
51
|
export const RepoConfig = z.object({
|
|
52
52
|
/** 可用的 agent;沒有內建,全部都要在這裡定義(通常用 agent add) */
|
|
53
53
|
agents: z.record(z.string(), AgentDef).default({}),
|
|
54
|
-
/**
|
|
54
|
+
/** 各 CLI adapter 的預設模型;agent 自己指定 model 時優先使用個別設定 */
|
|
55
|
+
defaultModels: z.strictObject({
|
|
56
|
+
claude: z.string().trim().min(1).optional(),
|
|
57
|
+
codex: z.string().trim().min(1).optional(),
|
|
58
|
+
gemini: z.string().trim().min(1).optional(),
|
|
59
|
+
}).default({}),
|
|
60
|
+
/** 參與的 agent(順序不影響分工);未設定時取 agents 裡已安裝的 CLI */
|
|
55
61
|
cycle: z.array(z.string()).min(1).optional(),
|
|
56
62
|
/** review 後的修正由誰做:ring=輪到下一位;author=最後寫程式的 agent */
|
|
57
63
|
fixStrategy: z.enum(["ring", "author"]).default("ring"),
|
|
@@ -94,7 +100,7 @@ export const FlowRun = z.object({
|
|
|
94
100
|
/** 暫停前所在的階段與原因(額度用完時) */
|
|
95
101
|
pausedStage: Stage.optional(),
|
|
96
102
|
pauseReason: z.string().optional(),
|
|
97
|
-
/** 這個 run
|
|
103
|
+
/** 這個 run 參與的 agent(建立時決定,resume 時沿用) */
|
|
98
104
|
cycle: z.array(z.string()).min(1),
|
|
99
105
|
/** 最後一個寫程式的 agent,reviewer 不可以是它 */
|
|
100
106
|
lastWriter: z.string().optional(),
|
package/dist/setup.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function runSetup(initial, deps) {
|
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
48
|
for (;;) {
|
|
49
|
-
const list = (await ask("
|
|
49
|
+
const list = (await ask("參與的 agent,用逗號分隔", chosen.join(","))).split(",").map((s) => s.trim()).filter(Boolean);
|
|
50
50
|
try {
|
|
51
51
|
cfg = setCycle(cfg, list).cfg;
|
|
52
52
|
break;
|
|
@@ -59,7 +59,7 @@ export async function runSetup(initial, deps) {
|
|
|
59
59
|
log("\n即將寫入:");
|
|
60
60
|
for (const name of chosen)
|
|
61
61
|
log(` ${name.padEnd(14)} adapter=${agents[name].adapter}${agents[name].model ? ` model=${agents[name].model}` : ""}`);
|
|
62
|
-
log(`
|
|
62
|
+
log(` 參與的 agent:${cfg.cycle.join("、")}`);
|
|
63
63
|
log(" 需要自訂指令的 CLI 請改用 agent add <name> --adapter command -- <指令>");
|
|
64
64
|
if (!(await confirm("寫入 flow.config.json?", true))) {
|
|
65
65
|
log("已取消,設定沒有變更");
|