agentflowctl 0.1.2 → 0.3.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 +63 -10
- package/dist/agentConfig.js +111 -0
- package/dist/agents/claude.js +2 -3
- package/dist/agents/codex.js +6 -3
- package/dist/agents/gemini.js +2 -2
- package/dist/agents/index.js +2 -0
- package/dist/agents/types.js +14 -0
- package/dist/cli.js +91 -18
- package/dist/engine.js +16 -16
- package/dist/runner.js +40 -14
- package/dist/schemas.js +3 -10
- package/dist/store.js +10 -19
- package/examples/flow.config.json +1 -5
- package/package.json +1 -1
- package/prompts/fix.md +30 -9
- package/prompts/implement-code.md +32 -9
- package/prompts/implement-tests.md +29 -6
- package/prompts/plan-arbiter.md +31 -10
- package/prompts/plan-fix.md +29 -8
- package/prompts/plan-review.md +31 -10
- package/prompts/plan.md +34 -9
- package/prompts/review.md +29 -8
- package/prompts/spec.md +32 -7
- package/examples/github-actions.yml +0 -45
package/README.md
CHANGED
|
@@ -93,11 +93,25 @@ agentflowctl clean f-xxxx # 移除 worktree 與 run 紀錄,分支保
|
|
|
93
93
|
| `--base` | 基底分支,預設為目前分支 |
|
|
94
94
|
| `--cycle` | 這次 run 的輪替順序,例如 `claude,codex,gemini`;建立後就固定,`resume` 沿用 |
|
|
95
95
|
| `--max-agent-runs` | 這次 run 的 agent 執行次數上限 |
|
|
96
|
-
| `--budget` | 估計花費上限(美元);使用 API 計費時才需要 |
|
|
97
96
|
| `--manual-plan` | 計畫通過審查後進入 `awaiting_approval`,等 `approve` 才開始實作 |
|
|
98
97
|
|
|
99
98
|
`status` 會列出任務。進行中的任務會標出正在寫測試還是正在寫實作。
|
|
100
99
|
|
|
100
|
+
### 執行中的終端機輸出
|
|
101
|
+
|
|
102
|
+
agent 每次使用工具,都會印出完整的指令或主要參數,不截斷。多行指令的後續行會縮排對齊。install、測試、verify 這些專案指令也會以 `$ ` 開頭印出來:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
💬 [claude] 先讀現有的表單元件
|
|
106
|
+
🔧 [claude] Read: /repo/.agentflowctl/worktrees/f-xxxx/src/LoginForm.tsx
|
|
107
|
+
🔧 [claude] Bash: pnpm vitest run src/LoginForm.test.tsx
|
|
108
|
+
🔧 [codex] shell: bash -lc 'pnpm test'
|
|
109
|
+
🔧 [gemini] run_shell_command: npm run lint
|
|
110
|
+
$ pnpm install
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
工具參數依序取 command、檔案路徑、path、pattern、url、query,都沒有時印出整包 JSON。每次執行的完整輸出都在 `agentflowctl logs <id>`。
|
|
114
|
+
|
|
101
115
|
## 設定
|
|
102
116
|
|
|
103
117
|
專案根目錄的 `flow.config.json`。完整範例見 `examples/flow.config.json`。未提供的欄位使用內建預設(安裝指令、測試指令、檢查清單預設對應 Vite + TypeScript + Vitest)。
|
|
@@ -109,7 +123,7 @@ agentflowctl clean f-xxxx # 移除 worktree 與 run 紀錄,分支保
|
|
|
109
123
|
"tddSplit": true,
|
|
110
124
|
"tieBreak": "proceed",
|
|
111
125
|
"agents": {
|
|
112
|
-
"codex": { "adapter": "codex", "model": "你要用的模型"
|
|
126
|
+
"codex": { "adapter": "codex", "model": "你要用的模型" },
|
|
113
127
|
"aider": { "adapter": "command", "command": ["aider", "--yes-always", "--no-auto-commits", "--message", "{prompt}"] }
|
|
114
128
|
}
|
|
115
129
|
}
|
|
@@ -124,13 +138,37 @@ agentflowctl clean f-xxxx # 移除 worktree 與 run 紀錄,分支保
|
|
|
124
138
|
| `planReviewQuorum` | `1` | 計畫需要幾位不同審查者都 `approve` |
|
|
125
139
|
| `planArbiter` | `true` | 計畫審查僵持時交付仲裁。關掉之後,僵持會直接讓 run 失敗 |
|
|
126
140
|
| `tieBreak` | `proceed` | 兩家仲裁意見分歧時:`proceed` 繼續並記錄爭議;`stop` 停下 |
|
|
127
|
-
| `auth` | `subscription` | `subscription` 移除子程序裡的 API key;`api` 保留,給 CI 用 |
|
|
128
141
|
| `maxAgentRuns` | `60` | 單一 run 最多執行幾次 agent |
|
|
129
142
|
| `install` / `test` / `checks` | 見 `src/schemas.ts` | verify 階段實際執行的指令 |
|
|
130
143
|
| `agents` | 內建 claude、codex、gemini | 覆寫內建 agent,或用 `command` adapter 接上其他 CLI |
|
|
144
|
+
| `removedAgents` | `[]` | 移除的內建 agent,不會被自動偵測、不能放進輪替。通常用 `agent remove` 寫入 |
|
|
131
145
|
|
|
132
146
|
verify 失敗(型別、lint、建置)一律交回最後作者。審查意見才依 `fixStrategy` 決定修正者。
|
|
133
147
|
|
|
148
|
+
### 用指令管理 agent
|
|
149
|
+
|
|
150
|
+
`agents` 與 `cycle` 也可以用 `agent` 指令修改,不必手動編輯 JSON。每次寫入前都會先驗證整份設定:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
agentflowctl agent list # 內建與自訂 agent、是否已安裝、輪替位置
|
|
154
|
+
agentflowctl agent add claude-strong --adapter claude --model opus
|
|
155
|
+
agentflowctl agent add aider --adapter command -- aider --yes-always --message {prompt}
|
|
156
|
+
agentflowctl agent set codex --model 你要用的模型 --extra-arg=--search
|
|
157
|
+
agentflowctl agent set aider --adapter gemini # 換 adapter
|
|
158
|
+
agentflowctl agent remove aider
|
|
159
|
+
agentflowctl agent remove gemini # 內建的也能移除
|
|
160
|
+
agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯示目前的順序
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
修改會連帶更新相關設定,並在終端機列出:
|
|
164
|
+
|
|
165
|
+
- `set --adapter` 換 adapter 時,會清掉舊 adapter 的 `model`、`extraArgs`、`command`,這次有重新指定的除外。
|
|
166
|
+
- `remove` 會一併從 `cycle` 移除。`cycle` 變空就刪除這個欄位,改回自動偵測。
|
|
167
|
+
- 內建的 claude、codex、gemini 也能 `remove`:覆寫設定會一起刪掉,名稱記在 `removedAgents`,之後自動偵測會跳過它,也不能放進 `cycle`。要加回來用 `agent add gemini --adapter gemini`。
|
|
168
|
+
- `--extra-arg` 可以重複指定,會整個取代原本的 `extraArgs`。參數以 `-` 開頭時,寫成 `--extra-arg=--sandbox`。
|
|
169
|
+
|
|
170
|
+
已建立的 run 會沿用建立時的輪替順序,不受這些修改影響。
|
|
171
|
+
|
|
134
172
|
## 計畫怎麼在沒有人的情況下通過
|
|
135
173
|
|
|
136
174
|
人工確認計畫是為了擋住方向錯了還一路做下去。預設用三層機制取代它;加上 `--manual-plan` 時,三層都過了仍會停下來等你。
|
|
@@ -169,6 +207,23 @@ verify 失敗(型別、lint、建置)一律交回最後作者。審查意見
|
|
|
169
207
|
|
|
170
208
|
驗收條件寫在 `.flow/acceptance.json`(`AC-1`…),任務寫在 `.flow/tasks.json`(`T-1`…)。
|
|
171
209
|
|
|
210
|
+
## Prompt 結構
|
|
211
|
+
|
|
212
|
+
每個階段的 prompt 都有專屬角色:需求分析師、軟體架構師、計畫審查者、計畫修訂者、中立仲裁者、測試工程師、實作工程師、除錯工程師、程式碼審查者。內容用 XML 標籤分段:`<role>`、`<context>`、`<inputs>`、`<steps>`、`<constraints>`、`<output_format>`、`<reply_format>`。
|
|
213
|
+
|
|
214
|
+
Agent 的最後回覆要附上 XML 中繼資料:
|
|
215
|
+
|
|
216
|
+
```xml
|
|
217
|
+
<result>
|
|
218
|
+
<status>done 或 blocked</status>
|
|
219
|
+
<summary>做了什麼</summary>
|
|
220
|
+
<files_changed><file>src/form.ts</file></files_changed>
|
|
221
|
+
<concerns>對規格或測試的疑慮</concerns>
|
|
222
|
+
</result>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`blocked` 與 `concerns` 會印在終端機上,完整回覆留在 log。這份中繼資料只給人看;缺少或格式錯誤都不影響流程,是否通過仍由上表的程式檢查決定。
|
|
226
|
+
|
|
172
227
|
## Adapter
|
|
173
228
|
|
|
174
229
|
| adapter | 執行方式 | 權限 |
|
|
@@ -184,11 +239,11 @@ Codex 沙箱預設不能連網,所以建立 worktree 時會先跑 `install`。
|
|
|
184
239
|
|
|
185
240
|
## 登入、額度與代打
|
|
186
241
|
|
|
187
|
-
|
|
242
|
+
只支援各家 CLI 的訂閱登入。
|
|
188
243
|
|
|
189
|
-
執行 agent
|
|
244
|
+
執行 agent 時,一律從子程序環境移除 `ANTHROPIC_API_KEY`、`ANTHROPIC_AUTH_TOKEN`、`CODEX_API_KEY`、`OPENAI_API_KEY`、`GEMINI_API_KEY`、`GOOGLE_API_KEY`,避免環境裡的 key 蓋過訂閱登入。`doctor` 發現這些變數時會提醒。專案指令(install、test、build)不受影響。
|
|
190
245
|
|
|
191
|
-
上限是執行次數(`maxAgentRuns`,預設 60
|
|
246
|
+
上限是執行次數(`maxAgentRuns`,預設 60),不是金額。`status` 會列出各 agent 的執行次數與 token 數。
|
|
192
247
|
|
|
193
248
|
額度用完時:
|
|
194
249
|
|
|
@@ -204,8 +259,6 @@ Codex 沙箱預設不能連網,所以建立 worktree 時會先跑 `install`。
|
|
|
204
259
|
|
|
205
260
|
額度錯誤靠錯誤訊息辨識(usage limit、rate limit、quota、429 等),只在 agent 執行失敗時判斷。辨識不到時,會當成一般失敗重試。
|
|
206
261
|
|
|
207
|
-
CI 無法使用訂閱登入時,設 `"auth": "api"` 保留 API key,並用 `--budget` 設估計花費上限。Codex、Gemini 只回報 token,要在 `agents.<name>.pricing` 設定價格才會算進預算。GitHub Actions 範例見 `examples/github-actions.yml`。
|
|
208
|
-
|
|
209
262
|
## 在哪裡跑
|
|
210
263
|
|
|
211
264
|
agentflowctl 本身只依賴 Node.js 與 git。專案指令透過系統 shell 執行。
|
|
@@ -213,7 +266,7 @@ agentflowctl 本身只依賴 Node.js 與 git。專案指令透過系統 shell
|
|
|
213
266
|
| 環境 | 適合的用法 |
|
|
214
267
|
|---|---|
|
|
215
268
|
| 自己的電腦 | 自己的專案、自己寫的需求。剛開始可以加 `--manual-plan`,確認審查品質後再拿掉 |
|
|
216
|
-
|
|
|
269
|
+
| 容器、遠端開發機 | 無人值守。先在該環境內完成各家 CLI 的訂閱登入 |
|
|
217
270
|
| Claude Code、Codex 裡面 | 讓它們用 shell 執行 `npx agentflowctl` |
|
|
218
271
|
|
|
219
272
|
沒有容器隔離時,verify 會在你的電腦上執行 agent 寫出來的程式碼。Gemini 在無人值守時是 yolo 模式。處理外部 issue,或需求文字不是你自己寫的,放到可丟棄的環境。AI 審查計畫擋不住夾在需求裡的指示。
|
|
@@ -228,7 +281,7 @@ src/
|
|
|
228
281
|
runner.ts 執行 agent、正規化結果、執行專案指令
|
|
229
282
|
agents/ claude、codex、gemini、command
|
|
230
283
|
git.ts worktree 與 git 操作
|
|
231
|
-
store.ts
|
|
284
|
+
store.ts 狀態、用量、代打紀錄
|
|
232
285
|
tasks.ts 任務 DAG
|
|
233
286
|
schemas.ts zod schema
|
|
234
287
|
prompts/ 各階段 prompt
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { builtinAgents, DEFAULT_CYCLE } from "./agents/index.js";
|
|
4
|
+
import { AgentDef, RepoConfig } from "./schemas.js";
|
|
5
|
+
/** 這些欄位的意義取決於 adapter,換 adapter 時要清掉 */
|
|
6
|
+
const ADAPTER_FIELDS = ["model", "extraArgs", "command"];
|
|
7
|
+
const isBuiltin = (name) => DEFAULT_CYCLE.includes(name);
|
|
8
|
+
const agentsOf = (cfg) => ({ ...(cfg.agents ?? {}) });
|
|
9
|
+
const removedOf = (cfg) => (cfg.removedAgents ?? []);
|
|
10
|
+
const isDefined = (cfg, name) => builtinAgents(removedOf(cfg)).includes(name) || name in agentsOf(cfg);
|
|
11
|
+
/** 寫回 removedAgents;清單變空就刪掉欄位 */
|
|
12
|
+
function withRemoved(cfg, removed) {
|
|
13
|
+
const next = { ...cfg, removedAgents: removed };
|
|
14
|
+
if (!removed.length)
|
|
15
|
+
delete next.removedAgents;
|
|
16
|
+
return next;
|
|
17
|
+
}
|
|
18
|
+
/** 只留下有值的欄位,驗證後回傳 */
|
|
19
|
+
function buildAgent(base, patch) {
|
|
20
|
+
const next = { ...base };
|
|
21
|
+
for (const [k, v] of Object.entries(patch))
|
|
22
|
+
if (v !== undefined)
|
|
23
|
+
next[k] = v;
|
|
24
|
+
const parsed = AgentDef.safeParse(next);
|
|
25
|
+
if (!parsed.success)
|
|
26
|
+
throw new Error(`agent 設定不合法:\n${z.prettifyError(parsed.error)}`);
|
|
27
|
+
if (parsed.data.adapter === "command" && !parsed.data.command?.length) {
|
|
28
|
+
throw new Error("command adapter 需要指令,請寫在 -- 後面,例如:-- aider --message {prompt}");
|
|
29
|
+
}
|
|
30
|
+
if (parsed.data.adapter !== "command" && parsed.data.command)
|
|
31
|
+
throw new Error("只有 command adapter 可以設定 command");
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
export function addAgent(cfg, name, def) {
|
|
35
|
+
if (!/^[\w-]+$/.test(name))
|
|
36
|
+
throw new Error(`agent 名稱只能用英數字、底線與連字號:${name}`);
|
|
37
|
+
if (isDefined(cfg, name))
|
|
38
|
+
throw new Error(`agent ${name} 已存在,要修改請用 agent set`);
|
|
39
|
+
const { adapter, ...patch } = def;
|
|
40
|
+
const next = { ...cfg, agents: { ...agentsOf(cfg), [name]: buildAgent({ adapter }, patch) } };
|
|
41
|
+
// 加回先前移除的內建 agent
|
|
42
|
+
const removed = removedOf(cfg);
|
|
43
|
+
return { cfg: removed.includes(name) ? withRemoved(next, removed.filter((n) => n !== name)) : next, changes: [] };
|
|
44
|
+
}
|
|
45
|
+
export function setAgent(cfg, name, patch) {
|
|
46
|
+
if (!isDefined(cfg, name))
|
|
47
|
+
throw new Error(`未定義的 agent:${name}`);
|
|
48
|
+
if (Object.values(patch).every((v) => v === undefined)) {
|
|
49
|
+
throw new Error("沒有要修改的欄位(--adapter、--model、--extra-arg 或 -- <command>)");
|
|
50
|
+
}
|
|
51
|
+
const agents = agentsOf(cfg);
|
|
52
|
+
// 內建 agent 第一次修改時,新增一筆覆寫設定
|
|
53
|
+
const base = { ...(agents[name] ?? { adapter: name }) };
|
|
54
|
+
const changes = [];
|
|
55
|
+
if (patch.adapter !== undefined && patch.adapter !== base.adapter) {
|
|
56
|
+
const cleared = ADAPTER_FIELDS.filter((f) => base[f] !== undefined && patch[f] === undefined);
|
|
57
|
+
for (const f of cleared)
|
|
58
|
+
delete base[f];
|
|
59
|
+
if (cleared.length)
|
|
60
|
+
changes.push(`adapter 從 ${String(base.adapter)} 換成 ${patch.adapter},已清掉舊的 ${cleared.join("、")}`);
|
|
61
|
+
}
|
|
62
|
+
return { cfg: { ...cfg, agents: { ...agents, [name]: buildAgent(base, patch) } }, changes };
|
|
63
|
+
}
|
|
64
|
+
export function removeAgent(cfg, name) {
|
|
65
|
+
if (!isDefined(cfg, name))
|
|
66
|
+
throw new Error(`未定義的 agent:${name}`);
|
|
67
|
+
const agents = agentsOf(cfg);
|
|
68
|
+
delete agents[name];
|
|
69
|
+
// 內建 agent 沒有設定也存在,要記在 removedAgents 才算移除;覆寫設定一併刪掉
|
|
70
|
+
let next = { ...cfg, agents };
|
|
71
|
+
if (isBuiltin(name))
|
|
72
|
+
next = withRemoved(next, [...removedOf(cfg), name]);
|
|
73
|
+
const changes = [];
|
|
74
|
+
const cycle = cfg.cycle;
|
|
75
|
+
if (cycle?.includes(name)) {
|
|
76
|
+
const rest = cycle.filter((n) => n !== name);
|
|
77
|
+
if (rest.length) {
|
|
78
|
+
next.cycle = rest;
|
|
79
|
+
changes.push(`已從輪替順序移除,現在是 ${rest.join(" → ")}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
delete next.cycle;
|
|
83
|
+
changes.push("輪替順序因此變空,已刪除 cycle,改回自動偵測已安裝的 CLI");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { cfg: next, changes };
|
|
87
|
+
}
|
|
88
|
+
export function setCycle(cfg, names) {
|
|
89
|
+
if (!names.length)
|
|
90
|
+
throw new Error("輪替順序至少要有一個 agent");
|
|
91
|
+
const dup = names.find((n, i) => names.indexOf(n) !== i);
|
|
92
|
+
if (dup)
|
|
93
|
+
throw new Error(`輪替順序裡 ${dup} 重複了`);
|
|
94
|
+
const missing = names.filter((n) => !isDefined(cfg, n));
|
|
95
|
+
if (missing.length)
|
|
96
|
+
throw new Error(`未定義的 agent:${missing.join("、")}(先用 agent add 新增)`);
|
|
97
|
+
return { cfg: { ...cfg, cycle: names }, changes: [] };
|
|
98
|
+
}
|
|
99
|
+
export function readRawConfig(path) {
|
|
100
|
+
if (!existsSync(path))
|
|
101
|
+
return {};
|
|
102
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
103
|
+
}
|
|
104
|
+
/** 先用 RepoConfig 驗證整份設定,通過才寫入 */
|
|
105
|
+
export function writeRawConfig(path, cfg) {
|
|
106
|
+
const parsed = RepoConfig.safeParse(cfg);
|
|
107
|
+
if (!parsed.success)
|
|
108
|
+
throw new Error(`設定不合法,未寫入:\n${z.prettifyError(parsed.error)}`);
|
|
109
|
+
writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=agentConfig.js.map
|
package/dist/agents/claude.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { num, str, tryJson } from "./types.js";
|
|
3
|
+
import { num, str, toolDetail, tryJson } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Claude Code 的權限設定:acceptEdits 只自動允許工作目錄內的修改,
|
|
6
6
|
* 版本控制交給 agentflowctl,並啟用內建沙箱限制 shell 指令。
|
|
@@ -46,7 +46,7 @@ export const claude = {
|
|
|
46
46
|
if (b.type === "text" && str(b.text)?.trim())
|
|
47
47
|
out.push({ kind: "text", text: str(b.text) });
|
|
48
48
|
if (b.type === "tool_use")
|
|
49
|
-
out.push({ kind: "tool", name: str(b.name) ?? "tool" });
|
|
49
|
+
out.push({ kind: "tool", name: str(b.name) ?? "tool", detail: toolDetail(b.input) });
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
else if (ev.type === "result") {
|
|
@@ -55,7 +55,6 @@ export const claude = {
|
|
|
55
55
|
kind: "usage",
|
|
56
56
|
inputTokens: num(usage.input_tokens),
|
|
57
57
|
outputTokens: num(usage.output_tokens),
|
|
58
|
-
costUsd: num(ev.total_cost_usd),
|
|
59
58
|
});
|
|
60
59
|
out.push({ kind: "done", ok: ev.is_error !== true, summary: str(ev.result) });
|
|
61
60
|
}
|
package/dist/agents/codex.js
CHANGED
|
@@ -21,9 +21,12 @@ export const codex = {
|
|
|
21
21
|
if (item.type === "agent_message" && str(item.text)?.trim())
|
|
22
22
|
out.push({ kind: "text", text: str(item.text) });
|
|
23
23
|
else if (item.type === "command_execution")
|
|
24
|
-
out.push({ kind: "tool", name:
|
|
25
|
-
else if (item.type === "file_change")
|
|
26
|
-
|
|
24
|
+
out.push({ kind: "tool", name: "shell", detail: str(item.command) });
|
|
25
|
+
else if (item.type === "file_change") {
|
|
26
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
27
|
+
const paths = changes.map((c) => str(c.path)).filter(Boolean);
|
|
28
|
+
out.push({ kind: "tool", name: "edit", detail: paths.length ? paths.join(", ") : undefined });
|
|
29
|
+
}
|
|
27
30
|
}
|
|
28
31
|
else if (ev.type === "turn.completed") {
|
|
29
32
|
const usage = (ev.usage ?? {});
|
package/dist/agents/gemini.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { num, str, tryJson } from "./types.js";
|
|
1
|
+
import { num, str, toolDetail, tryJson } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Google Gemini CLI:`gemini -p ... --output-format stream-json`。
|
|
4
4
|
* Gemini CLI 沒有「只允許工作目錄內的 shell 指令」這種細緻權限,
|
|
@@ -21,7 +21,7 @@ export const gemini = {
|
|
|
21
21
|
out.push({ kind: "text", text: str(ev.content) });
|
|
22
22
|
}
|
|
23
23
|
else if (ev.type === "tool_use") {
|
|
24
|
-
out.push({ kind: "tool", name: str(ev.tool_name) ?? "tool" });
|
|
24
|
+
out.push({ kind: "tool", name: str(ev.tool_name) ?? "tool", detail: toolDetail(ev.parameters) });
|
|
25
25
|
}
|
|
26
26
|
else if (ev.type === "result") {
|
|
27
27
|
const stats = (ev.stats ?? {});
|
package/dist/agents/index.js
CHANGED
|
@@ -5,4 +5,6 @@ import { gemini } from "./gemini.js";
|
|
|
5
5
|
export const ADAPTERS = { claude, codex, gemini, command };
|
|
6
6
|
/** 沒有設定時,依序偵測這些已安裝的 CLI 組成輪替順序 */
|
|
7
7
|
export const DEFAULT_CYCLE = ["claude", "codex", "gemini"];
|
|
8
|
+
/** 扣掉設定裡 removedAgents 之後,仍可使用的內建 agent */
|
|
9
|
+
export const builtinAgents = (removed = []) => DEFAULT_CYCLE.filter((n) => !removed.includes(n));
|
|
8
10
|
//# sourceMappingURL=index.js.map
|
package/dist/agents/types.js
CHANGED
|
@@ -9,4 +9,18 @@ export function tryJson(line) {
|
|
|
9
9
|
}
|
|
10
10
|
export const num = (v) => (typeof v === "number" ? v : undefined);
|
|
11
11
|
export const str = (v) => (typeof v === "string" ? v : undefined);
|
|
12
|
+
/** 從工具參數挑出最能代表「正在做什麼」的欄位;都沒有時以 JSON 顯示全部參數 */
|
|
13
|
+
export function toolDetail(input) {
|
|
14
|
+
if (!input || typeof input !== "object")
|
|
15
|
+
return undefined;
|
|
16
|
+
const o = input;
|
|
17
|
+
for (const key of ["command", "cmd", "file_path", "absolute_path", "path", "pattern", "url", "query"]) {
|
|
18
|
+
const v = o[key];
|
|
19
|
+
if (typeof v === "string" && v.trim())
|
|
20
|
+
return v;
|
|
21
|
+
if (Array.isArray(v) && v.length)
|
|
22
|
+
return v.join(" ");
|
|
23
|
+
}
|
|
24
|
+
return Object.keys(o).length ? JSON.stringify(o) : undefined;
|
|
25
|
+
}
|
|
12
26
|
//# sourceMappingURL=types.js.map
|
package/dist/cli.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { builtinAgents } from "./agents/index.js";
|
|
6
6
|
import { advance, loadRepoConfig } from "./engine.js";
|
|
7
7
|
import { API_KEY_VARS, probeAgent, resolveAgent, runCommand } from "./runner.js";
|
|
8
8
|
import { addWorktree, git, removeWorktree } from "./git.js";
|
|
9
9
|
import { flowDir, logDir, projectRoot, runDir, worktreeDir } from "./paths.js";
|
|
10
10
|
import { TaskList } from "./schemas.js";
|
|
11
|
-
import { agentRuns,
|
|
11
|
+
import { agentRuns, getRun, listRuns, listSubstitutions, saveRun, usageByAgent } from "./store.js";
|
|
12
12
|
import { readJsonFile } from "./util.js";
|
|
13
|
+
import { addAgent, readRawConfig, removeAgent, setAgent, setCycle, writeRawConfig } from "./agentConfig.js";
|
|
13
14
|
function mustGetRun(id) {
|
|
14
15
|
const run = getRun(id);
|
|
15
16
|
if (!run)
|
|
@@ -28,7 +29,7 @@ function printSummary(run) {
|
|
|
28
29
|
console.log("");
|
|
29
30
|
console.log(`run ${run.id}`);
|
|
30
31
|
console.log(`階段 ${run.stage}`);
|
|
31
|
-
console.log(`用量 agent 執行 ${agentRuns(run.id)} / ${run.maxAgentRuns}
|
|
32
|
+
console.log(`用量 agent 執行 ${agentRuns(run.id)} / ${run.maxAgentRuns} 次`);
|
|
32
33
|
console.log(`agent ${run.cycle.join(" → ")}${run.lastWriter ? `(最後作者:${run.lastWriter})` : ""}`);
|
|
33
34
|
console.log(`分支 ${run.branch}`);
|
|
34
35
|
console.log(`worktree ${worktreeDir(run.id)}`);
|
|
@@ -61,11 +62,11 @@ async function resolveCycle(flag) {
|
|
|
61
62
|
return wanted;
|
|
62
63
|
}
|
|
63
64
|
const found = [];
|
|
64
|
-
for (const name of
|
|
65
|
+
for (const name of builtinAgents(cfg.removedAgents))
|
|
65
66
|
if (await probeAgent(resolveAgent(cfg, name)))
|
|
66
67
|
found.push(name);
|
|
67
68
|
if (!found.length)
|
|
68
|
-
throw new Error(
|
|
69
|
+
throw new Error(`沒有偵測到任何 agent CLI(${builtinAgents(cfg.removedAgents).join("、") || "內建 agent 都已移除"}),可用 agentflowctl doctor 檢查`);
|
|
69
70
|
return found;
|
|
70
71
|
}
|
|
71
72
|
const program = new Command()
|
|
@@ -78,7 +79,6 @@ program
|
|
|
78
79
|
.option("--req-file <file>", "從檔案讀取需求")
|
|
79
80
|
.option("--base <branch>", "基底分支(預設為目前的分支)")
|
|
80
81
|
.option("--max-agent-runs <n>", "單一 run 最多執行幾次 agent(預設取 flow.config.json 的 maxAgentRuns)")
|
|
81
|
-
.option("--budget <usd>", "選用:估計花費上限(美元),使用 API 計費時才需要")
|
|
82
82
|
.option("--manual-plan", "計畫通過 AI 審查後,仍停下來等你確認", false)
|
|
83
83
|
.option("--cycle <agents>", "agent 輪替順序,例如 claude,codex,gemini")
|
|
84
84
|
.action(async (opts) => {
|
|
@@ -108,7 +108,6 @@ program
|
|
|
108
108
|
requirement: requirement.trim(),
|
|
109
109
|
stage: "spec",
|
|
110
110
|
autopilot: !opts.manualPlan,
|
|
111
|
-
budgetUsd: opts.budget ? Number(opts.budget) : undefined,
|
|
112
111
|
maxAgentRuns: opts.maxAgentRuns ? Number(opts.maxAgentRuns) : cfg.maxAgentRuns,
|
|
113
112
|
cycle,
|
|
114
113
|
attempts: {},
|
|
@@ -132,11 +131,8 @@ program
|
|
|
132
131
|
.command("resume <id>")
|
|
133
132
|
.description("從暫停、中斷或失敗的階段接續")
|
|
134
133
|
.option("--max-agent-runs <n>", "調整 agent 執行次數上限")
|
|
135
|
-
.option("--budget <usd>", "調整估計花費上限(美元)")
|
|
136
134
|
.action(async (id, opts) => {
|
|
137
135
|
let run = mustGetRun(id);
|
|
138
|
-
if (opts.budget)
|
|
139
|
-
run = { ...run, budgetUsd: Number(opts.budget) };
|
|
140
136
|
if (opts.maxAgentRuns)
|
|
141
137
|
run = { ...run, maxAgentRuns: Number(opts.maxAgentRuns) };
|
|
142
138
|
if (run.stage === "paused") {
|
|
@@ -171,11 +167,11 @@ program
|
|
|
171
167
|
.action((id) => {
|
|
172
168
|
const run = mustGetRun(id);
|
|
173
169
|
printSummary(run);
|
|
174
|
-
const byAgent =
|
|
170
|
+
const byAgent = usageByAgent(id);
|
|
175
171
|
if (Object.keys(byAgent).length) {
|
|
176
172
|
console.log("\n各 agent 用量");
|
|
177
173
|
for (const [agent, c] of Object.entries(byAgent)) {
|
|
178
|
-
console.log(` ${agent.padEnd(10)} ${String(c.runs).padStart(3)} 次 ${String(c.tokens).padStart(9)} tokens
|
|
174
|
+
console.log(` ${agent.padEnd(10)} ${String(c.runs).padStart(3)} 次 ${String(c.tokens).padStart(9)} tokens`);
|
|
179
175
|
}
|
|
180
176
|
}
|
|
181
177
|
const subs = listSubstitutions(id);
|
|
@@ -194,12 +190,91 @@ program
|
|
|
194
190
|
console.log(` ${mark} ${t.id} ${t.title}`);
|
|
195
191
|
});
|
|
196
192
|
});
|
|
193
|
+
// ───────────── agent 管理:讀寫 flow.config.json 的 agents 與 cycle ─────────────
|
|
194
|
+
const configPath = () => join(projectRoot(), "flow.config.json");
|
|
195
|
+
const collect = (value, prev = []) => [...prev, value];
|
|
196
|
+
function applyEdit(edit, done) {
|
|
197
|
+
const before = readRawConfig(configPath());
|
|
198
|
+
const { cfg, changes } = edit(before);
|
|
199
|
+
writeRawConfig(configPath(), cfg);
|
|
200
|
+
console.log(`✅ ${done}(${configPath()})`);
|
|
201
|
+
for (const c of changes)
|
|
202
|
+
console.log(` ↳ ${c}`);
|
|
203
|
+
if (JSON.stringify(before.cycle) !== JSON.stringify(cfg.cycle)) {
|
|
204
|
+
console.log(" 已建立的 run 會沿用建立時的輪替順序,不受影響");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const agent = program.command("agent").description("管理 agent 與 adapter 設定(寫入 flow.config.json)");
|
|
208
|
+
agent
|
|
209
|
+
.command("list")
|
|
210
|
+
.description("列出內建與自訂 agent、是否已安裝、在輪替中的位置")
|
|
211
|
+
.action(async () => {
|
|
212
|
+
const cfg = loadRepoConfig();
|
|
213
|
+
const cycle = await resolveCycle().catch(() => cfg.cycle ?? []);
|
|
214
|
+
const names = [...new Set([...builtinAgents(cfg.removedAgents), ...Object.keys(cfg.agents)])];
|
|
215
|
+
for (const name of names) {
|
|
216
|
+
const def = resolveAgent(cfg, name);
|
|
217
|
+
const ok = await probeAgent(def);
|
|
218
|
+
const pos = cycle.indexOf(name);
|
|
219
|
+
const kind = builtinAgents(cfg.removedAgents).includes(name) ? (name in cfg.agents ? "內建(已覆寫)" : "內建") : "自訂";
|
|
220
|
+
const detail = [
|
|
221
|
+
`adapter=${def.adapter}`,
|
|
222
|
+
def.model && `model=${def.model}`,
|
|
223
|
+
def.extraArgs.length && `extraArgs=${def.extraArgs.join(" ")}`,
|
|
224
|
+
def.command && `command=${def.command.join(" ")}`,
|
|
225
|
+
].filter(Boolean);
|
|
226
|
+
console.log(`${ok ? "✅" : "❌"} ${name.padEnd(14)} ${kind.padEnd(8)} ${pos >= 0 ? `輪替 #${pos + 1}` : "不在輪替"} ${detail.join(" ")}`);
|
|
227
|
+
}
|
|
228
|
+
if (cfg.removedAgents.length)
|
|
229
|
+
console.log(`\n已移除的內建 agent:${cfg.removedAgents.join("、")}(可用 agent add <name> --adapter <name> 加回)`);
|
|
230
|
+
console.log(`
|
|
231
|
+
輪替順序:${cycle.length ? cycle.join(" → ") : "(沒有可用的 agent)"}${cfg.cycle ? "" : "(自動偵測)"}`);
|
|
232
|
+
});
|
|
233
|
+
agent
|
|
234
|
+
.command("add <name> [command...]")
|
|
235
|
+
.description("新增 agent;command adapter 的指令寫在 -- 後面")
|
|
236
|
+
.requiredOption("--adapter <adapter>", "claude、codex、gemini 或 command")
|
|
237
|
+
.option("--model <model>", "模型名稱")
|
|
238
|
+
.option("--extra-arg <arg>", "額外參數,可重複;以 - 開頭時寫成 --extra-arg=--sandbox", collect)
|
|
239
|
+
.action((name, command, opts) => {
|
|
240
|
+
applyEdit((cfg) => addAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已新增 ${name};要加進輪替請用 agent cycle`);
|
|
241
|
+
});
|
|
242
|
+
agent
|
|
243
|
+
.command("set <name> [command...]")
|
|
244
|
+
.description("修改 agent;更換 adapter 時會清掉舊 adapter 的 model、extraArgs、command")
|
|
245
|
+
.option("--adapter <adapter>", "claude、codex、gemini 或 command")
|
|
246
|
+
.option("--model <model>", "模型名稱")
|
|
247
|
+
.option("--extra-arg <arg>", "額外參數,可重複,會整個取代原本的設定", collect)
|
|
248
|
+
.action((name, command, opts) => {
|
|
249
|
+
applyEdit((cfg) => setAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已更新 ${name}`);
|
|
250
|
+
});
|
|
251
|
+
agent
|
|
252
|
+
.command("remove <name>")
|
|
253
|
+
.description("刪除 agent(含內建的 claude、codex、gemini),一併從輪替移除;內建的可用 agent add 加回")
|
|
254
|
+
.action((name) => applyEdit((cfg) => removeAgent(cfg, name), `已移除 ${name}`));
|
|
255
|
+
agent
|
|
256
|
+
.command("cycle [names]")
|
|
257
|
+
.description("顯示輪替順序,或用逗號分隔設定新的順序")
|
|
258
|
+
.action(async (names) => {
|
|
259
|
+
if (!names) {
|
|
260
|
+
const cfg = loadRepoConfig();
|
|
261
|
+
console.log(`${(await resolveCycle()).join(" → ")}${cfg.cycle ? "" : "(自動偵測)"}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const list = names.split(",").map((s) => s.trim()).filter(Boolean);
|
|
265
|
+
applyEdit((cfg) => setCycle(cfg, list), `輪替順序設為 ${list.join(" → ")}`);
|
|
266
|
+
const cfg = loadRepoConfig();
|
|
267
|
+
for (const n of list) {
|
|
268
|
+
if (!(await probeAgent(resolveAgent(cfg, n))))
|
|
269
|
+
console.log(`⚠️ ${n} 目前找不到可執行的 CLI,run 會失敗,請先安裝或用 agent set 修正`);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
197
272
|
program
|
|
198
273
|
.command("doctor")
|
|
199
274
|
.description("檢查可用的 agent CLI 與目前的輪替設定")
|
|
200
275
|
.action(async () => {
|
|
201
276
|
const cfg = loadRepoConfig();
|
|
202
|
-
const names = [...new Set([...
|
|
277
|
+
const names = [...new Set([...builtinAgents(cfg.removedAgents), ...Object.keys(cfg.agents)])];
|
|
203
278
|
for (const name of names) {
|
|
204
279
|
const def = resolveAgent(cfg, name);
|
|
205
280
|
const ok = await probeAgent(def);
|
|
@@ -212,11 +287,9 @@ program
|
|
|
212
287
|
console.log(`\n${e.message}`);
|
|
213
288
|
}
|
|
214
289
|
const leaked = API_KEY_VARS.filter((k) => process.env[k]);
|
|
215
|
-
console.log(
|
|
290
|
+
console.log("\n登入方式:訂閱登入(執行 agent 時會移除 API key)");
|
|
216
291
|
if (leaked.length) {
|
|
217
|
-
console.log(
|
|
218
|
-
? `⚠️ 環境中有 ${leaked.join("、")},agentflowctl 執行 agent 時會移除,但你自己直接執行 CLI 時仍可能改走 API 計費`
|
|
219
|
-
: `使用中的 API key:${leaked.join("、")}`);
|
|
292
|
+
console.log(`⚠️ 環境中有 ${leaked.join("、")},agentflowctl 執行 agent 時會移除,但你自己直接執行 CLI 時仍可能改走 API 計費`);
|
|
220
293
|
}
|
|
221
294
|
console.log(`單一 run 的 agent 執行上限:${cfg.maxAgentRuns} 次`);
|
|
222
295
|
console.log(`修正策略:${cfg.fixStrategy} 測試與實作分開:${cfg.tddSplit ? "是" : "否"}`);
|
|
@@ -228,7 +301,7 @@ program
|
|
|
228
301
|
.action(() => {
|
|
229
302
|
for (const r of listRuns()) {
|
|
230
303
|
const req = r.requirement.split("\n")[0].slice(0, 40);
|
|
231
|
-
console.log(`${r.id} ${r.stage.padEnd(17)}
|
|
304
|
+
console.log(`${r.id} ${r.stage.padEnd(17)} ${String(agentRuns(r.id)).padStart(3)} 次 ${r.updatedAt.slice(0, 16)} ${req}`);
|
|
232
305
|
}
|
|
233
306
|
});
|
|
234
307
|
program
|
package/dist/engine.js
CHANGED
|
@@ -7,7 +7,7 @@ import { exec } from "./proc.js";
|
|
|
7
7
|
import { arbiterPanel, availableAgent, fixAgent, planAgent, planFixAgent, reviewers, specAgent, taskAgents } from "./roles.js";
|
|
8
8
|
import { resolveAgent, runAgent, runCommand } from "./runner.js";
|
|
9
9
|
import { AcceptanceList, RepoConfig, ReviewResult, TaskList, } from "./schemas.js";
|
|
10
|
-
import {
|
|
10
|
+
import { addSubstitution, addUsage, agentRuns, saveRun } from "./store.js";
|
|
11
11
|
import { orderTasks } from "./tasks.js";
|
|
12
12
|
import { readJsonFile, renderPrompt, tail } from "./util.js";
|
|
13
13
|
// ───────────────────────── 共用工具 ─────────────────────────
|
|
@@ -43,18 +43,27 @@ async function agentStep(run, planned, step, prompt, mode) {
|
|
|
43
43
|
info(run, `🔁 ${agent} 額度已用完,${step} 由 ${sub} 代打${note ? `(注意:${note})` : ""}`);
|
|
44
44
|
agent = sub;
|
|
45
45
|
}
|
|
46
|
-
const r = await runAgent(agent, resolveAgent(cfg, agent), target(run, `${step}-${agent}`), prompt
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!r.quotaExhausted)
|
|
46
|
+
const r = await runAgent(agent, resolveAgent(cfg, agent), target(run, `${step}-${agent}`), prompt);
|
|
47
|
+
addUsage(run.id, { stage: step, agent, inputTokens: r.inputTokens, outputTokens: r.outputTokens });
|
|
48
|
+
if (!r.quotaExhausted) {
|
|
49
|
+
reportMeta(run, agent, r);
|
|
51
50
|
return { r, agent };
|
|
51
|
+
}
|
|
52
52
|
info(run, `⛽ ${agent} 的額度已用完`);
|
|
53
53
|
exhausted.add(agent);
|
|
54
54
|
await reset();
|
|
55
55
|
// 迴圈回到開頭:review 會停下,write 會找代打
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/** 印出回覆裡的 XML 中繼資料;只供人檢視,關卡仍由程式檢查決定 */
|
|
59
|
+
function reportMeta(run, agent, r) {
|
|
60
|
+
if (!r.meta)
|
|
61
|
+
return;
|
|
62
|
+
if (r.meta.status === "blocked")
|
|
63
|
+
info(run, ` 🚧 ${agent} 回報卡住:${r.meta.summary}`);
|
|
64
|
+
if (r.meta.concerns)
|
|
65
|
+
info(run, ` 💭 ${agent} 的疑慮:${r.meta.concerns}`);
|
|
66
|
+
}
|
|
58
67
|
function readFeedback(run) {
|
|
59
68
|
const p = flowFile(run, "feedback.md");
|
|
60
69
|
return existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
@@ -481,7 +490,7 @@ async function prStage(run) {
|
|
|
481
490
|
const title = run.requirement.split("\n")[0].slice(0, 72);
|
|
482
491
|
const spec = existsSync(flowFile(run, "spec.md")) ? readFileSync(flowFile(run, "spec.md"), "utf8") : "";
|
|
483
492
|
const bodyPath = join(runDir(run.id), "pr-body.md");
|
|
484
|
-
writeFileSync(bodyPath, `> 由 agentflowctl 自動產生(run: ${run.id},參與的 agent:${run.cycle.join("、")}
|
|
493
|
+
writeFileSync(bodyPath, `> 由 agentflowctl 自動產生(run: ${run.id},參與的 agent:${run.cycle.join("、")},執行 agent ${agentRuns(run.id)} 次)\n\n${spec}`);
|
|
485
494
|
const r = await exec("gh", ["pr", "create", "--head", run.branch, "--base", run.baseBranch, "--title", title, "--body-file", bodyPath], { cwd: repo });
|
|
486
495
|
if (r.code !== 0) {
|
|
487
496
|
info(run, `⚠️ gh pr create 失敗,分支已推送,請手動建立 PR:${r.stderr.trim()}`);
|
|
@@ -516,15 +525,6 @@ export async function advance(initial) {
|
|
|
516
525
|
failureReason: `已執行 agent ${runs} 次,達到上限 ${run.maxAgentRuns}(可用 resume --max-agent-runs 調高)`,
|
|
517
526
|
});
|
|
518
527
|
}
|
|
519
|
-
const cost = getCost(run.id);
|
|
520
|
-
if (run.budgetUsd !== undefined && cost >= run.budgetUsd) {
|
|
521
|
-
return saveRun({
|
|
522
|
-
...run,
|
|
523
|
-
stage: "failed",
|
|
524
|
-
failedStage: stage,
|
|
525
|
-
failureReason: `估計花費 $${cost.toFixed(2)},超過預算 $${run.budgetUsd}`,
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
528
|
try {
|
|
529
529
|
run = saveRun(await STAGES[stage](run));
|
|
530
530
|
}
|