agentflowctl 0.1.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/LICENSE +22 -0
- package/README.md +235 -0
- package/dist/agents/claude.js +65 -0
- package/dist/agents/codex.js +39 -0
- package/dist/agents/command.js +21 -0
- package/dist/agents/gemini.js +41 -0
- package/dist/agents/index.js +8 -0
- package/dist/agents/types.js +12 -0
- package/dist/cli.js +252 -0
- package/dist/config.js +9 -0
- package/dist/engine.js +540 -0
- package/dist/git.js +59 -0
- package/dist/paths.js +22 -0
- package/dist/proc.js +40 -0
- package/dist/roles.js +79 -0
- package/dist/runner.js +125 -0
- package/dist/schemas.js +131 -0
- package/dist/store.js +77 -0
- package/dist/tasks.js +51 -0
- package/dist/util.js +33 -0
- package/examples/flow.config.json +26 -0
- package/examples/github-actions.yml +47 -0
- package/package.json +56 -0
- package/prompts/fix.md +17 -0
- package/prompts/implement-code.md +27 -0
- package/prompts/implement-tests.md +21 -0
- package/prompts/plan-arbiter.md +37 -0
- package/prompts/plan-fix.md +23 -0
- package/prompts/plan-review.md +44 -0
- package/prompts/plan.md +37 -0
- package/prompts/review.md +37 -0
- package/prompts/spec.md +24 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { DEFAULT_CYCLE } from "./agents/index.js";
|
|
6
|
+
import { advance, loadRepoConfig } from "./engine.js";
|
|
7
|
+
import { API_KEY_VARS, probeAgent, resolveAgent, runCommand } from "./runner.js";
|
|
8
|
+
import { addWorktree, git, removeWorktree } from "./git.js";
|
|
9
|
+
import { flowDir, logDir, projectRoot, runDir, worktreeDir } from "./paths.js";
|
|
10
|
+
import { TaskList } from "./schemas.js";
|
|
11
|
+
import { agentRuns, costByAgent, getCost, getRun, listRuns, listSubstitutions, saveRun } from "./store.js";
|
|
12
|
+
import { readJsonFile } from "./util.js";
|
|
13
|
+
function mustGetRun(id) {
|
|
14
|
+
const run = getRun(id);
|
|
15
|
+
if (!run)
|
|
16
|
+
throw new Error(`找不到 run:${id}`);
|
|
17
|
+
return run;
|
|
18
|
+
}
|
|
19
|
+
async function drive(run) {
|
|
20
|
+
// Ctrl-C 會同時送給子程序(claude、測試指令),狀態已經寫在 state.json,之後可用 resume 接續
|
|
21
|
+
process.once("SIGINT", () => {
|
|
22
|
+
console.log(`\n已中斷,之後可用 agentflowctl resume ${run.id} 接續`);
|
|
23
|
+
process.exit(130);
|
|
24
|
+
});
|
|
25
|
+
printSummary(await advance(run));
|
|
26
|
+
}
|
|
27
|
+
function printSummary(run) {
|
|
28
|
+
console.log("");
|
|
29
|
+
console.log(`run ${run.id}`);
|
|
30
|
+
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 ${run.cycle.join(" → ")}${run.lastWriter ? `(最後作者:${run.lastWriter})` : ""}`);
|
|
33
|
+
console.log(`分支 ${run.branch}`);
|
|
34
|
+
console.log(`worktree ${worktreeDir(run.id)}`);
|
|
35
|
+
if (run.prUrl)
|
|
36
|
+
console.log(`PR ${run.prUrl}`);
|
|
37
|
+
if (run.planWriter)
|
|
38
|
+
console.log(`計畫 作者 ${run.planWriter}${run.planReviewer ? `,最後提出意見的是 ${run.planReviewer}` : ""}`);
|
|
39
|
+
if (run.stage === "failed") {
|
|
40
|
+
console.log(`失敗於 ${run.failedStage ?? "?"}`);
|
|
41
|
+
console.log(`原因 ${run.failureReason ?? "?"}`);
|
|
42
|
+
console.log(`\n可檢查 ${logDir(run.id)},必要時直接在 worktree 裡修正,再執行 agentflowctl resume ${run.id}`);
|
|
43
|
+
}
|
|
44
|
+
if (run.stage === "paused") {
|
|
45
|
+
console.log(`暫停於 ${run.pausedStage ?? "?"}`);
|
|
46
|
+
console.log(`原因 ${run.pauseReason ?? "?"}`);
|
|
47
|
+
console.log(`\n額度恢復後執行 agentflowctl resume ${run.id}`);
|
|
48
|
+
}
|
|
49
|
+
if (run.stage === "awaiting_approval")
|
|
50
|
+
console.log(`\n確認計畫後執行 agentflowctl approve ${run.id}`);
|
|
51
|
+
}
|
|
52
|
+
/** 決定輪替順序:指令參數 > flow.config.json > 自動偵測已安裝的 CLI */
|
|
53
|
+
async function resolveCycle(flag) {
|
|
54
|
+
const cfg = loadRepoConfig();
|
|
55
|
+
const wanted = flag ? flag.split(",").map((s) => s.trim()).filter(Boolean) : cfg.cycle;
|
|
56
|
+
if (wanted) {
|
|
57
|
+
for (const name of wanted) {
|
|
58
|
+
if (!(await probeAgent(resolveAgent(cfg, name))))
|
|
59
|
+
throw new Error(`找不到可執行的 agent:${name}(可用 agentflowctl doctor 檢查)`);
|
|
60
|
+
}
|
|
61
|
+
return wanted;
|
|
62
|
+
}
|
|
63
|
+
const found = [];
|
|
64
|
+
for (const name of DEFAULT_CYCLE)
|
|
65
|
+
if (await probeAgent(resolveAgent(cfg, name)))
|
|
66
|
+
found.push(name);
|
|
67
|
+
if (!found.length)
|
|
68
|
+
throw new Error("沒有偵測到任何 agent CLI(claude、codex、gemini),可用 agentflowctl doctor 檢查");
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
const program = new Command()
|
|
72
|
+
.name("agentflowctl")
|
|
73
|
+
.description("在專案資料夾內執行的 Agent 開發流程:規格 → 計畫 → TDD 實作 → 驗證 → 審查 → PR");
|
|
74
|
+
program
|
|
75
|
+
.command("run")
|
|
76
|
+
.description("在目前的專案建立並執行新的 flow")
|
|
77
|
+
.option("--req <text>", "需求描述")
|
|
78
|
+
.option("--req-file <file>", "從檔案讀取需求")
|
|
79
|
+
.option("--base <branch>", "基底分支(預設為目前的分支)")
|
|
80
|
+
.option("--max-agent-runs <n>", "單一 run 最多執行幾次 agent(預設取 flow.config.json 的 maxAgentRuns)")
|
|
81
|
+
.option("--budget <usd>", "選用:估計花費上限(美元),使用 API 計費時才需要")
|
|
82
|
+
.option("--manual-plan", "計畫通過 AI 審查後,仍停下來等你確認", false)
|
|
83
|
+
.option("--cycle <agents>", "agent 輪替順序,例如 claude,codex,gemini")
|
|
84
|
+
.action(async (opts) => {
|
|
85
|
+
const requirement = opts.reqFile ? readFileSync(opts.reqFile, "utf8") : opts.req;
|
|
86
|
+
if (!requirement?.trim())
|
|
87
|
+
throw new Error("請用 --req 或 --req-file 提供需求");
|
|
88
|
+
const root = projectRoot();
|
|
89
|
+
const base = opts.base ?? (await git(root, "branch", "--show-current"));
|
|
90
|
+
if (!base)
|
|
91
|
+
throw new Error("目前不在任何分支上,請用 --base 指定基底分支");
|
|
92
|
+
const cycle = await resolveCycle(opts.cycle);
|
|
93
|
+
const id = `f-${Date.now().toString(36)}`;
|
|
94
|
+
const branch = `flow/${id}`;
|
|
95
|
+
console.log(`[${id}] 🌿 從 ${base} 建立 worktree(分支 ${branch})`);
|
|
96
|
+
await addWorktree(root, worktreeDir(id), base, branch);
|
|
97
|
+
console.log(`[${id}] 🤝 agent 輪替順序:${cycle.join(" → ")}`);
|
|
98
|
+
// 先裝好相依套件:有些 agent 的沙箱不能連網,無法自己安裝
|
|
99
|
+
const cfg = loadRepoConfig();
|
|
100
|
+
const install = await runCommand({ runId: id, cwd: worktreeDir(id), logFile: join(logDir(id), "install.log") }, cfg.install);
|
|
101
|
+
if (!install.ok)
|
|
102
|
+
console.log(`[${id}] ⚠️ 安裝相依套件失敗,稍後 verify 階段會再試一次(見 ${join(logDir(id), "install.log")})`);
|
|
103
|
+
const now = new Date().toISOString();
|
|
104
|
+
const run = saveRun({
|
|
105
|
+
id,
|
|
106
|
+
baseBranch: base,
|
|
107
|
+
branch,
|
|
108
|
+
requirement: requirement.trim(),
|
|
109
|
+
stage: "spec",
|
|
110
|
+
autopilot: !opts.manualPlan,
|
|
111
|
+
budgetUsd: opts.budget ? Number(opts.budget) : undefined,
|
|
112
|
+
maxAgentRuns: opts.maxAgentRuns ? Number(opts.maxAgentRuns) : cfg.maxAgentRuns,
|
|
113
|
+
cycle,
|
|
114
|
+
attempts: {},
|
|
115
|
+
taskIndex: 0,
|
|
116
|
+
taskPhase: "tests",
|
|
117
|
+
createdAt: now,
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
});
|
|
120
|
+
await drive(run);
|
|
121
|
+
});
|
|
122
|
+
program
|
|
123
|
+
.command("approve <id>")
|
|
124
|
+
.description("核准計畫並開始實作")
|
|
125
|
+
.action(async (id) => {
|
|
126
|
+
const run = mustGetRun(id);
|
|
127
|
+
if (run.stage !== "awaiting_approval")
|
|
128
|
+
throw new Error(`run 目前在 ${run.stage},不需要核准`);
|
|
129
|
+
await drive(saveRun({ ...run, stage: "implement" }));
|
|
130
|
+
});
|
|
131
|
+
program
|
|
132
|
+
.command("resume <id>")
|
|
133
|
+
.description("從暫停、中斷或失敗的階段接續")
|
|
134
|
+
.option("--max-agent-runs <n>", "調整 agent 執行次數上限")
|
|
135
|
+
.option("--budget <usd>", "調整估計花費上限(美元)")
|
|
136
|
+
.action(async (id, opts) => {
|
|
137
|
+
let run = mustGetRun(id);
|
|
138
|
+
if (opts.budget)
|
|
139
|
+
run = { ...run, budgetUsd: Number(opts.budget) };
|
|
140
|
+
if (opts.maxAgentRuns)
|
|
141
|
+
run = { ...run, maxAgentRuns: Number(opts.maxAgentRuns) };
|
|
142
|
+
if (run.stage === "paused") {
|
|
143
|
+
run = { ...run, stage: run.pausedStage ?? "spec", pausedStage: undefined, pauseReason: undefined };
|
|
144
|
+
}
|
|
145
|
+
if (run.stage === "failed") {
|
|
146
|
+
run = { ...run, stage: run.failedStage ?? "spec", attempts: {}, failedStage: undefined, failureReason: undefined };
|
|
147
|
+
}
|
|
148
|
+
await drive(saveRun(run));
|
|
149
|
+
});
|
|
150
|
+
program
|
|
151
|
+
.command("cancel <id>")
|
|
152
|
+
.description("標記 run 為失敗(執行中的 run 請直接在該終端機按 Ctrl-C)")
|
|
153
|
+
.action((id) => {
|
|
154
|
+
const run = mustGetRun(id);
|
|
155
|
+
saveRun({ ...run, stage: "failed", failedStage: run.stage, failureReason: "使用者取消" });
|
|
156
|
+
console.log(`已取消 ${id}`);
|
|
157
|
+
});
|
|
158
|
+
program
|
|
159
|
+
.command("clean <id>")
|
|
160
|
+
.description("移除 run 的 worktree 與紀錄(分支會保留)")
|
|
161
|
+
.action(async (id) => {
|
|
162
|
+
mustGetRun(id);
|
|
163
|
+
if (existsSync(worktreeDir(id)))
|
|
164
|
+
await removeWorktree(projectRoot(), worktreeDir(id));
|
|
165
|
+
rmSync(runDir(id), { recursive: true, force: true });
|
|
166
|
+
console.log(`已清除 ${id},分支仍保留,不需要時可用 git branch -D 刪除`);
|
|
167
|
+
});
|
|
168
|
+
program
|
|
169
|
+
.command("status <id>")
|
|
170
|
+
.description("顯示 run 狀態與任務進度")
|
|
171
|
+
.action((id) => {
|
|
172
|
+
const run = mustGetRun(id);
|
|
173
|
+
printSummary(run);
|
|
174
|
+
const byAgent = costByAgent(id);
|
|
175
|
+
if (Object.keys(byAgent).length) {
|
|
176
|
+
console.log("\n各 agent 用量");
|
|
177
|
+
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)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const subs = listSubstitutions(id);
|
|
182
|
+
if (subs.length) {
|
|
183
|
+
console.log("\n代打紀錄");
|
|
184
|
+
for (const sub of subs)
|
|
185
|
+
console.log(` ${sub.at.slice(0, 16)} ${sub.step.padEnd(14)} ${sub.planned} → ${sub.actual}${sub.note ? `(${sub.note})` : ""}`);
|
|
186
|
+
}
|
|
187
|
+
const tasks = readJsonFile(join(flowDir(id), "tasks.ordered.json"), TaskList);
|
|
188
|
+
if (!tasks.ok)
|
|
189
|
+
return;
|
|
190
|
+
console.log("\n任務");
|
|
191
|
+
tasks.data.forEach((t, i) => {
|
|
192
|
+
const active = i === run.taskIndex && run.stage === "implement";
|
|
193
|
+
const mark = i < run.taskIndex ? "✅" : active ? (run.taskPhase === "tests" ? "🧪" : "🛠️ ") : "⬜";
|
|
194
|
+
console.log(` ${mark} ${t.id} ${t.title}`);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
program
|
|
198
|
+
.command("doctor")
|
|
199
|
+
.description("檢查可用的 agent CLI 與目前的輪替設定")
|
|
200
|
+
.action(async () => {
|
|
201
|
+
const cfg = loadRepoConfig();
|
|
202
|
+
const names = [...new Set([...DEFAULT_CYCLE, ...Object.keys(cfg.agents)])];
|
|
203
|
+
for (const name of names) {
|
|
204
|
+
const def = resolveAgent(cfg, name);
|
|
205
|
+
const ok = await probeAgent(def);
|
|
206
|
+
console.log(`${ok ? "✅" : "❌"} ${name.padEnd(10)} adapter=${def.adapter}${def.model ? ` model=${def.model}` : ""}`);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
console.log(`\n輪替順序:${(await resolveCycle()).join(" → ")}`);
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
console.log(`\n${e.message}`);
|
|
213
|
+
}
|
|
214
|
+
const leaked = API_KEY_VARS.filter((k) => process.env[k]);
|
|
215
|
+
console.log(`\n登入方式:${cfg.auth === "subscription" ? "訂閱登入(執行 agent 時會移除 API key)" : "API key"}`);
|
|
216
|
+
if (leaked.length) {
|
|
217
|
+
console.log(cfg.auth === "subscription"
|
|
218
|
+
? `⚠️ 環境中有 ${leaked.join("、")},agentflowctl 執行 agent 時會移除,但你自己直接執行 CLI 時仍可能改走 API 計費`
|
|
219
|
+
: `使用中的 API key:${leaked.join("、")}`);
|
|
220
|
+
}
|
|
221
|
+
console.log(`單一 run 的 agent 執行上限:${cfg.maxAgentRuns} 次`);
|
|
222
|
+
console.log(`修正策略:${cfg.fixStrategy} 測試與實作分開:${cfg.tddSplit ? "是" : "否"}`);
|
|
223
|
+
console.log(`程式碼審查人數:${cfg.reviewQuorum} 計畫審查人數:${cfg.planReviewQuorum} 計畫仲裁:${cfg.planArbiter ? "開啟" : "關閉"}`);
|
|
224
|
+
});
|
|
225
|
+
program
|
|
226
|
+
.command("list")
|
|
227
|
+
.description("列出這個專案的所有 run")
|
|
228
|
+
.action(() => {
|
|
229
|
+
for (const r of listRuns()) {
|
|
230
|
+
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}`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
program
|
|
235
|
+
.command("logs <id>")
|
|
236
|
+
.description("列出 log 檔,或顯示最新一份")
|
|
237
|
+
.option("--latest", "顯示最新一份 log 的內容", false)
|
|
238
|
+
.action((id, opts) => {
|
|
239
|
+
mustGetRun(id);
|
|
240
|
+
const dir = logDir(id);
|
|
241
|
+
const files = existsSync(dir) ? readdirSync(dir).sort() : [];
|
|
242
|
+
if (!files.length)
|
|
243
|
+
return console.log("還沒有 log");
|
|
244
|
+
if (!opts.latest)
|
|
245
|
+
return files.forEach((f) => console.log(join(dir, f)));
|
|
246
|
+
console.log(readFileSync(join(dir, files.at(-1)), "utf8"));
|
|
247
|
+
});
|
|
248
|
+
program.parseAsync().catch((err) => {
|
|
249
|
+
console.error(`錯誤:${err.message}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|
|
252
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
/** 指定 Claude Code 使用的模型;未設定則用 Claude Code 預設值 */
|
|
3
|
+
model: process.env.AGENTFLOWCTL_MODEL,
|
|
4
|
+
/** 單一 Agent 執行最多幾輪工具迴圈,避免卡在迴圈裡 */
|
|
5
|
+
maxTurns: Number(process.env.AGENTFLOWCTL_MAX_TURNS ?? 80),
|
|
6
|
+
/** 同一個關卡連續失敗幾次後停止 */
|
|
7
|
+
maxAttempts: Number(process.env.AGENTFLOWCTL_MAX_ATTEMPTS ?? 3),
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=config.js.map
|