agentflowctl 0.1.2 → 0.2.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 CHANGED
@@ -93,7 +93,6 @@ 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` 會列出任務。進行中的任務會標出正在寫測試還是正在寫實作。
@@ -109,7 +108,7 @@ agentflowctl clean f-xxxx # 移除 worktree 與 run 紀錄,分支保
109
108
  "tddSplit": true,
110
109
  "tieBreak": "proceed",
111
110
  "agents": {
112
- "codex": { "adapter": "codex", "model": "你要用的模型", "pricing": { "inputPerMTok": 1.25, "outputPerMTok": 10 } },
111
+ "codex": { "adapter": "codex", "model": "你要用的模型" },
113
112
  "aider": { "adapter": "command", "command": ["aider", "--yes-always", "--no-auto-commits", "--message", "{prompt}"] }
114
113
  }
115
114
  }
@@ -124,13 +123,34 @@ agentflowctl clean f-xxxx # 移除 worktree 與 run 紀錄,分支保
124
123
  | `planReviewQuorum` | `1` | 計畫需要幾位不同審查者都 `approve` |
125
124
  | `planArbiter` | `true` | 計畫審查僵持時交付仲裁。關掉之後,僵持會直接讓 run 失敗 |
126
125
  | `tieBreak` | `proceed` | 兩家仲裁意見分歧時:`proceed` 繼續並記錄爭議;`stop` 停下 |
127
- | `auth` | `subscription` | `subscription` 移除子程序裡的 API key;`api` 保留,給 CI 用 |
128
126
  | `maxAgentRuns` | `60` | 單一 run 最多執行幾次 agent |
129
127
  | `install` / `test` / `checks` | 見 `src/schemas.ts` | verify 階段實際執行的指令 |
130
128
  | `agents` | 內建 claude、codex、gemini | 覆寫內建 agent,或用 `command` adapter 接上其他 CLI |
131
129
 
132
130
  verify 失敗(型別、lint、建置)一律交回最後作者。審查意見才依 `fixStrategy` 決定修正者。
133
131
 
132
+ ### 用指令管理 agent
133
+
134
+ `agents` 與 `cycle` 也可以用 `agent` 指令修改,不必手動編輯 JSON。每次寫入前都會先驗證整份設定:
135
+
136
+ ```bash
137
+ agentflowctl agent list # 內建與自訂 agent、是否已安裝、輪替位置
138
+ agentflowctl agent add claude-strong --adapter claude --model opus
139
+ agentflowctl agent add aider --adapter command -- aider --yes-always --message {prompt}
140
+ agentflowctl agent set codex --model 你要用的模型 --extra-arg=--search
141
+ agentflowctl agent set aider --adapter gemini # 換 adapter
142
+ agentflowctl agent remove aider
143
+ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯示目前的順序
144
+ ```
145
+
146
+ 修改會連帶更新相關設定,並在終端機列出:
147
+
148
+ - `set --adapter` 換 adapter 時,會清掉舊 adapter 的 `model`、`extraArgs`、`command`,這次有重新指定的除外。
149
+ - `remove` 刪除自訂 agent 時,會一併從 `cycle` 移除。`cycle` 變空就刪除這個欄位,改回自動偵測。對內建 agent 用 `remove`,只會刪掉覆寫設定。
150
+ - `--extra-arg` 可以重複指定,會整個取代原本的 `extraArgs`。參數以 `-` 開頭時,寫成 `--extra-arg=--sandbox`。
151
+
152
+ 已建立的 run 會沿用建立時的輪替順序,不受這些修改影響。
153
+
134
154
  ## 計畫怎麼在沒有人的情況下通過
135
155
 
136
156
  人工確認計畫是為了擋住方向錯了還一路做下去。預設用三層機制取代它;加上 `--manual-plan` 時,三層都過了仍會停下來等你。
@@ -169,6 +189,23 @@ verify 失敗(型別、lint、建置)一律交回最後作者。審查意見
169
189
 
170
190
  驗收條件寫在 `.flow/acceptance.json`(`AC-1`…),任務寫在 `.flow/tasks.json`(`T-1`…)。
171
191
 
192
+ ## Prompt 結構
193
+
194
+ 每個階段的 prompt 都有專屬角色:需求分析師、軟體架構師、計畫審查者、計畫修訂者、中立仲裁者、測試工程師、實作工程師、除錯工程師、程式碼審查者。內容用 XML 標籤分段:`<role>`、`<context>`、`<inputs>`、`<steps>`、`<constraints>`、`<output_format>`、`<reply_format>`。
195
+
196
+ Agent 的最後回覆要附上 XML 中繼資料:
197
+
198
+ ```xml
199
+ <result>
200
+ <status>done 或 blocked</status>
201
+ <summary>做了什麼</summary>
202
+ <files_changed><file>src/form.ts</file></files_changed>
203
+ <concerns>對規格或測試的疑慮</concerns>
204
+ </result>
205
+ ```
206
+
207
+ `blocked` 與 `concerns` 會印在終端機上,完整回覆留在 log。這份中繼資料只給人看;缺少或格式錯誤都不影響流程,是否通過仍由上表的程式檢查決定。
208
+
172
209
  ## Adapter
173
210
 
174
211
  | adapter | 執行方式 | 權限 |
@@ -184,11 +221,11 @@ Codex 沙箱預設不能連網,所以建立 worktree 時會先跑 `install`。
184
221
 
185
222
  ## 登入、額度與代打
186
223
 
187
- 預設 `auth: "subscription"`,只用各家 CLI 的訂閱登入。
224
+ 只支援各家 CLI 的訂閱登入。
188
225
 
189
- 執行 agent 時,會從子程序環境移除 `ANTHROPIC_API_KEY`、`ANTHROPIC_AUTH_TOKEN`、`CODEX_API_KEY`、`OPENAI_API_KEY`、`GEMINI_API_KEY`、`GOOGLE_API_KEY`,避免環境裡的 key 蓋過訂閱登入。`doctor` 發現這些變數時會提醒。專案指令(install、test、build)不受影響。
226
+ 執行 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
227
 
191
- 上限是執行次數(`maxAgentRuns`,預設 60),不是金額。Claude Code 回報的美元金額是 API 價格的估計值。
228
+ 上限是執行次數(`maxAgentRuns`,預設 60),不是金額。`status` 會列出各 agent 的執行次數與 token 數。
192
229
 
193
230
  額度用完時:
194
231
 
@@ -204,8 +241,6 @@ Codex 沙箱預設不能連網,所以建立 worktree 時會先跑 `install`。
204
241
 
205
242
  額度錯誤靠錯誤訊息辨識(usage limit、rate limit、quota、429 等),只在 agent 執行失敗時判斷。辨識不到時,會當成一般失敗重試。
206
243
 
207
- CI 無法使用訂閱登入時,設 `"auth": "api"` 保留 API key,並用 `--budget` 設估計花費上限。Codex、Gemini 只回報 token,要在 `agents.<name>.pricing` 設定價格才會算進預算。GitHub Actions 範例見 `examples/github-actions.yml`。
208
-
209
244
  ## 在哪裡跑
210
245
 
211
246
  agentflowctl 本身只依賴 Node.js 與 git。專案指令透過系統 shell 執行。
@@ -213,7 +248,7 @@ agentflowctl 本身只依賴 Node.js 與 git。專案指令透過系統 shell
213
248
  | 環境 | 適合的用法 |
214
249
  |---|---|
215
250
  | 自己的電腦 | 自己的專案、自己寫的需求。剛開始可以加 `--manual-plan`,確認審查品質後再拿掉 |
216
- | CI、容器、遠端開發機 | 無人值守。見 `examples/github-actions.yml`:issue 加上標籤就跑完並開 PR |
251
+ | 容器、遠端開發機 | 無人值守。先在該環境內完成各家 CLI 的訂閱登入 |
217
252
  | Claude Code、Codex 裡面 | 讓它們用 shell 執行 `npx agentflowctl` |
218
253
 
219
254
  沒有容器隔離時,verify 會在你的電腦上執行 agent 寫出來的程式碼。Gemini 在無人值守時是 yolo 模式。處理外部 issue,或需求文字不是你自己寫的,放到可丟棄的環境。AI 審查計畫擋不住夾在需求裡的指示。
@@ -228,7 +263,7 @@ src/
228
263
  runner.ts 執行 agent、正規化結果、執行專案指令
229
264
  agents/ claude、codex、gemini、command
230
265
  git.ts worktree 與 git 操作
231
- store.ts 狀態、花費、代打紀錄
266
+ store.ts 狀態、用量、代打紀錄
232
267
  tasks.ts 任務 DAG
233
268
  schemas.ts zod schema
234
269
  prompts/ 各階段 prompt
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { 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 isDefined = (cfg, name) => isBuiltin(name) || name in agentsOf(cfg);
10
+ /** 只留下有值的欄位,驗證後回傳 */
11
+ function buildAgent(base, patch) {
12
+ const next = { ...base };
13
+ for (const [k, v] of Object.entries(patch))
14
+ if (v !== undefined)
15
+ next[k] = v;
16
+ const parsed = AgentDef.safeParse(next);
17
+ if (!parsed.success)
18
+ throw new Error(`agent 設定不合法:\n${z.prettifyError(parsed.error)}`);
19
+ if (parsed.data.adapter === "command" && !parsed.data.command?.length) {
20
+ throw new Error("command adapter 需要指令,請寫在 -- 後面,例如:-- aider --message {prompt}");
21
+ }
22
+ if (parsed.data.adapter !== "command" && parsed.data.command)
23
+ throw new Error("只有 command adapter 可以設定 command");
24
+ return next;
25
+ }
26
+ export function addAgent(cfg, name, def) {
27
+ if (!/^[\w-]+$/.test(name))
28
+ throw new Error(`agent 名稱只能用英數字、底線與連字號:${name}`);
29
+ if (isDefined(cfg, name))
30
+ throw new Error(`agent ${name} 已存在,要修改請用 agent set`);
31
+ const { adapter, ...patch } = def;
32
+ return { cfg: { ...cfg, agents: { ...agentsOf(cfg), [name]: buildAgent({ adapter }, patch) } }, changes: [] };
33
+ }
34
+ export function setAgent(cfg, name, patch) {
35
+ if (!isDefined(cfg, name))
36
+ throw new Error(`未定義的 agent:${name}`);
37
+ if (Object.values(patch).every((v) => v === undefined)) {
38
+ throw new Error("沒有要修改的欄位(--adapter、--model、--extra-arg 或 -- <command>)");
39
+ }
40
+ const agents = agentsOf(cfg);
41
+ // 內建 agent 第一次修改時,新增一筆覆寫設定
42
+ const base = { ...(agents[name] ?? { adapter: name }) };
43
+ const changes = [];
44
+ if (patch.adapter !== undefined && patch.adapter !== base.adapter) {
45
+ const cleared = ADAPTER_FIELDS.filter((f) => base[f] !== undefined && patch[f] === undefined);
46
+ for (const f of cleared)
47
+ delete base[f];
48
+ if (cleared.length)
49
+ changes.push(`adapter 從 ${String(base.adapter)} 換成 ${patch.adapter},已清掉舊的 ${cleared.join("、")}`);
50
+ }
51
+ return { cfg: { ...cfg, agents: { ...agents, [name]: buildAgent(base, patch) } }, changes };
52
+ }
53
+ export function removeAgent(cfg, name) {
54
+ const agents = agentsOf(cfg);
55
+ if (!(name in agents)) {
56
+ throw new Error(isBuiltin(name) ? `${name} 是內建 agent,沒有覆寫設定可以刪除` : `未定義的 agent:${name}`);
57
+ }
58
+ delete agents[name];
59
+ const next = { ...cfg, agents };
60
+ const changes = [];
61
+ const cycle = cfg.cycle;
62
+ // 自訂 agent 刪掉後就不存在了,一併從輪替順序移除;內建 agent 只是恢復預設,仍可留在輪替裡
63
+ if (cycle?.includes(name) && !isBuiltin(name)) {
64
+ const rest = cycle.filter((n) => n !== name);
65
+ if (rest.length) {
66
+ next.cycle = rest;
67
+ changes.push(`已從輪替順序移除,現在是 ${rest.join(" → ")}`);
68
+ }
69
+ else {
70
+ delete next.cycle;
71
+ changes.push("輪替順序因此變空,已刪除 cycle,改回自動偵測已安裝的 CLI");
72
+ }
73
+ }
74
+ return { cfg: next, changes };
75
+ }
76
+ export function setCycle(cfg, names) {
77
+ if (!names.length)
78
+ throw new Error("輪替順序至少要有一個 agent");
79
+ const dup = names.find((n, i) => names.indexOf(n) !== i);
80
+ if (dup)
81
+ throw new Error(`輪替順序裡 ${dup} 重複了`);
82
+ const missing = names.filter((n) => !isDefined(cfg, n));
83
+ if (missing.length)
84
+ throw new Error(`未定義的 agent:${missing.join("、")}(先用 agent add 新增)`);
85
+ return { cfg: { ...cfg, cycle: names }, changes: [] };
86
+ }
87
+ export function readRawConfig(path) {
88
+ if (!existsSync(path))
89
+ return {};
90
+ return JSON.parse(readFileSync(path, "utf8"));
91
+ }
92
+ /** 先用 RepoConfig 驗證整份設定,通過才寫入 */
93
+ export function writeRawConfig(path, cfg) {
94
+ const parsed = RepoConfig.safeParse(cfg);
95
+ if (!parsed.success)
96
+ throw new Error(`設定不合法,未寫入:\n${z.prettifyError(parsed.error)}`);
97
+ writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`);
98
+ }
99
+ //# sourceMappingURL=agentConfig.js.map
@@ -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/cli.js CHANGED
@@ -8,8 +8,9 @@ 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, costByAgent, getCost, getRun, listRuns, listSubstitutions, saveRun } from "./store.js";
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} 次(估計花費 $${getCost(run.id).toFixed(2)}${run.budgetUsd ? ` / $${run.budgetUsd}` : ",訂閱登入時僅供參考"})`);
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)}`);
@@ -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 = costByAgent(id);
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 $${c.usd.toFixed(2)}`);
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,6 +190,83 @@ 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([...DEFAULT_CYCLE, ...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 = DEFAULT_CYCLE.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
+ console.log(`
229
+ 輪替順序:${cycle.length ? cycle.join(" → ") : "(沒有可用的 agent)"}${cfg.cycle ? "" : "(自動偵測)"}`);
230
+ });
231
+ agent
232
+ .command("add <name> [command...]")
233
+ .description("新增 agent;command adapter 的指令寫在 -- 後面")
234
+ .requiredOption("--adapter <adapter>", "claude、codex、gemini 或 command")
235
+ .option("--model <model>", "模型名稱")
236
+ .option("--extra-arg <arg>", "額外參數,可重複;以 - 開頭時寫成 --extra-arg=--sandbox", collect)
237
+ .action((name, command, opts) => {
238
+ applyEdit((cfg) => addAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已新增 ${name};要加進輪替請用 agent cycle`);
239
+ });
240
+ agent
241
+ .command("set <name> [command...]")
242
+ .description("修改 agent;更換 adapter 時會清掉舊 adapter 的 model、extraArgs、command")
243
+ .option("--adapter <adapter>", "claude、codex、gemini 或 command")
244
+ .option("--model <model>", "模型名稱")
245
+ .option("--extra-arg <arg>", "額外參數,可重複,會整個取代原本的設定", collect)
246
+ .action((name, command, opts) => {
247
+ applyEdit((cfg) => setAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已更新 ${name}`);
248
+ });
249
+ agent
250
+ .command("remove <name>")
251
+ .description("刪除自訂 agent(一併從輪替移除),或刪除內建 agent 的覆寫設定")
252
+ .action((name) => applyEdit((cfg) => removeAgent(cfg, name), `已刪除 ${name} 的設定`));
253
+ agent
254
+ .command("cycle [names]")
255
+ .description("顯示輪替順序,或用逗號分隔設定新的順序")
256
+ .action(async (names) => {
257
+ if (!names) {
258
+ const cfg = loadRepoConfig();
259
+ console.log(`${(await resolveCycle()).join(" → ")}${cfg.cycle ? "" : "(自動偵測)"}`);
260
+ return;
261
+ }
262
+ const list = names.split(",").map((s) => s.trim()).filter(Boolean);
263
+ applyEdit((cfg) => setCycle(cfg, list), `輪替順序設為 ${list.join(" → ")}`);
264
+ const cfg = loadRepoConfig();
265
+ for (const n of list) {
266
+ if (!(await probeAgent(resolveAgent(cfg, n))))
267
+ console.log(`⚠️ ${n} 目前找不到可執行的 CLI,run 會失敗,請先安裝或用 agent set 修正`);
268
+ }
269
+ });
197
270
  program
198
271
  .command("doctor")
199
272
  .description("檢查可用的 agent CLI 與目前的輪替設定")
@@ -212,11 +285,9 @@ program
212
285
  console.log(`\n${e.message}`);
213
286
  }
214
287
  const leaked = API_KEY_VARS.filter((k) => process.env[k]);
215
- console.log(`\n登入方式:${cfg.auth === "subscription" ? "訂閱登入(執行 agent 時會移除 API key)" : "API key"}`);
288
+ console.log("\n登入方式:訂閱登入(執行 agent 時會移除 API key)");
216
289
  if (leaked.length) {
217
- console.log(cfg.auth === "subscription"
218
- ? `⚠️ 環境中有 ${leaked.join("、")},agentflowctl 執行 agent 時會移除,但你自己直接執行 CLI 時仍可能改走 API 計費`
219
- : `使用中的 API key:${leaked.join("、")}`);
290
+ console.log(`⚠️ 環境中有 ${leaked.join("、")},agentflowctl 執行 agent 時會移除,但你自己直接執行 CLI 時仍可能改走 API 計費`);
220
291
  }
221
292
  console.log(`單一 run 的 agent 執行上限:${cfg.maxAgentRuns} 次`);
222
293
  console.log(`修正策略:${cfg.fixStrategy} 測試與實作分開:${cfg.tddSplit ? "是" : "否"}`);
@@ -228,7 +299,7 @@ program
228
299
  .action(() => {
229
300
  for (const r of listRuns()) {
230
301
  const req = r.requirement.split("\n")[0].slice(0, 40);
231
- console.log(`${r.id} ${r.stage.padEnd(17)} $${getCost(r.id).toFixed(2).padStart(6)} ${r.updatedAt.slice(0, 16)} ${req}`);
302
+ console.log(`${r.id} ${r.stage.padEnd(17)} ${String(agentRuns(r.id)).padStart(3)} 次 ${r.updatedAt.slice(0, 16)} ${req}`);
232
303
  }
233
304
  });
234
305
  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 { addCost, addSubstitution, agentRuns, getCost, saveRun } from "./store.js";
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
- stripApiKeys: cfg.auth === "subscription",
48
- });
49
- addCost(run.id, { stage: step, agent, usd: r.costUsd, inputTokens: r.inputTokens, outputTokens: r.outputTokens });
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("、")},花費約 $${getCost(run.id).toFixed(2)})\n\n${spec}`);
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
  }
package/dist/runner.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
+ import { z } from "zod";
3
4
  import { ADAPTERS, DEFAULT_CYCLE } from "./agents/index.js";
4
5
  import { AgentDef } from "./schemas.js";
5
6
  import { projectRoot, runDir } from "./paths.js";
6
7
  import { exec, execShell } from "./proc.js";
7
8
  import { tail } from "./util.js";
8
- /** 會讓各家 CLI 改走 API 計費的環境變數;訂閱登入模式下執行 agent 時會移除 */
9
+ /** 會讓各家 CLI 改走 API 計費的環境變數;只用訂閱登入,執行 agent 時一律移除 */
9
10
  export const API_KEY_VARS = [
10
11
  "ANTHROPIC_API_KEY",
11
12
  "ANTHROPIC_AUTH_TOKEN",
@@ -29,6 +30,28 @@ const QUOTA_PATTERNS = [
29
30
  /\b429\b/,
30
31
  ];
31
32
  export const isQuotaError = (text) => QUOTA_PATTERNS.some((p) => p.test(text));
33
+ /** Agent 回覆結尾的 <result> 中繼資料(格式定義在各 prompt 的 <reply_format>) */
34
+ export const ResultMeta = z.object({
35
+ status: z.enum(["done", "blocked"]),
36
+ summary: z.string().default(""),
37
+ filesChanged: z.array(z.string()).default([]),
38
+ concerns: z.string().default(""),
39
+ });
40
+ const tagText = (xml, tag) => xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim();
41
+ /** 取回覆中最後一個 <result> 區塊;沒有或格式不合時回傳 undefined,不影響關卡判斷 */
42
+ export function parseResultMeta(text) {
43
+ const block = [...text.matchAll(/<result>([\s\S]*?)<\/result>/g)].at(-1)?.[1];
44
+ if (block === undefined)
45
+ return undefined;
46
+ const files = tagText(block, "files_changed") ?? "";
47
+ const parsed = ResultMeta.safeParse({
48
+ status: tagText(block, "status"),
49
+ summary: tagText(block, "summary"),
50
+ filesChanged: [...files.matchAll(/<file>([\s\S]*?)<\/file>/g)].map((m) => m[1].trim()).filter(Boolean),
51
+ concerns: tagText(block, "concerns"),
52
+ });
53
+ return parsed.success ? parsed.data : undefined;
54
+ }
32
55
  function appendLog(file, text) {
33
56
  mkdirSync(dirname(file), { recursive: true });
34
57
  appendFileSync(file, text.endsWith("\n") ? text : `${text}\n`);
@@ -54,7 +77,7 @@ export async function probeAgent(def) {
54
77
  }
55
78
  }
56
79
  /** 執行任一家的 agent CLI,把輸出正規化成同一種結果 */
57
- export async function runAgent(name, def, t, prompt, opts) {
80
+ export async function runAgent(name, def, t, prompt) {
58
81
  const adapter = ADAPTERS[def.adapter];
59
82
  const inv = adapter.invoke({
60
83
  prompt,
@@ -68,13 +91,12 @@ export async function runAgent(name, def, t, prompt, opts) {
68
91
  appendLog(t.logFile, `# agent=${name} adapter=${def.adapter} cmd=${inv.cmd}`);
69
92
  let done;
70
93
  let lastText = "";
71
- let costUsd;
72
94
  let inputTokens = 0;
73
95
  let outputTokens = 0;
74
96
  const r = await exec(inv.cmd, inv.args, {
75
97
  cwd: t.cwd,
76
98
  env: inv.env,
77
- unsetEnv: opts.stripApiKeys ? API_KEY_VARS : [],
99
+ unsetEnv: API_KEY_VARS,
78
100
  input: inv.input,
79
101
  onStdoutLine: (line) => {
80
102
  if (!line.trim())
@@ -91,8 +113,6 @@ export async function runAgent(name, def, t, prompt, opts) {
91
113
  else if (ev.kind === "usage") {
92
114
  inputTokens += ev.inputTokens ?? 0;
93
115
  outputTokens += ev.outputTokens ?? 0;
94
- if (ev.costUsd !== undefined)
95
- costUsd = (costUsd ?? 0) + ev.costUsd;
96
116
  }
97
117
  else if (ev.kind === "done") {
98
118
  done = { ok: ev.ok, summary: ev.summary };
@@ -102,14 +122,11 @@ export async function runAgent(name, def, t, prompt, opts) {
102
122
  });
103
123
  if (r.stderr.trim())
104
124
  appendLog(t.logFile, `[stderr]\n${r.stderr}`);
105
- // CLI 沒回報花費時,用設定的價格從 token 數估算
106
- if (costUsd === undefined && def.pricing) {
107
- costUsd = (inputTokens * def.pricing.inputPerMTok + outputTokens * def.pricing.outputPerMTok) / 1_000_000;
108
- }
109
125
  const ok = r.code === 0 && (done?.ok ?? true);
110
126
  const summary = done?.summary || lastText || tail(r.stdout, 2000) || tail(r.stderr, 2000);
111
127
  const quotaExhausted = !ok && isQuotaError(`${summary}\n${done?.summary ?? ""}\n${r.stderr}\n${tail(r.stdout, 4000)}`);
112
- return { ok, quotaExhausted, summary, costUsd: costUsd ?? 0, inputTokens, outputTokens };
128
+ const meta = parseResultMeta(summary) ?? parseResultMeta(lastText);
129
+ return { ok, quotaExhausted, summary, meta, inputTokens, outputTokens };
113
130
  }
114
131
  /**
115
132
  * 在 worktree 內執行專案指令(安裝、測試、建置)。
package/dist/schemas.js CHANGED
@@ -46,8 +46,6 @@ export const AgentDef = z.object({
46
46
  extraArgs: z.array(z.string()).default([]),
47
47
  /** 只有 command adapter 使用,`{prompt}` 會被替換成 prompt */
48
48
  command: z.array(z.string()).optional(),
49
- /** CLI 沒有回報花費時,用 token 數估算(每百萬 token 美元) */
50
- pricing: z.object({ inputPerMTok: z.number(), outputPerMTok: z.number() }).optional(),
51
49
  });
52
50
  /** 目標專案可選的 flow.config.json,預設值對應 Vite + TypeScript + Vitest 專案 */
53
51
  export const RepoConfig = z.object({
@@ -70,12 +68,7 @@ export const RepoConfig = z.object({
70
68
  * proceed=繼續實作,爭議記錄在計畫裡,後面還有測試、驗證與程式碼審查把關;stop=停下來等人
71
69
  */
72
70
  tieBreak: z.enum(["proceed", "stop"]).default("proceed"),
73
- /**
74
- * subscription:使用各家 CLI 的訂閱登入,執行 agent 時會移除環境中的 API key,避免意外改走 API 計費;
75
- * api:保留 API key(例如在 CI 中)
76
- */
77
- auth: z.enum(["subscription", "api"]).default("subscription"),
78
- /** 單一 run 最多執行幾次 agent;訂閱制下用這個取代金額預算 */
71
+ /** 單一 run 最多執行幾次 agent */
79
72
  maxAgentRuns: z.number().int().positive().default(60),
80
73
  install: z.string().default("npm install --no-audit --no-fund"),
81
74
  test: z.string().default("npx vitest run"),
@@ -96,8 +89,6 @@ export const FlowRun = z.object({
96
89
  requirement: z.string(),
97
90
  stage: Stage,
98
91
  autopilot: z.boolean(),
99
- /** 選用:以估計花費(美元)為上限;訂閱登入時通常不設定 */
100
- budgetUsd: z.number().positive().optional(),
101
92
  /** 單一 run 最多執行幾次 agent */
102
93
  maxAgentRuns: z.number().int().positive(),
103
94
  /** 暫停前所在的階段與原因(額度用完時) */
package/dist/store.js CHANGED
@@ -3,7 +3,8 @@ import { dirname, join } from "node:path";
3
3
  import { agentflowctlDir, runDir } from "./paths.js";
4
4
  import { FlowRun } from "./schemas.js";
5
5
  const statePath = (id) => join(runDir(id), "state.json");
6
- const costPath = (id) => join(runDir(id), "costs.jsonl");
6
+ /** 檔名沿用舊版的 costs.jsonl,進行中的 run 升級後執行次數不會歸零 */
7
+ const usagePath = (id) => join(runDir(id), "costs.jsonl");
7
8
  /** 先寫暫存檔再 rename,確保 state.json 不會因中斷而只寫一半 */
8
9
  function writeAtomic(path, content) {
9
10
  mkdirSync(dirname(path), { recursive: true });
@@ -29,38 +30,28 @@ export function listRuns() {
29
30
  .filter((r) => r !== undefined)
30
31
  .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
31
32
  }
32
- export function addCost(id, entry) {
33
+ export function addUsage(id, entry) {
33
34
  mkdirSync(runDir(id), { recursive: true });
34
- appendFileSync(costPath(id), `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`);
35
+ appendFileSync(usagePath(id), `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`);
35
36
  }
36
- /** 依 agent 加總花費與 token,方便比較各家模型 */
37
- export function costByAgent(id) {
38
- const p = costPath(id);
37
+ /** 依 agent 加總 token 與執行次數,方便比較各家模型 */
38
+ export function usageByAgent(id) {
39
+ const p = usagePath(id);
39
40
  const out = {};
40
41
  if (!existsSync(p))
41
42
  return out;
42
43
  for (const line of readFileSync(p, "utf8").split("\n").filter(Boolean)) {
43
44
  const e = JSON.parse(line);
44
45
  const key = e.agent ?? "?";
45
- const acc = (out[key] ??= { usd: 0, tokens: 0, runs: 0 });
46
- acc.usd += e.usd ?? 0;
46
+ const acc = (out[key] ??= { tokens: 0, runs: 0 });
47
47
  acc.tokens += (e.inputTokens ?? 0) + (e.outputTokens ?? 0);
48
48
  acc.runs += 1;
49
49
  }
50
50
  return out;
51
51
  }
52
- export function getCost(id) {
53
- const p = costPath(id);
54
- if (!existsSync(p))
55
- return 0;
56
- return readFileSync(p, "utf8")
57
- .split("\n")
58
- .filter(Boolean)
59
- .reduce((sum, line) => sum + (JSON.parse(line).usd ?? 0), 0);
60
- }
61
- /** 這個 run 已執行 agent 的次數(每次執行都會記一筆花費,即使是 0) */
52
+ /** 這個 run 已執行 agent 的次數(每次執行都會記一筆用量) */
62
53
  export function agentRuns(id) {
63
- const p = costPath(id);
54
+ const p = usagePath(id);
64
55
  return existsSync(p) ? readFileSync(p, "utf8").split("\n").filter(Boolean).length : 0;
65
56
  }
66
57
  const subPath = (id) => join(runDir(id), "substitutions.jsonl");
@@ -6,14 +6,10 @@
6
6
  "planReviewQuorum": 1,
7
7
  "planArbiter": true,
8
8
  "tieBreak": "proceed",
9
- "auth": "subscription",
10
9
  "maxAgentRuns": 60,
11
10
  "agents": {
12
11
  "claude": { "adapter": "claude" },
13
- "codex": {
14
- "adapter": "codex",
15
- "pricing": { "inputPerMTok": 1.25, "outputPerMTok": 10 }
16
- }
12
+ "codex": { "adapter": "codex" }
17
13
  },
18
14
  "install": "pnpm ci",
19
15
  "test": "npx vitest run",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentflowctl",
3
3
  "license": "MIT",
4
- "version": "0.1.2",
4
+ "version": "0.2.0",
5
5
  "description": "跨廠商 AI 開發 harness:Claude Code、Codex、Gemini 輪流實作、審查、修正",
6
6
  "keywords": [
7
7
  "ai",
package/prompts/fix.md CHANGED
@@ -1,17 +1,38 @@
1
- 你是資深工程師,這個階段負責修正驗證或程式碼審查發現的問題。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是除錯工程師,負責修正驗證失敗或程式碼審查指出的問題。你要找出每個問題的根本原因,而不是只讓症狀消失。
3
+ </role>
2
4
 
3
- ## 要修正的問題
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
5
- 請先閱讀 .flow/feedback.md,裡面列出了失敗的檢查(型別、lint、測試、建置)或審查意見。
6
- 規格請參考 .flow/spec.md 與 .flow/acceptance.json。
7
-
8
- ## 工作步驟
9
+ <inputs>
10
+ - .flow/feedback.md:失敗的檢查(型別、lint、測試、建置)或審查意見
11
+ - .flow/spec.md 與 .flow/acceptance.json:規格
12
+ </inputs>
9
13
 
14
+ <steps>
10
15
  1. 找出每個問題的根本原因再修正,不要只針對症狀打補丁。
11
16
  2. 在沙箱內自行執行相關檢查,確認問題已解決且沒有造成新的錯誤。
17
+ </steps>
12
18
 
13
- ## 限制
14
-
19
+ <constraints>
15
20
  - 不可刪除測試檔(檔名符合 `{{testPattern}}`),也不可用 skip、放寬斷言、`@ts-ignore`、`eslint-disable` 等方式讓檢查通過。
16
- - 如果測試本身確實有誤,可以修正測試,但必須在回覆中說明理由。
21
+ - 如果測試本身確實有誤,可以修正測試,但必須在回覆的 `<concerns>` 說明理由。
17
22
  - 不要執行 git commit(權限設定已禁止)。
23
+ </constraints>
24
+
25
+ <reply_format>
26
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
27
+
28
+ ```xml
29
+ <result>
30
+ <status>done 或 blocked</status>
31
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
32
+ <files_changed>
33
+ <file>每個新增或修改的檔案路徑各一行</file>
34
+ </files_changed>
35
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
36
+ </result>
37
+ ```
38
+ </reply_format>
@@ -1,27 +1,50 @@
1
- 你是資深工程師,正在用 TDD 開發。測試已經寫好並提交,這個階段要**實作到測試通過**。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是實作工程師,在 TDD 的綠燈階段負責**實作到測試通過**。測試由另一位工程師寫好並提交,你要用符合專案風格的最小實作讓它通過,不可以修改測試。
3
+ </role>
2
4
 
3
- ## 目前任務
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
9
+ <task>
5
10
  ```json
6
11
  {{task}}
7
12
  ```
13
+ </task>
8
14
 
15
+ <inputs>
9
16
  完整規格與計畫請參考 .flow/spec.md、.flow/plan.md。
17
+ </inputs>
10
18
 
11
- ## 目前失敗的測試輸出
12
-
19
+ <red_output>
13
20
  ```
14
21
  {{redOutput}}
15
22
  ```
23
+ </red_output>
16
24
 
17
- ## 工作步驟
18
-
25
+ <steps>
19
26
  1. 若 .flow/feedback.md 存在,先閱讀,並依內容修正。
20
27
  2. 撰寫讓測試通過的最小實作,符合專案既有的程式風格與架構。
21
28
  3. 執行 `{{testCmd}}` 確認全部測試通過(包含既有測試)。
22
29
  4. 測試通過後,在不改變行為的前提下整理程式碼。
30
+ </steps>
23
31
 
24
- ## 限制
25
-
26
- - **不可修改任何測試檔**,修改會被自動還原並視為失敗。若認為測試本身有誤,請在回覆中說明原因。
32
+ <constraints>
33
+ - **不可修改任何測試檔**,修改會被自動還原並視為失敗。若認為測試本身有誤,請寫在回覆的 `<concerns>`。
27
34
  - 不要執行 git commit(權限設定已禁止)。
35
+ </constraints>
36
+
37
+ <reply_format>
38
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
39
+
40
+ ```xml
41
+ <result>
42
+ <status>done 或 blocked</status>
43
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
44
+ <files_changed>
45
+ <file>每個新增或修改的檔案路徑各一行</file>
46
+ </files_changed>
47
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
48
+ </result>
49
+ ```
50
+ </reply_format>
@@ -1,21 +1,44 @@
1
- 你是資深工程師,正在用 TDD 開發。這個階段**只寫測試,不寫實作**。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是測試工程師,在 TDD 的紅燈階段**只寫測試,不寫實作**。你的測試要精準描述任務要新增的行為,並且在功能實作前確實失敗;之後會由另一位工程師實作到通過,而且對方不能修改你的測試。
3
+ </role>
2
4
 
3
- ## 目前任務
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
9
+ <task>
5
10
  ```json
6
11
  {{task}}
7
12
  ```
13
+ </task>
8
14
 
15
+ <inputs>
9
16
  完整規格與計畫請參考 .flow/spec.md、.flow/plan.md。
17
+ </inputs>
10
18
 
11
- ## 工作步驟
12
-
19
+ <steps>
13
20
  1. 若 .flow/feedback.md 存在,先閱讀,並依內容調整做法。
14
21
  2. 依任務描述撰寫測試,檔名必須符合正規表示式 `{{testPattern}}`。
15
22
  3. 測試必須驗證這個任務要新增的行為,並且因為功能尚未實作而**失敗**。
16
23
  4. 可以執行 `{{testCmd}}` 確認測試確實失敗,且失敗原因是斷言或找不到尚未實作的模組,而不是語法錯誤或測試本身寫錯。
24
+ </steps>
17
25
 
18
- ## 限制
19
-
26
+ <constraints>
20
27
  - 不可實作功能本身。可以建立讓測試能編譯所需的最小型別或空殼匯出,但不可以有真正的邏輯。
21
28
  - 不要執行 git commit(權限設定已禁止),外部流程會提交並驗證測試是否失敗。
29
+ </constraints>
30
+
31
+ <reply_format>
32
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
33
+
34
+ ```xml
35
+ <result>
36
+ <status>done 或 blocked</status>
37
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
38
+ <files_changed>
39
+ <file>每個新增或修改的檔案路徑各一行</file>
40
+ </files_changed>
41
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
42
+ </result>
43
+ ```
44
+ </reply_format>
@@ -1,24 +1,29 @@
1
- 你是資深技術主管,負責仲裁一場僵持不下的計畫審查。計畫的作者與審查者已經來回修改多次,仍未達成共識。為了公正,他們的身分已被隱藏;你沒有參與前面的討論,請只根據內容獨立判斷。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是中立的仲裁者,負責裁決一場僵持不下的計畫審查。計畫的作者與審查者已經來回修改多次,仍未達成共識。為了公正,他們的身分已被隱藏;你沒有參與前面的討論,請只根據內容獨立判斷。
3
+ </role>
2
4
 
3
- ## 原始需求
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
9
+ <requirement>
5
10
  {{requirement}}
11
+ </requirement>
6
12
 
7
- ## 要閱讀的檔案
8
-
13
+ <inputs>
9
14
  - .flow/spec.md、.flow/acceptance.json、.flow/plan.md、.flow/tasks.json:目前的規格與計畫(plan.md 最後有作者對審查意見的回應)
10
15
  - .flow/dispute.md:尚未被接受的審查意見
16
+ </inputs>
11
17
 
12
- ## 判斷標準
13
-
18
+ <criteria>
14
19
  只問一個問題:**照目前的計畫實作,能不能正確滿足原始需求?**
15
20
 
16
21
  - 意見如果只是偏好、風格或「也可以這樣做」,而計畫本身能正確完成需求,就核准。
17
22
  - 如果有意見指出計畫會導致錯誤結果、遺漏需求,或無法用測試驗證,就不核准。
18
23
  - 不要因為意見聽起來很有道理就預設它是對的,也不要因為作者有回應就預設問題已解決;請對照需求與程式碼實際判斷。
24
+ </criteria>
19
25
 
20
- ## 輸出
21
-
26
+ <output_format>
22
27
  寫入 .flow/plan-arbiter.json:
23
28
 
24
29
  ```json
@@ -31,7 +36,23 @@
31
36
  ```
32
37
 
33
38
  dispute.md 裡的每一條意見都要列一筆並說明你的判斷。
39
+ </output_format>
34
40
 
35
- ## 限制
36
-
41
+ <constraints>
37
42
  - 只能寫入 .flow/plan-arbiter.json,不可修改規格、計畫或任何程式碼。
43
+ </constraints>
44
+
45
+ <reply_format>
46
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
47
+
48
+ ```xml
49
+ <result>
50
+ <status>done 或 blocked</status>
51
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
52
+ <files_changed>
53
+ <file>每個新增或修改的檔案路徑各一行</file>
54
+ </files_changed>
55
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
56
+ </result>
57
+ ```
58
+ </reply_format>
@@ -1,23 +1,44 @@
1
- 你是資深工程師,負責依照審查意見修改規格與計畫。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是計畫修訂者,負責依照審查意見修改規格與計畫。你要逐條回應每個意見:接受的就改,不同意的就提出具體理由,不可略過。
3
+ </role>
2
4
 
3
- ## 原始需求
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
9
+ <requirement>
5
10
  {{requirement}}
11
+ </requirement>
6
12
 
7
- ## 工作步驟
8
-
13
+ <steps>
9
14
  1. 閱讀 .flow/feedback.md,裡面是其他模型的審查意見。
10
15
  2. 閱讀目前的 .flow/spec.md、.flow/acceptance.json、.flow/plan.md、.flow/tasks.json 與相關程式碼。
11
16
  3. 逐條處理審查意見,直接修改上述四個檔案。
12
17
  4. 在 .flow/plan.md 最後的「## 審查回應」一節,逐條說明每個意見怎麼處理;不同意的意見,請寫出具體理由,而不是忽略它。回應時只談內容,不要提到審查者或你自己是哪個模型、哪家公司,之後可能由第三方匿名仲裁。
18
+ </steps>
13
19
 
14
- ## 格式要求
15
-
20
+ <output_format>
16
21
  - acceptance.json 與 tasks.json 的格式必須維持不變(見檔案內現有內容)。
17
22
  - 每一條驗收條件都至少要有一個任務負責;`dependsOn` 不可有循環。
18
23
  - 測試檔名必須符合正規表示式 `{{testPattern}}`。
24
+ </output_format>
19
25
 
20
- ## 限制
21
-
26
+ <constraints>
22
27
  - 只能修改 .flow/ 底下的檔案,其他變更都會被捨棄。
23
28
  - 不要執行 git commit、切換分支或修改 git 設定。
29
+ </constraints>
30
+
31
+ <reply_format>
32
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
33
+
34
+ ```xml
35
+ <result>
36
+ <status>done 或 blocked</status>
37
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
38
+ <files_changed>
39
+ <file>每個新增或修改的檔案路徑各一行</file>
40
+ </files_changed>
41
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
42
+ </result>
43
+ ```
44
+ </reply_format>
@@ -1,29 +1,34 @@
1
- 你是嚴謹的資深工程師({{reviewer}}),負責在動手實作之前,獨立審查其他 AI agent 撰寫的規格與計畫。規格與計畫的作者:{{author}}。你和作者來自不同的模型,請不要預設他們的判斷是對的。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是計畫審查者({{reviewer}}),負責在動手實作之前,獨立找出其他 AI agent 撰寫的規格與計畫中會影響實作結果的缺陷。規格與計畫的作者:{{author}}。你和作者來自不同的模型,請不要預設他們的判斷是對的。
3
+ </role>
2
4
 
3
- ## 原始需求
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
9
+ <requirement>
5
10
  {{requirement}}
11
+ </requirement>
6
12
 
7
- ## 要審查的檔案
8
-
13
+ <inputs>
9
14
  - .flow/spec.md:規格
10
15
  - .flow/acceptance.json:驗收條件
11
16
  - .flow/plan.md:實作方式
12
17
  - .flow/tasks.json:任務拆解
13
18
 
14
19
  請同時閱讀相關的既有程式碼,確認計畫符合專案的實際架構。
20
+ </inputs>
15
21
 
16
- ## 審查重點
17
-
22
+ <review_focus>
18
23
  1. **需求覆蓋**:規格是否完整涵蓋原始需求?有沒有遺漏、誤解,或加入需求沒要求的範圍?
19
24
  2. **驗收條件**:每一條是否具體、可以用自動化測試驗證?有沒有重要的邊界情況或錯誤處理沒被列入?
20
25
  3. **任務拆解**:每個任務是否小到一次 TDD 循環就能完成,而且能寫出「實作前會失敗」的測試?相依順序是否合理?
21
26
  4. **技術方向**:是否符合專案既有的架構與慣例?有沒有更簡單的做法,或明顯的風險?
22
27
 
23
28
  措辭、格式這類不影響實作結果的小問題,不需要要求修改。
29
+ </review_focus>
24
30
 
25
- ## 輸出
26
-
31
+ <output_format>
27
32
  寫入 .flow/plan-review.json:
28
33
 
29
34
  ```json
@@ -38,7 +43,23 @@
38
43
 
39
44
  - `verdict`:沒有會影響實作結果的問題時為 `approve`,否則為 `changes_requested`。
40
45
  - 每個問題的 `note` 請寫出具體要改哪個檔案的哪個部分,以及建議怎麼改。
46
+ </output_format>
41
47
 
42
- ## 限制
43
-
48
+ <constraints>
44
49
  - 只能寫入 .flow/plan-review.json,不可修改規格、計畫或任何程式碼,其他變更都會被還原。
50
+ </constraints>
51
+
52
+ <reply_format>
53
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
54
+
55
+ ```xml
56
+ <result>
57
+ <status>done 或 blocked</status>
58
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
59
+ <files_changed>
60
+ <file>每個新增或修改的檔案路徑各一行</file>
61
+ </files_changed>
62
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
63
+ </result>
64
+ ```
65
+ </reply_format>
package/prompts/plan.md CHANGED
@@ -1,16 +1,25 @@
1
- 你是資深工程師,這個階段負責把規格拆解成可以逐一用 TDD 完成的任務。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是軟體架構師,負責把規格拆解成可以逐一用 TDD 完成的任務。你關心模組邊界、任務之間的相依順序,以及每個任務能不能先寫出會失敗的測試。
3
+ </role>
2
4
 
3
- ## 輸入
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
9
+ <inputs>
5
10
  - .flow/spec.md
6
11
  - .flow/acceptance.json
12
+ </inputs>
7
13
 
8
- ## 工作步驟
9
-
14
+ <steps>
10
15
  1. 若 .flow/feedback.md 存在,先閱讀,並依內容修正前次的產出。
11
16
  2. 閱讀規格與相關程式碼。
12
17
  3. 撰寫 .flow/plan.md:整體實作方式、要新增或修改的模組、任務順序的理由。
13
- 4. 撰寫 .flow/tasks.json,格式如下:
18
+ 4. 撰寫 .flow/tasks.json。
19
+ </steps>
20
+
21
+ <output_format>
22
+ .flow/tasks.json 的格式:
14
23
 
15
24
  ```json
16
25
  [
@@ -23,15 +32,31 @@
23
32
  }
24
33
  ]
25
34
  ```
35
+ </output_format>
26
36
 
27
- ## 任務拆解原則
28
-
37
+ <guidelines>
29
38
  - 每個任務是一個可獨立測試的垂直切片,小到一次 TDD 循環就能完成。
30
39
  - 每個任務都必須能寫出「在實作前會失敗」的測試;純設定或重構類工作請併入相關任務。
31
40
  - 測試檔名必須符合正規表示式 `{{testPattern}}`。
32
41
  - 每一條驗收條件都至少要有一個任務負責;`dependsOn` 不可有循環。
42
+ </guidelines>
33
43
 
34
- ## 限制
35
-
44
+ <constraints>
36
45
  - 這個階段只能寫入 .flow/ 底下的檔案,其他變更都會被捨棄。
37
46
  - 不要執行 git commit(權限設定已禁止)。
47
+ </constraints>
48
+
49
+ <reply_format>
50
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
51
+
52
+ ```xml
53
+ <result>
54
+ <status>done 或 blocked</status>
55
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
56
+ <files_changed>
57
+ <file>每個新增或修改的檔案路徑各一行</file>
58
+ </files_changed>
59
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
60
+ </result>
61
+ ```
62
+ </reply_format>
package/prompts/review.md CHANGED
@@ -1,22 +1,27 @@
1
- 你是嚴謹的資深工程師({{reviewer}}),負責獨立審查其他 AI agent 的實作。本次變更的作者:{{authors}}。你和作者來自不同的模型,請不要預設他們的做法是對的。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是程式碼審查者({{reviewer}}),負責獨立審查其他 AI agent 的實作,確認每條驗收條件都真的被實作並被測試驗證。本次變更的作者:{{authors}}。你和作者來自不同的模型,請不要預設他們的做法是對的。
3
+ </role>
2
4
 
3
- ## 輸入
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
9
+ <inputs>
5
10
  - 規格:.flow/spec.md
6
11
  - 驗收條件:.flow/acceptance.json
7
12
  - 本次變更:.flow/diff.patch
8
13
  - 自動化檢查結果:.flow/verify.json(已全部通過)
14
+ </inputs>
9
15
 
10
- ## 審查重點
11
-
16
+ <review_focus>
12
17
  1. 逐條確認每個驗收條件是否真的被實作,而且有對應的測試真正驗證它(不是空洞的測試)。
13
18
  2. 是否有明顯的錯誤、邊界情況遺漏、安全問題或效能問題。
14
19
  3. 是否符合專案既有的架構與慣例。
15
20
 
16
21
  風格偏好與無關緊要的小問題不需要要求修改。
22
+ </review_focus>
17
23
 
18
- ## 輸出
19
-
24
+ <output_format>
20
25
  寫入 .flow/review.json:
21
26
 
22
27
  ```json
@@ -31,7 +36,23 @@
31
36
 
32
37
  - `verdict`:全部驗收條件都 `met` 且沒有嚴重問題時為 `approve`,否則為 `changes_requested`。
33
38
  - 每個驗收條件都要有一筆;額外發現的問題也各自列一筆,`note` 請寫出具體位置與修正方向。
39
+ </output_format>
34
40
 
35
- ## 限制
36
-
41
+ <constraints>
37
42
  - 只能寫入 .flow/review.json,不可修改任何程式碼,其他變更都會被捨棄。
43
+ </constraints>
44
+
45
+ <reply_format>
46
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
47
+
48
+ ```xml
49
+ <result>
50
+ <status>done 或 blocked</status>
51
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
52
+ <files_changed>
53
+ <file>每個新增或修改的檔案路徑各一行</file>
54
+ </files_changed>
55
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
56
+ </result>
57
+ ```
58
+ </reply_format>
package/prompts/spec.md CHANGED
@@ -1,24 +1,49 @@
1
- 你是資深工程師,這個階段負責把需求整理成可驗證的規格。目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
1
+ <role>
2
+ 你是需求分析師,負責把模糊的需求轉成範圍明確、每一條都能用自動化測試驗證的規格。你重視的是「做什麼」與「怎樣算完成」,而不是怎麼實作。
3
+ </role>
2
4
 
3
- ## 需求
5
+ <context>
6
+ 目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
7
+ </context>
4
8
 
9
+ <requirement>
5
10
  {{requirement}}
11
+ </requirement>
6
12
 
7
- ## 工作步驟
8
-
13
+ <steps>
9
14
  1. 若 .flow/feedback.md 存在,先閱讀,並依內容修正前次的產出。
10
15
  2. 閱讀專案結構與相關程式碼(package.json、src/、既有測試、設定檔),了解現況與慣例。
11
16
  3. 撰寫 .flow/spec.md,包含:背景、範圍(包含/不包含)、設計重點、介面或資料結構、風險與待確認事項。
12
- 4. 撰寫 .flow/acceptance.json,每一條驗收條件都必須能用自動化測試驗證,格式如下:
17
+ 4. 撰寫 .flow/acceptance.json,每一條驗收條件都必須能用自動化測試驗證。
18
+ </steps>
19
+
20
+ <output_format>
21
+ .flow/acceptance.json 的格式:
13
22
 
14
23
  ```json
15
24
  [
16
25
  { "id": "AC-1", "description": "使用者點擊送出後,表單欄位驗證失敗時顯示錯誤訊息" }
17
26
  ]
18
27
  ```
28
+ </output_format>
19
29
 
20
- ## 限制
21
-
30
+ <constraints>
22
31
  - 這個階段只能寫入 .flow/ 底下的檔案,其他變更都會被捨棄。
23
32
  - 不要執行 git commit,版本控制由外部流程處理(權限設定已禁止)。
24
33
  - 需求不明確時,採用最合理的假設並寫進 spec.md 的「待確認事項」,不要停下來發問。
34
+ </constraints>
35
+
36
+ <reply_format>
37
+ 完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
38
+
39
+ ```xml
40
+ <result>
41
+ <status>done 或 blocked</status>
42
+ <summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
43
+ <files_changed>
44
+ <file>每個新增或修改的檔案路徑各一行</file>
45
+ </files_changed>
46
+ <concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
47
+ </result>
48
+ ```
49
+ </reply_format>
@@ -1,45 +0,0 @@
1
- # 在 issue 加上 flow 標籤時,由 claude 與 codex 輪流完成並開 PR。
2
- # 放到 .github/workflows/flow.yml;在 repo secrets 設定兩家的 API key。
3
- # 注意:CI 無法使用訂閱登入,這個範例會產生 API 費用,
4
- # 專案的 flow.config.json 需設定 "auth": "api",否則 agentflowctl 會移除這些 API key。
5
- name: flow
6
- on:
7
- issues:
8
- types: [labeled]
9
-
10
- permissions:
11
- contents: write
12
- pull-requests: write
13
-
14
- jobs:
15
- flow:
16
- if: github.event.label.name == 'flow'
17
- runs-on: ubuntu-latest
18
- timeout-minutes: 90
19
- steps:
20
- - uses: actions/checkout@v4
21
- with:
22
- fetch-depth: 0
23
- - uses: actions/setup-node@v4
24
- with:
25
- node-version: 22
26
- - name: 安裝 agent CLI
27
- run: npm install -g @anthropic-ai/claude-code @openai/codex
28
- - name: 執行 flow
29
- env:
30
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
31
- CODEX_API_KEY: ${{ secrets.OPENAI_API_KEY }}
32
- GH_TOKEN: ${{ github.token }}
33
- ISSUE_BODY: ${{ github.event.issue.body }}
34
- ISSUE_TITLE: ${{ github.event.issue.title }}
35
- run: |
36
- git config user.name "agentflowctl"
37
- git config user.email "agentflowctl@users.noreply.github.com"
38
- printf '%s\n\n%s\n' "$ISSUE_TITLE" "$ISSUE_BODY" > /tmp/req.md
39
- npx agentflowctl run --req-file /tmp/req.md --budget 10 --cycle claude,codex
40
- - name: 保存紀錄
41
- if: always()
42
- uses: actions/upload-artifact@v4
43
- with:
44
- name: agentflowctl-runs
45
- path: .agentflowctl/runs/