agentflowctl 0.15.0 → 0.16.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
@@ -1,5 +1,7 @@
1
1
  # agentflowctl
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/agentflowctl.svg)](https://www.npmjs.com/package/agentflowctl)
4
+
3
5
  讓 Claude Code、Codex、Gemini CLI 等 agent 在同一個專案裡分工:整理需求、規劃、寫測試與程式、交叉審查,最後建立 PR。agentflowctl 負責推進流程,並用檔案、測試和檢查結果決定能否進到下一步。
4
6
 
5
7
  目前內建支援三種 LLM CLI:**Claude Code、Codex、Gemini CLI**。未來可擴充自定義 LLM adapter,讓其他模型供應商加入流程。目前若要串接其他 CLI,可使用 `command` adapter,自行提供執行命令;需要模型驗證時,也須提供探測命令。
@@ -37,11 +39,13 @@ npx agentflowctl run --req-file ./requirement.md
37
39
 
38
40
  流程預設會自動往下走。想在計畫通過審查後親自確認,可加 `--manual-plan`;確認後執行 `agentflowctl approve <id>`。
39
41
 
42
+ 需要先取用某個階段的產出時,可用 `--stop-after <階段>`。可選停點是 `spec`(規格)、`plan`(計畫審查完成)、`implement`(所有任務完成)、`verify`(測試與 checks 通過)、`review`(程式碼審查完成)或 `pr`(PR 流程完成)。除了 `pr` 會照常結束外,其他停點完成後會進入 `paused`,可檢視 worktree 與 `.flow/` 檔案,再執行 `agentflowctl resume <id>` 從下一階段接續;`--manual-plan` 與 `--stop-after` 不能同時使用。
43
+
40
44
  agentflowctl 會依專案的 `packageManager`、lockfile 與 `package.json` scripts 選擇安裝、測試及檢查指令。第一次執行時,請留意終端機印出的偵測結果;需要調整可在 `flow.config.json` 指定 `install`、`test` 或 `checks`。`package.json` 的依賴或 `test` script 看不出測試框架(且沒有手動設定 `test`)時,終端機會提示「未偵測到測試框架」,並略過紅綠燈;要改回來,在 `flow.config.json` 設定 `test`。
41
45
 
42
46
  ## 查看進度
43
47
 
44
- `run` 開始時會印出 run id,例如 `f-xxxx`。執行中預設只顯示階段進度;加 `-v` 可看到 agent 文字、工具呼叫與專案指令。
48
+ `run` 開始時會印出 run id,例如 `f-xxxx`。執行中預設只顯示階段進度;加 `-v`(或在 `flow.config.json` 設 `"verbose": true`)可看到 agent 文字、工具呼叫與專案指令。
45
49
 
46
50
  ```bash
47
51
  agentflowctl list # 列出 run
@@ -70,6 +74,7 @@ agentflowctl resume f-xxxx # 從暫停、中斷或失敗處接續
70
74
  | 按 Ctrl-C,或終端機意外關閉 | 執行 `agentflowctl resume <id>`;沒有結束紀錄的步驟會重跑 |
71
75
  | `awaiting_approval`:計畫等你確認 | 閱讀 `.agentflowctl/worktrees/<id>/.flow/plan.md`,確認後執行 `agentflowctl approve <id>` |
72
76
  | `paused`:agent 額度用完 | 等額度恢復後執行 `agentflowctl resume <id>`;審查步驟不會換 agent 代審。在仲裁途中暫停時,resume 直接回到仲裁,不重跑計畫審查;暫停期間若改了計畫檔,或在 `flow.config.json` 把 `planArbiter` 關掉,就改成重新審查 |
77
+ | `paused`:已完成指定停點 | 依 `status` 顯示的下一階段檢視產出,再執行 `agentflowctl resume <id>` 接續;run 會保留原本的停點設定 |
73
78
  | `failed`:仲裁連續沒有產生有效裁決 | 仲裁者沒寫出 `.flow/plan-arbiter.json`、格式錯誤或交接無效時不會暫停,會把原因寫進 `.flow/feedback.md` 並自動重跑仲裁(不重跑計畫審查);無效的檔案移到 `.agentflowctl/runs/<id>/reviews/plan-arbiter-<輪>-<agent>-invalid.json`。連續達重試上限才失敗,查看 log 後執行 `agentflowctl resume <id>` 會再回到仲裁 |
74
79
  | `failed`:測試、檢查、審查或 agent 執行失敗 | 依 `status` 提示查看失敗的 log,處理原因後執行 `agentflowctl resume <id>`;失敗階段會重試 |
75
80
  | `failed`:已達 agent 執行次數上限 | 用 `agentflowctl resume <id> --max-agent-runs 100` 調高上限後接續,數字須大於已執行次數 |
@@ -94,10 +99,11 @@ agentflowctl resume f-xxxx
94
99
  | --- | --- |
95
100
  | `run --req "..."` / `--req-file <檔案>` | 二選一,直接輸入需求或讀取檔案 |
96
101
  | `run --manual-plan` | 計畫通過審查後等待你確認,再用 `approve <id>` 繼續 |
102
+ | `run --stop-after <階段>` | 在 `spec`、`plan`、`implement`、`verify` 或 `review` 完成後暫停;`pr` 會完成 PR 流程並結束。與 `--manual-plan` 互斥 |
97
103
  | `run --cycle <名單>` | 指定這次參與的 agent,例如 `--cycle claude,codex`;優先於設定檔的 `cycle` |
98
104
  | `run --model-mode balanced\|adaptive` | 只覆蓋這次 run 的模型模式;`resume` 沿用建立時的模式 |
99
105
  | `run --base <分支>` | 指定起始分支;未設定時使用目前分支 |
100
- | `run --max-attempts <次數>` | 覆蓋這次的重試上限(至少 3;預設取 `AGENTFLOWCTL_MAX_ATTEMPTS`,未設定為 5);失敗後可用 `resume <id> --max-attempts <次數>` 調高 |
106
+ | `run --max-attempts <次數>` | 覆蓋這次的重試上限(至少 3;預設取 `flow.config.json` 的 `maxAttempts`,未設定為 5);失敗後可用 `resume <id> --max-attempts <次數>` 調高 |
101
107
  | `run --max-agent-runs <次數>` | 覆蓋這次的 `maxAgentRuns`;上限不夠時可用 `resume <id> --max-agent-runs <次數>` 調高 |
102
108
  | `-v` / `--verbose` | 執行時顯示 agent 文字、工具呼叫與專案指令,適用於 `run`、`resume`、`approve` |
103
109
 
@@ -105,6 +111,7 @@ agentflowctl resume f-xxxx
105
111
 
106
112
  ```bash
107
113
  agentflowctl run --req-file ./requirement.md --cycle claude,codex --max-agent-runs 80 --manual-plan
114
+ agentflowctl run --req-file ./requirement.md --stop-after plan
108
115
  agentflowctl resume f-xxxx --max-agent-runs 100
109
116
  agentflowctl resume f-xxxx --max-attempts 8
110
117
  ```
@@ -123,7 +130,7 @@ agentflowctl agent cycle claude,codex
123
130
 
124
131
  `agent add` 的 `--adapter` 可填 `claude`、`codex`、`gemini` 或 `command`。`--model` 指定個別 agent 的模型;`--extra-arg=--參數` 可重複使用,傳給該 CLI。使用 `command` adapter 時,把指令寫在 `--` 後,例如 `agentflowctl agent add aider --adapter command -- aider --message {prompt}`。`agent remove <名稱>` 會移除設定與參與名單;`agent cycle` 不帶名單則顯示目前參與者。
125
132
 
126
- `model add/set/remove` 只修改指定 agent 的模型清單;`model remove` 移除最後一個模型時,會檢查參與的 agent 是否仍有模型,不論目前使用哪種模型模式。同一 adapter 的 `agent set` 會保留清單;換 adapter 時會清掉舊 adapter 的模型設定。`agent setup` 遇到同名 agent 會先詢問是否覆寫。
133
+ `model add/set/remove` 只修改指定 agent 的模型清單;`model remove` 移除最後一個模型時,會檢查參與的 agent 是否仍有模型,不論目前使用哪種模型模式。同一 adapter 的 `agent set` 會保留清單;換 adapter 時會清掉舊 adapter 的模型設定。`agent setup` 遇到同名 agent 會先詢問是否覆寫。目前是 adaptive 模式、但參與的 agent 沒有 `models` 時,`agent setup` 會提醒並詢問是否改回 balanced;`run` 遇到同樣情況會列出所有缺 `models` 的 agent,並附上 `model add` 與 `model mode balanced` 兩種修法。
127
134
 
128
135
  ### 依階段與任務難度選模型
129
136
 
@@ -195,6 +202,8 @@ Codex 另有幾點差異:
195
202
  | `planReviewLayers` | `{ "enabled": true, "minTasks": 7, "maxGroups": 5, "tasksPerGroup": 3 }` | 任務夠多時把計畫審查拆成索引與任務群;說明見表格下方 |
196
203
  | `tieBreak` | `"proceed"` | 兩位仲裁者意見分歧時,`"proceed"` 繼續、`"stop"` 停止 |
197
204
  | `maxAgentRuns` | `60` | 一次 run 最多執行幾次 agent;可用指令選項覆蓋 |
205
+ | `maxAttempts` | `5` | 同一關連續失敗幾次後停止,至少 3;可用 `run`/`resume` 的 `--max-attempts` 覆蓋 |
206
+ | `verbose` | `false` | 顯示 agent 文字、工具呼叫與專案指令,效果同 `-v` |
198
207
  | `install`、`test` | 依專案偵測 | 寫成指令字串,例如 `"install": "pnpm install"` |
199
208
  | `checks` | 依專案偵測 | 檢查清單,例如 `[{ "name": "test", "cmd": "pnpm test" }]`;提供時會取代整份預設清單 |
200
209
  | `testPattern` | 常見的 `.test.`、`.spec.` 檔名 | 辨識測試檔的正規表示式字串;非標準檔名時調整 |
@@ -205,15 +214,19 @@ Codex 另有幾點差異:
205
214
 
206
215
  `install`、`test`、`checks` 未設定時,會依 `packageManager`、lockfile 和 `package.json` scripts 偵測。完整範例見 [examples/flow.config.json](examples/flow.config.json)。專案設定每一步都會重新讀取,但已建立 run 的參與 agent 與執行次數上限會沿用建立時的值;要調高後者請用 `resume --max-agent-runs`。
207
216
 
208
- ### 環境變數
217
+ agentflowctl 不讀取任何 `AGENTFLOWCTL_*` 環境變數,設定都寫在 `flow.config.json`。`maxAttempts`(預設 5、至少 3;設得更小會直接報設定錯誤)是單一關卡的重試上限(至少 3),單一 run 可用 `run`/`resume` 的 `--max-attempts` 覆蓋;計畫審查何時交付仲裁與它無關:意見沒有變化,或第 2 輪(修訂過一次)仍被要求修改時就交付,兩家 agent 時自動進入雙盲交叉仲裁,有第三方時由第三方單獨仲裁;`maxAgentRuns` 則是整次 run 的 agent 執行次數上限。修正成功、或計畫審查與程式碼審查整組完成一輪有效審查後,該關的失敗次數會歸零,所以上限只計算連續失敗。分層計畫審查時,同一輪裡只要有一次審查呼叫真的執行成功,計畫審查的失敗次數也會歸零;所以索引與各群輪流各失敗一次、每次重跑都有進展時,不會因累計達上限而失敗。
209
218
 
210
- | 變數 | 預設 | 設定方式與用途 |
211
- | --- | --- | --- |
212
- | `AGENTFLOWCTL_MAX_ATTEMPTS` | `5` | 同一關連續失敗幾次後停止,至少 3(設得更小以 3 計);例如 `AGENTFLOWCTL_MAX_ATTEMPTS=10 agentflowctl run --req "..."` |
213
- | `AGENTFLOWCTL_VERBOSE` | 未開啟 | 設為 `1` 顯示詳細輸出,效果同 `-v` |
214
- | `AGENTFLOWCTL_MAX_TURNS` | `200` | 目前程式會讀取此值,但尚未用它限制 agent 執行 |
219
+ ## 發版(維護者)
220
+
221
+ 在 clone 下來的 repo 裡用 `pnpm release <patch|minor|major|x.y.z> [--dry-run]` 發版,只能在 `main` 執行:
222
+
223
+ ```bash
224
+ pnpm release patch --dry-run # 只檢查與驗證,不建立 Release
225
+ pnpm release minor # 確認後建立 v0.x+1.0 的 GitHub Release
226
+ pnpm release 1.0.0 # 指定版本,必須大於目前最新的 tag
227
+ ```
215
228
 
216
- 環境變數對新啟動的 agentflowctl 程序生效。`AGENTFLOWCTL_MAX_ATTEMPTS` 是單一關卡的重試上限(至少 3),單一 run 可用 `run`/`resume` 的 `--max-attempts` 覆蓋;計畫審查何時交付仲裁與它無關:意見沒有變化,或第 2 輪(修訂過一次)仍被要求修改時就交付,兩家 agent 時自動進入雙盲交叉仲裁,有第三方時由第三方單獨仲裁;`maxAgentRuns` 則是整次 run 的 agent 執行次數上限。修正成功、或計畫審查與程式碼審查整組完成一輪有效審查後,該關的失敗次數會歸零,所以上限只計算連續失敗。分層計畫審查時,同一輪裡只要有一次審查呼叫真的執行成功,計畫審查的失敗次數也會歸零;所以索引與各群輪流各失敗一次、每次重跑都有進展時,不會因累計達上限而失敗。
229
+ 腳本會先檢查目前在 `main`、工作區乾淨、與 `origin/main` 同步、`gh` 已登入、新 tag 不存在,再跑 typecheck、test、build(通過只顯示 ✓,失敗才印出完整輸出),列出自上個 tag 以來的 commit 並等你輸入 `y` 確認,然後用 `gh release create --generate-notes` 建立 Release。npm 由 Release 觸發的 `npm-publish.yml` 發布;版本號取自 tag,腳本不會改 `package.json`。
217
230
 
218
231
  ## 更多文件
219
232
 
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { cleanableRuns, cleanRun } from "./cleanup.js";
12
12
  import { describeDetected, detectProjectDefaults } from "./detect.js";
13
13
  import { CMD_AGENT, listLogs, localTime, logMark, nextLogFile, renderLog } from "./logs.js";
14
14
  import { flowDir, logDir, projectRoot, worktreeDir } from "./paths.js";
15
- import { ModelStage, ModelStrength, TaskList } from "./schemas.js";
15
+ import { ModelStage, ModelStrength, StopAfterStage, TaskList } from "./schemas.js";
16
16
  import { computeInsights, failureLabel, retryLabel } from "./insights.js";
17
17
  import { computeUsageInsights } from "./usageInsights.js";
18
18
  import { computeStats, formatDuration } from "./stats.js";
@@ -41,6 +41,14 @@ function positiveInt(text, flag, min = 1) {
41
41
  throw new Error(`${flag} 必須是不小於 ${min} 的整數:${text}`);
42
42
  return n;
43
43
  }
44
+ function parseStopAfter(value) {
45
+ if (value === undefined)
46
+ return undefined;
47
+ const result = StopAfterStage.safeParse(value);
48
+ if (!result.success)
49
+ throw new Error(`未知的流程停點:${value}(可用:spec、plan、implement、verify、review、pr)`);
50
+ return result.data;
51
+ }
44
52
  function mustGetRun(id) {
45
53
  const run = getRun(id);
46
54
  if (!run)
@@ -60,6 +68,8 @@ function printSummary(run, interrupted = false) {
60
68
  console.log("");
61
69
  console.log(`run ${run.id}`);
62
70
  console.log(`階段 ${run.stage}`);
71
+ if (run.stopAfter)
72
+ console.log(`停點 ${run.stopAfter}`);
63
73
  console.log(`用量 agent 執行 ${agentRuns(run.id)} / ${run.maxAgentRuns} 次`);
64
74
  console.log(`agent ${run.cycle.join("、")}${run.lastWriter ? `(最後作者:${run.lastWriter})` : ""}`);
65
75
  console.log(`分支 ${run.branch}`);
@@ -115,10 +125,17 @@ const TASK_PHASE_MARK = { tests: "🧪", code: "🛠️ ", review: "👀", verif
115
125
  const program = new Command()
116
126
  .name("agentflowctl")
117
127
  .description("在專案資料夾內執行的 Agent 開發流程:規格 → 計畫 → TDD 實作 → 驗證 → 審查 → PR")
118
- .option("-v, --verbose", "執行時印出 agent 的文字、工具呼叫與專案指令(預設只印階段進度)")
128
+ .option("-v, --verbose", "執行時印出 agent 的文字、工具呼叫與專案指令(預設只印階段進度;也可在 flow.config.json 設 verbose)")
119
129
  .hook("preAction", (cmd) => {
120
130
  if (cmd.opts().verbose)
121
131
  config.verbose = true;
132
+ else {
133
+ // doctor、agent setup 等可能在沒有專案或設定壞掉時執行;這裡讀不到就維持安靜,指令本身會回報設定錯誤
134
+ try {
135
+ config.verbose = loadRepoConfig().verbose;
136
+ }
137
+ catch { /* 維持預設 */ }
138
+ }
122
139
  });
123
140
  program
124
141
  .command("run")
@@ -127,14 +144,18 @@ program
127
144
  .option("--req-file <file>", "從檔案讀取需求")
128
145
  .option("--base <branch>", "基底分支(預設為目前的分支)")
129
146
  .option("--max-agent-runs <n>", "單一 run 最多執行幾次 agent(預設取 flow.config.json 的 maxAgentRuns)")
130
- .option("--max-attempts <n>", "同一關連續失敗幾次後停止(預設取 AGENTFLOWCTL_MAX_ATTEMPTS,未設定為 5)")
147
+ .option("--max-attempts <n>", "同一關連續失敗幾次後停止(預設取 flow.config.json 的 maxAttempts,未設定為 5)")
131
148
  .option("--manual-plan", "計畫通過 AI 審查後,仍停下來等你確認", false)
149
+ .option("--stop-after <stage>", "完成公開階段後暫停:spec、plan、implement、verify、review 或 pr")
132
150
  .option("--cycle <agents>", "參與的 agent,例如 claude,codex,gemini(順序不影響分工)")
133
151
  .option("--model-mode <mode>", "這次 run 的模型模式:balanced 或 adaptive")
134
152
  .action(async (opts) => {
135
153
  const requirement = opts.reqFile ? readFileSync(opts.reqFile, "utf8") : opts.req;
136
154
  if (!requirement?.trim())
137
155
  throw new Error("請用 --req 或 --req-file 提供需求");
156
+ const stopAfter = parseStopAfter(opts.stopAfter);
157
+ if (opts.manualPlan && stopAfter)
158
+ throw new Error("--manual-plan 與 --stop-after 不能同時使用");
138
159
  const root = projectRoot();
139
160
  const base = opts.base ?? (await git(root, "branch", "--show-current"));
140
161
  if (!base)
@@ -159,6 +180,7 @@ program
159
180
  branch,
160
181
  requirement: requirement.trim(),
161
182
  stage: "spec",
183
+ stopAfter,
162
184
  autopilot: !opts.manualPlan,
163
185
  maxAgentRuns: opts.maxAgentRuns ? Number(opts.maxAgentRuns) : cfg.maxAgentRuns,
164
186
  maxAttempts: opts.maxAttempts ? positiveInt(opts.maxAttempts, "--max-attempts", MIN_ATTEMPTS) : undefined,
package/dist/config.js CHANGED
@@ -1,11 +1,8 @@
1
1
  /** 重試上限的下限:低於這個值時,修正與審查來不及往返一輪 */
2
2
  export const MIN_ATTEMPTS = 3;
3
+ /** 執行期旗標;沒有環境變數,設定一律來自 flow.config.json 與命令列選項 */
3
4
  export const config = {
4
- /** 單一 Agent 執行最多幾輪工具迴圈,避免卡在迴圈裡 */
5
- maxTurns: Number(process.env.AGENTFLOWCTL_MAX_TURNS ?? 200),
6
- /** 同一個關卡連續失敗幾次後停止;環境變數低於 3 時以 3 計 */
7
- maxAttempts: Math.max(MIN_ATTEMPTS, Number(process.env.AGENTFLOWCTL_MAX_ATTEMPTS ?? 5) || 5),
8
- /** 終端機是否印出 agent 的文字、工具呼叫與專案指令;預設安靜,-v 或 AGENTFLOWCTL_VERBOSE=1 開啟 */
9
- verbose: process.env.AGENTFLOWCTL_VERBOSE === "1",
5
+ /** 終端機是否印出 agent 的文字、工具呼叫與專案指令;預設安靜,flow.config.json 的 verbose 或 -v 開啟 */
6
+ verbose: false,
10
7
  };
11
8
  //# sourceMappingURL=config.js.map
package/dist/engine.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
- import { config } from "./config.js";
5
4
  import { arbitrationDecision } from "./arbitration.js";
6
5
  import { detectProjectDefaults, usesTestFramework, withProjectDefaults } from "./detect.js";
7
6
  import { escapeXml, opinion, reviewIssue } from "./feedback.js";
@@ -127,9 +126,9 @@ function readFeedback(run) {
127
126
  }
128
127
  /** 計畫審查第幾輪仍有人要求修改時交付仲裁;固定值,不受重試上限影響 */
129
128
  const PLAN_ARBITRATION_ROUND = 2;
130
- /** 這個 run 的重試上限:`--max-attempts` 存在 run 裡,沒設定才用環境變數 */
129
+ /** 這個 run 的重試上限:`--max-attempts` 存在 run 裡,沒設定才用 flow.config.json 的 maxAttempts */
131
130
  function attemptLimit(run) {
132
- return run.maxAttempts ?? config.maxAttempts;
131
+ return run.maxAttempts ?? loadRepoConfig().maxAttempts;
133
132
  }
134
133
  /** 關卡未通過:寫入 feedback.md 給下一次嘗試參考,並把原因分類記進 retries.jsonl;超過上限就讓整個 run 失敗 */
135
134
  function retry(run, key, reason, backTo, category) {
@@ -151,6 +150,31 @@ function succeed(run, key, next) {
151
150
  delete attempts[key];
152
151
  return { ...run, attempts, stage: next };
153
152
  }
153
+ /** 回傳這次 transition 完成的公開階段;內部審查/修正 transition 不算交付邊界。 */
154
+ function completedStopStage(current, next) {
155
+ if (current === "spec" && next === "plan")
156
+ return "spec";
157
+ if (current === "plan_review" && next === "implement")
158
+ return "plan";
159
+ if (current === "implement" && next === "verify")
160
+ return "implement";
161
+ if (current === "verify" && next === "review")
162
+ return "verify";
163
+ if (current === "review" && next === "pr")
164
+ return "review";
165
+ return undefined;
166
+ }
167
+ function pauseAtStopAfter(run, next) {
168
+ const completed = completedStopStage(run.stage, next.stage);
169
+ if (!completed || run.stopAfter !== completed)
170
+ return next;
171
+ return {
172
+ ...next,
173
+ stage: "paused",
174
+ pausedStage: next.stage,
175
+ pauseReason: `已完成指定階段 ${completed},等待使用者執行 resume 接續`,
176
+ };
177
+ }
154
178
  /** 設定檔放在主專案根目錄,未 commit 的修改也會生效 */
155
179
  /** 讀取 flow.config.json;沒寫的 install、test、checks 依專案現況偵測 */
156
180
  export function loadRepoConfig() {
@@ -1052,7 +1076,7 @@ export async function advance(initial) {
1052
1076
  });
1053
1077
  }
1054
1078
  try {
1055
- run = saveRun(await STAGES[stage](run));
1079
+ run = saveRun(pauseAtStopAfter(run, await STAGES[stage](run)));
1056
1080
  }
1057
1081
  catch (err) {
1058
1082
  if (err instanceof QuotaPause) {
@@ -90,15 +90,26 @@ export function selectModel(run, cfg, agent, step, complexity, reviewer, scope)
90
90
  ?? def.models.reduce((best, current) => LEVEL[current.strength] > LEVEL[best.strength] ? current : best);
91
91
  return { mode, name: chosen.name, strength: chosen.strength, targetStrength, insufficient: LEVEL[chosen.strength] < LEVEL[targetStrength] };
92
92
  }
93
+ /** adaptive 模式下缺少 models 的錯誤說明:列出全部 agent,並給兩種修法 */
94
+ export function missingModelsMessage(names) {
95
+ return [
96
+ `目前是 adaptive 模式,但以下 agent 沒有登記 models(adaptive 只看 models,不看 model):${names.join("、")}`,
97
+ " 修法一:登記模型與強度(會用目前帳號送一個短請求驗證)",
98
+ ...names.map((n) => ` agentflowctl model add ${n} <模型名稱> --strength low|medium|high`),
99
+ " 修法二:改用 balanced,沿用各 agent 的 model",
100
+ " agentflowctl model mode balanced(只改這次 run:run --model-mode balanced)",
101
+ ].join("\n");
102
+ }
93
103
  /** 啟用 adaptive 前檢查本次實際參與的 agent。 */
94
104
  export function validateAdaptiveConfig(cfg, cycle) {
105
+ const lacking = cycle.filter((name) => cfg.agents[name] && !cfg.agents[name].models?.length);
106
+ if (lacking.length)
107
+ throw new Error(missingModelsMessage(lacking));
95
108
  for (const name of cycle) {
96
109
  const def = cfg.agents[name];
97
110
  if (!def)
98
111
  throw new Error(`未定義的 agent:${name}`);
99
- if (!def.models?.length)
100
- throw new Error(`agent ${name} 的 models 至少要有一個模型`);
101
- const names = def.models.map((m) => m.name);
112
+ const names = (def.models ?? []).map((m) => m.name);
102
113
  if (new Set(names).size !== names.length)
103
114
  throw new Error(`agent ${name} 的 models 有重複名稱`);
104
115
  if (def.adapter === "command") {
package/dist/schemas.js CHANGED
@@ -14,6 +14,8 @@ export const Stage = z.enum([
14
14
  "done",
15
15
  "failed",
16
16
  ]);
17
+ /** 使用者可指定的公開流程停點;內部修正階段不列入。 */
18
+ export const StopAfterStage = z.enum(["spec", "plan", "implement", "verify", "review", "pr"]);
17
19
  export const HandoffSource = z.object({
18
20
  stage: Stage,
19
21
  step: z.string().min(1),
@@ -160,6 +162,10 @@ export const RepoConfig = z.object({
160
162
  tieBreak: z.enum(["proceed", "stop"]).default("proceed"),
161
163
  /** 單一 run 最多執行幾次 agent */
162
164
  maxAgentRuns: z.number().int().positive().default(60),
165
+ /** 同一關連續失敗幾次後停止;至少 3,修正與審查才來得及往返一輪 */
166
+ maxAttempts: z.number().int().min(3).default(5),
167
+ /** 終端機是否印出 agent 的文字、工具呼叫與專案指令;命令列 -v 也能開啟 */
168
+ verbose: z.boolean().default(false),
163
169
  install: z.string().default("npm install --no-audit --no-fund"),
164
170
  test: z.string().default("npx vitest run"),
165
171
  testPattern: z.string().default("\\.(test|spec)\\.[cm]?[jt]sx?$"),
@@ -180,10 +186,12 @@ export const FlowRun = z.object({
180
186
  branch: z.string(),
181
187
  requirement: z.string(),
182
188
  stage: Stage,
189
+ /** 完成這個公開階段後暫停;舊 run 沒有此欄位時一路跑完。 */
190
+ stopAfter: StopAfterStage.optional(),
183
191
  autopilot: z.boolean(),
184
192
  /** 單一 run 最多執行幾次 agent */
185
193
  maxAgentRuns: z.number().int().positive(),
186
- /** 這個 run 同一關連續失敗的上限;沒寫就用 AGENTFLOWCTL_MAX_ATTEMPTS(舊 state.json 沒有此欄位) */
194
+ /** 這個 run 同一關連續失敗的上限;沒寫就用 flow.config.json 的 maxAttempts(舊 state.json 沒有此欄位) */
187
195
  maxAttempts: z.number().int().min(3).optional(),
188
196
  /** 暫停前所在的階段與原因(額度用完時) */
189
197
  pausedStage: Stage.optional(),
package/dist/setup.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { addAgent, setAgent, setCycle } from "./agentConfig.js";
2
+ import { missingModelsMessage } from "./modelSelection.js";
3
+ import { setModelMode } from "./modelConfig.js";
2
4
  import { ADAPTERS } from "./agents/index.js";
3
5
  /**
4
6
  * `agentflowctl agent setup` 的互動精靈。
@@ -74,6 +76,16 @@ export async function runSetup(initial, deps) {
74
76
  log(` ${e.message}`);
75
77
  }
76
78
  }
79
+ // adaptive 只看 models;精靈只會寫 model,參與的 agent 缺 models 時 run 會直接失敗
80
+ const defs = (cfg.agents ?? {});
81
+ const lacking = cfg.cycle.filter((n) => !defs[n]?.models?.length);
82
+ if (lacking.length && (cfg.modelSelection ?? {}).mode === "adaptive") {
83
+ log(`\n⚠️ ${missingModelsMessage(lacking)}`);
84
+ if (await confirm("改回 balanced 模式?(選 n 則維持 adaptive,請之後自行登記 models)", true)) {
85
+ cfg = setModelMode(cfg, "balanced");
86
+ changes.push("modelSelection.mode → balanced");
87
+ }
88
+ }
77
89
  const agents = cfg.agents;
78
90
  log("\n即將寫入:");
79
91
  for (const name of chosen)
@@ -51,6 +51,10 @@ export function stopReport(i) {
51
51
  for (const item of i.open)
52
52
  out.push(` [${item.targetStage}] ${item.id} ${item.summary}(${item.status})`);
53
53
  }
54
+ const stoppedAfterStage = run.stage === "paused" && run.stopAfter && run.pauseReason?.startsWith("已完成指定階段");
55
+ if (stoppedAfterStage) {
56
+ out.push("", "── 指定停點 ──", ` ${run.pauseReason}`, ` 指定停點:${run.stopAfter}`, ` 下一階段:${run.pausedStage ?? "?"}`);
57
+ }
54
58
  const cmd = (c, why) => ` ${c.padEnd(40)} ${why}`;
55
59
  const actions = [];
56
60
  if (run.stage === "failed") {
@@ -61,7 +65,7 @@ export function stopReport(i) {
61
65
  actions.push(cmd(`agentflowctl cancel ${run.id}`, "放棄這個 run"));
62
66
  }
63
67
  else if (run.stage === "paused") {
64
- actions.push(cmd(`agentflowctl resume ${run.id}`, "額度恢復後接續"));
68
+ actions.push(cmd(`agentflowctl resume ${run.id}`, stoppedAfterStage ? `從 ${run.pausedStage ?? "下一階段"} 接續` : "額度恢復後接續"));
65
69
  }
66
70
  else if (run.stage === "awaiting_approval") {
67
71
  actions.push(cmd(`less ${join(i.worktree, ".flow", "plan.md")}`, "檢視計畫"));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentflowctl",
3
3
  "license": "MIT",
4
- "version": "0.15.0",
4
+ "version": "0.16.0",
5
5
  "description": "跨廠商 AI 開發 harness:Claude Code、Codex、Gemini 輪流實作、審查、修正",
6
6
  "keywords": [
7
7
  "ai",
@@ -41,7 +41,8 @@
41
41
  "build": "tsc",
42
42
  "typecheck": "tsc --noEmit",
43
43
  "test": "vitest run",
44
- "prepublishOnly": "pnpm run typecheck && pnpm test && pnpm run build"
44
+ "prepublishOnly": "pnpm run typecheck && pnpm test && pnpm run build",
45
+ "release": "bash scripts/release.sh"
45
46
  },
46
47
  "dependencies": {
47
48
  "commander": "^12.1.0",