agentflowctl 0.5.0 → 0.6.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 +13 -3
- package/dist/arbitration.js +5 -3
- package/dist/engine.js +111 -23
- package/dist/handoff.js +109 -0
- package/dist/paths.js +1 -0
- package/dist/schemas.js +49 -0
- package/package.json +1 -1
- package/prompts/fix.md +11 -0
- package/prompts/implement-code.md +11 -0
- package/prompts/implement-tests.md +11 -0
- package/prompts/plan-arbiter.md +13 -1
- package/prompts/plan-fix.md +11 -0
- package/prompts/plan-review.md +12 -1
- package/prompts/plan.md +11 -0
- package/prompts/review.md +12 -1
- package/prompts/spec.md +11 -0
package/README.md
CHANGED
|
@@ -307,12 +307,14 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
307
307
|
| 有幾家 | 誰來仲裁 | 結果 |
|
|
308
308
|
| --- | --- | --- |
|
|
309
309
|
| 三家以上 | 沒參與這次討論的那一家 | 核准就繼續,否則 run 失敗 |
|
|
310
|
-
| 兩家 | 兩家各自在全新 context 裡判斷 |
|
|
310
|
+
| 兩家 | 兩家各自在全新 context 裡判斷 | 都核准就繼續;都不核准就依裁決意見修訂並重新審查;分歧依 `tieBreak` |
|
|
311
311
|
| 一家 | 同一家 | 由它自己仲裁 |
|
|
312
312
|
|
|
313
313
|
兩家時的仲裁是雙盲的。仲裁者只看計畫,以及一份不含模型名稱的爭議清單(`.flow/dispute.md`)。帶有名稱的審查紀錄移到 worktree 以外。`tieBreak` 預設 `proceed`,因為後面還有測試紅燈、綠燈、verify 與程式碼審查。
|
|
314
314
|
|
|
315
|
-
計畫定案或仲裁最終停止時,裁決與每位仲裁者的理由附在 `plan.md` 最後的「仲裁紀錄」。需再修訂時,裁決理由寫進 `.flow/feedback.md`,供修訂者處理;重新審查會從第一輪計數。原始審查與每輪仲裁紀錄在 `.agentflowctl/runs/<id>/reviews
|
|
315
|
+
計畫定案或仲裁最終停止時,裁決與每位仲裁者的理由附在 `plan.md` 最後的「仲裁紀錄」。需再修訂時,裁決理由寫進 `.flow/feedback.md`,供修訂者處理;重新審查會從第一輪計數。原始審查與每輪仲裁紀錄在 `.agentflowctl/runs/<id>/reviews/`。兩家都要求修改時不因仲裁輪數而直接失敗;整個 run 仍受 `maxAgentRuns` 限制。
|
|
316
|
+
|
|
317
|
+
仲裁 JSON 的 `verdict` 應為 `approve` 或 `changes_requested`;若模型寫成 `reject`,程式會當成 `changes_requested` 並保留理由。缺少檔案、JSON 格式錯誤或其他不合法輸出不算反對票,run 會暫停並顯示驗證錯誤;檢查 `agentflowctl logs <id>` 與 `.flow/plan-arbiter.json` 後可用 `resume` 重新執行。
|
|
316
318
|
|
|
317
319
|
## 階段與通過條件
|
|
318
320
|
|
|
@@ -322,7 +324,7 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
|
|
|
322
324
|
| plan | 與 spec 同一位 | zod、相依存在、無循環、每條驗收條件都有任務 | 重試 |
|
|
323
325
|
| plan_review | 計畫作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve` | 進入 plan_fix |
|
|
324
326
|
| plan_fix | 依 `fixStrategy` | 修改後仍通過 plan 的格式與 DAG 檢查 | 還原並重試 |
|
|
325
|
-
| 仲裁 | 見上一節 | 一致核准;分歧依 `tieBreak` |
|
|
327
|
+
| 仲裁 | 見上一節 | 一致核准;分歧依 `tieBreak` | 兩家都不核准時進入 plan_fix 再審查;第三方不核准或 `tieBreak: stop` 時失敗 |
|
|
326
328
|
| 人工確認 | 你(只有 `--manual-plan`) | `agentflowctl approve` | — |
|
|
327
329
|
| implement 紅燈 | 洗牌輪流,每家各一次 | 有測試變更,而且測試執行後失敗 | 還原並重試 |
|
|
328
330
|
| implement 綠燈 | 測試作者以外隨機一位 | 測試檔沒有任何修改,而且測試通過 | 還原,或帶著輸出重試 |
|
|
@@ -349,6 +351,14 @@ Agent 的最後回覆要附上 XML 中繼資料:
|
|
|
349
351
|
|
|
350
352
|
`blocked` 與 `concerns` 會印在終端機上,完整回覆留在 log,可用 `agentflowctl logs` 查看。這份中繼資料只給人看;缺少或格式錯誤都不影響流程,是否通過仍由上表的程式檢查決定。
|
|
351
353
|
|
|
354
|
+
### Agent 交接紀錄
|
|
355
|
+
|
|
356
|
+
每次 agent 執行前,程式會把與當前階段有關的未結事項寫入 `.flow/handoff-context.md`。agent 完成時必須寫 `.flow/handoff-response.json`,包含 `newIssues` 和 `dispositions` 兩個陣列;沒有事項也要明確寫成 `{ "newIssues": [], "dispositions": [] }`。缺少檔案或 JSON 格式不合法,會依該步驟的重試規則處理。
|
|
357
|
+
|
|
358
|
+
程式只在原有關卡通過後接收交接回覆,並將正式紀錄原子儲存於 `.agentflowctl/runs/<id>/handoff.json`。`action` 是需要後續處理的事項;`info` 只供參考。作者只能提出已修正並附證據,審查者才能確認結案或附理由接受。XML `<concerns>` 可以供人閱讀,但重要疑慮必須寫進交接 JSON,才能交給下一位 agent。額度代打與重試不會接收失敗呼叫的交接內容;中斷後可用 `resume` 接續。
|
|
359
|
+
|
|
360
|
+
計畫審查或程式碼審查若核准,但該階段仍有未結的 `action`,程式會視為互相矛盾的審查結果並重試。計畫定案和開 PR 前也會再檢查一次;`info` 會提供給目標階段閱讀,但不阻擋通關。
|
|
361
|
+
|
|
352
362
|
## Adapter
|
|
353
363
|
|
|
354
364
|
| adapter | 執行方式 | 權限 |
|
package/dist/arbitration.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export function arbitrationDecision(verdicts, tieBreak
|
|
1
|
+
/** 兩家仲裁都要求修改時交回計畫修訂;整體執行次數由 maxAgentRuns 限制。 */
|
|
2
|
+
export function arbitrationDecision(verdicts, tieBreak) {
|
|
3
|
+
if (verdicts.includes("abstain"))
|
|
4
|
+
return "invalid";
|
|
3
5
|
const approvals = verdicts.filter((v) => v === "approve").length;
|
|
4
6
|
if (approvals === verdicts.length)
|
|
5
7
|
return "proceed";
|
|
6
8
|
if (approvals > 0)
|
|
7
9
|
return tieBreak;
|
|
8
|
-
return verdicts.length === 2
|
|
10
|
+
return verdicts.length === 2 ? "revise" : "stop";
|
|
9
11
|
}
|
|
10
12
|
//# sourceMappingURL=arbitration.js.map
|
package/dist/engine.js
CHANGED
|
@@ -5,12 +5,13 @@ import { config } from "./config.js";
|
|
|
5
5
|
import { arbitrationDecision } from "./arbitration.js";
|
|
6
6
|
import { detectProjectDefaults, withProjectDefaults } from "./detect.js";
|
|
7
7
|
import { changedFiles, commitAll, discardChanges, git, headCommit, resetTo } from "./git.js";
|
|
8
|
+
import { acceptHandoff, openActions, prepareHandoff, previewHandoff, readHandoff, recoverHandoff, reviewHandoffGate, validateHandoffResponse } from "./handoff.js";
|
|
8
9
|
import { flowDir, logDir, projectRoot, runDir, worktreeDir } from "./paths.js";
|
|
9
10
|
import { CMD_AGENT, nextLogFile } from "./logs.js";
|
|
10
11
|
import { exec } from "./proc.js";
|
|
11
12
|
import { arbiterPanel, availableAgent, fixAgent, planAgent, planFixAgent, reviewers, specAgent, taskAgents } from "./roles.js";
|
|
12
13
|
import { resolveAgent, runAgent, runCommand } from "./runner.js";
|
|
13
|
-
import { AcceptanceList, RepoConfig, ReviewResult, TaskList, } from "./schemas.js";
|
|
14
|
+
import { AcceptanceList, ArbiterResult, RepoConfig, ReviewResult, TaskList, } from "./schemas.js";
|
|
14
15
|
import { addSubstitution, addUsage, agentRuns, saveRun } from "./store.js";
|
|
15
16
|
import { orderTasks } from "./tasks.js";
|
|
16
17
|
import { readJsonFile, renderPrompt, tail } from "./util.js";
|
|
@@ -30,6 +31,14 @@ export class QuotaPause extends Error {
|
|
|
30
31
|
}
|
|
31
32
|
/** 這次執行中已確認額度用完的 agent;程序結束即清空,resume 時會重新嘗試 */
|
|
32
33
|
const exhausted = new Set();
|
|
34
|
+
function handoffTarget(run) {
|
|
35
|
+
return ["spec", "plan", "plan_review", "plan_fix"].includes(run.stage) ? "plan" : "code";
|
|
36
|
+
}
|
|
37
|
+
/** 同一輪重跑使用相同 key;重試次數或 panel 位置改變時使用新 key。 */
|
|
38
|
+
function handoffKey(run, step, slot, agent) {
|
|
39
|
+
const attempts = Object.entries(run.attempts).sort(([a], [b]) => a.localeCompare(b));
|
|
40
|
+
return JSON.stringify([run.id, run.stage, step, run.taskIndex, run.taskPhase, attempts, slot, agent]);
|
|
41
|
+
}
|
|
33
42
|
async function agentStep(run, planned, step, prompt, mode) {
|
|
34
43
|
const cfg = loadRepoConfig();
|
|
35
44
|
const reset = mode.reset ?? (() => discardChanges(worktreeDir(run.id)));
|
|
@@ -47,11 +56,13 @@ async function agentStep(run, planned, step, prompt, mode) {
|
|
|
47
56
|
info(run, `🔁 ${agent} 額度已用完,${step} 由 ${sub} 代打${note ? `(注意:${note})` : ""}`);
|
|
48
57
|
agent = sub;
|
|
49
58
|
}
|
|
59
|
+
const callKey = handoffKey(run, step, mode.slot ?? 0, agent);
|
|
60
|
+
prepareHandoff(run.id, callKey, handoffTarget(run), mode.blind ?? false);
|
|
50
61
|
const r = await runAgent(agent, resolveAgent(cfg, agent), target(run, step, agent), prompt);
|
|
51
62
|
addUsage(run.id, { stage: step, agent, inputTokens: r.inputTokens, outputTokens: r.outputTokens });
|
|
52
63
|
if (!r.quotaExhausted) {
|
|
53
64
|
reportMeta(run, agent, r);
|
|
54
|
-
return { r, agent };
|
|
65
|
+
return { r, agent, step, callKey };
|
|
55
66
|
}
|
|
56
67
|
info(run, `⛽ ${agent} 的額度已用完`);
|
|
57
68
|
exhausted.add(agent);
|
|
@@ -59,6 +70,28 @@ async function agentStep(run, planned, step, prompt, mode) {
|
|
|
59
70
|
// 迴圈回到開頭:review 會停下,write 會找代打
|
|
60
71
|
}
|
|
61
72
|
}
|
|
73
|
+
/** 原有關卡已通過後才接受交接;失敗回覆不進入正式紀錄。 */
|
|
74
|
+
function finishHandoff(run, outcome, role, gate) {
|
|
75
|
+
const parsed = validateHandoffResponse(run.id);
|
|
76
|
+
if (!parsed.ok)
|
|
77
|
+
return parsed.error;
|
|
78
|
+
try {
|
|
79
|
+
const source = {
|
|
80
|
+
stage: run.stage, step: outcome.step, agent: outcome.agent, callKey: outcome.callKey,
|
|
81
|
+
};
|
|
82
|
+
const preview = previewHandoff(readHandoff(run.id), outcome.callKey, source, parsed.data, role);
|
|
83
|
+
if (gate) {
|
|
84
|
+
const error = reviewHandoffGate(preview, gate.target, gate.verdict);
|
|
85
|
+
if (error)
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
acceptHandoff(run.id, outcome.callKey, source, parsed.data, role);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return error.message;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
62
95
|
/** 印出回覆裡的 XML 中繼資料;只供人檢視,關卡仍由程式檢查決定 */
|
|
63
96
|
function reportMeta(run, agent, r) {
|
|
64
97
|
if (!r.meta)
|
|
@@ -113,7 +146,8 @@ function loadOrderedTasks(run) {
|
|
|
113
146
|
async function specStage(run) {
|
|
114
147
|
const agent = specAgent(run.cycle, run.id);
|
|
115
148
|
info(run, `📝 產生規格(${agent})`);
|
|
116
|
-
const
|
|
149
|
+
const outcome = await agentStep(run, agent, "spec", renderPrompt("spec", { requirement: run.requirement }), { kind: "write" });
|
|
150
|
+
const { r } = outcome;
|
|
117
151
|
await discardChanges(worktreeDir(run.id)); // 這個階段只允許寫 .flow/
|
|
118
152
|
if (!r.ok)
|
|
119
153
|
return retry(run, "spec", `Agent 執行失敗:${r.summary}`, "spec");
|
|
@@ -125,6 +159,9 @@ async function specStage(run) {
|
|
|
125
159
|
const ids = ac.data.map((a) => a.id);
|
|
126
160
|
if (new Set(ids).size !== ids.length)
|
|
127
161
|
return retry(run, "spec", "驗收條件 id 有重複", "spec");
|
|
162
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
163
|
+
if (handoffError)
|
|
164
|
+
return retry(run, "spec", handoffError, "spec");
|
|
128
165
|
return succeed(run, "spec", "plan");
|
|
129
166
|
}
|
|
130
167
|
// ── 規格與計畫檔案:審查者與仲裁者只能讀,不能改 ──
|
|
@@ -177,6 +214,9 @@ function announceTasks(run, ordered) {
|
|
|
177
214
|
}
|
|
178
215
|
/** 計畫定案後:預設直接開始實作;--manual-plan 時才停下來等人 */
|
|
179
216
|
function planSettled(run, key) {
|
|
217
|
+
const pending = openActions(readHandoff(run.id), "plan");
|
|
218
|
+
if (pending.length)
|
|
219
|
+
return retry(run, "plan-handoff", `計畫仍有未結交接事項:${pending.map((item) => item.id).join("、")}`, "plan_fix");
|
|
180
220
|
const next = run.autopilot ? "implement" : "awaiting_approval";
|
|
181
221
|
const ordered = loadOrderedTasks(run);
|
|
182
222
|
announceTasks(run, ordered);
|
|
@@ -192,13 +232,17 @@ async function planStage(run) {
|
|
|
192
232
|
const agent = planAgent(run.cycle, run.id);
|
|
193
233
|
info(run, `🗺️ 拆解任務(${agent})`);
|
|
194
234
|
const cfg = loadRepoConfig();
|
|
195
|
-
const
|
|
235
|
+
const outcome = await agentStep(run, agent, "plan", renderPrompt("plan", { testPattern: cfg.testPattern }), { kind: "write" });
|
|
236
|
+
const { r, agent: actual } = outcome;
|
|
196
237
|
await discardChanges(worktreeDir(run.id));
|
|
197
238
|
if (!r.ok)
|
|
198
239
|
return retry(run, "plan", `Agent 執行失敗:${r.summary}`, "plan");
|
|
199
240
|
const ordered = validatePlan(run);
|
|
200
241
|
if (typeof ordered === "string")
|
|
201
242
|
return retry(run, "plan", ordered, "plan");
|
|
243
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
244
|
+
if (handoffError)
|
|
245
|
+
return retry(run, "plan", handoffError, "plan");
|
|
202
246
|
acceptPlan(run, ordered);
|
|
203
247
|
rmSync(flowFile(run, "plan-review-last.txt"), { force: true });
|
|
204
248
|
return { ...succeed(run, "plan", "plan_review"), planWriter: actual };
|
|
@@ -215,7 +259,8 @@ async function planReviewStage(run) {
|
|
|
215
259
|
info(run, `🧐 計畫審查第 ${round} 輪(${reviewer},作者 ${author})`);
|
|
216
260
|
const snap = snapshotPlan(run);
|
|
217
261
|
rmSync(flowFile(run, "plan-review.json"), { force: true });
|
|
218
|
-
const
|
|
262
|
+
const outcome = await agentStep(run, reviewer, "plan-review", renderPrompt("plan-review", { reviewer, author, requirement: run.requirement }), { kind: "review", slot: panel.indexOf(reviewer), reset: async () => { await discardChanges(worktreeDir(run.id)); restorePlan(run, snap); } });
|
|
263
|
+
const { r } = outcome;
|
|
219
264
|
await discardChanges(worktreeDir(run.id));
|
|
220
265
|
const tampered = restorePlan(run, snap);
|
|
221
266
|
if (tampered.length)
|
|
@@ -225,6 +270,9 @@ async function planReviewStage(run) {
|
|
|
225
270
|
const review = readJsonFile(flowFile(run, "plan-review.json"), ReviewResult);
|
|
226
271
|
if (!review.ok)
|
|
227
272
|
return retry(run, "plan-review-run", review.error, "plan_review");
|
|
273
|
+
const handoffError = finishHandoff(run, outcome, "reviewer", { target: "plan", verdict: review.data.verdict });
|
|
274
|
+
if (handoffError)
|
|
275
|
+
return retry(run, "plan-review-run", handoffError, "plan_review");
|
|
228
276
|
// 審查紀錄移到 worktree 外面:之後的仲裁者看不到是哪一家提的意見
|
|
229
277
|
mkdirSync(join(runDir(run.id), "reviews"), { recursive: true });
|
|
230
278
|
renameSync(flowFile(run, "plan-review.json"), join(runDir(run.id), "reviews", `plan-review-${round}-${reviewer}.json`));
|
|
@@ -272,7 +320,8 @@ async function planFixStage(run) {
|
|
|
272
320
|
info(run, `✏️ 依 ${run.planReviewer ?? "審查者"} 的意見修改計畫(${agent})`);
|
|
273
321
|
const feedback = readFeedback(run);
|
|
274
322
|
const snap = snapshotPlan(run);
|
|
275
|
-
const
|
|
323
|
+
const outcome = await agentStep(run, agent, "plan-fix", renderPrompt("plan-fix", { requirement: run.requirement, testPattern: cfg.testPattern }), { kind: "write", reset: async () => { await discardChanges(worktreeDir(run.id)); restorePlan(run, snap); } });
|
|
324
|
+
const { r, agent: actual } = outcome;
|
|
276
325
|
await discardChanges(worktreeDir(run.id)); // 只允許改 .flow/
|
|
277
326
|
if (!r.ok) {
|
|
278
327
|
restorePlan(run, snap);
|
|
@@ -283,6 +332,11 @@ async function planFixStage(run) {
|
|
|
283
332
|
restorePlan(run, snap);
|
|
284
333
|
return retry(run, "plan-fix", `${feedback}\n\n另外,修改後的計畫沒有通過格式檢查,已還原:\n${ordered}`, "plan_fix");
|
|
285
334
|
}
|
|
335
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
336
|
+
if (handoffError) {
|
|
337
|
+
restorePlan(run, snap);
|
|
338
|
+
return retry(run, "plan-fix", handoffError, "plan_fix");
|
|
339
|
+
}
|
|
286
340
|
acceptPlan(run, ordered);
|
|
287
341
|
writeFileSync(flowFile(run, "feedback.md"), feedback); // 保留審查意見,讓下一輪審查者知道上次提了什麼
|
|
288
342
|
const attempts = { ...run.attempts };
|
|
@@ -299,30 +353,38 @@ async function arbitratePlan(run) {
|
|
|
299
353
|
const panel = arbiterPanel(run.cycle, `${run.id}:arbiter`, run.planWriter, run.planReviewer);
|
|
300
354
|
const mode = panel.length > 1 ? "雙盲交叉仲裁" : "第三方仲裁";
|
|
301
355
|
const verdicts = [];
|
|
302
|
-
for (const arbiter of panel) {
|
|
356
|
+
for (const [slot, arbiter] of panel.entries()) {
|
|
303
357
|
info(run, `⚖️ ${mode}(${arbiter})`);
|
|
304
358
|
const snap = snapshotPlan(run);
|
|
305
359
|
rmSync(flowFile(run, "plan-arbiter.json"), { force: true });
|
|
306
|
-
const
|
|
307
|
-
kind: "review",
|
|
360
|
+
const outcome = await agentStep(run, arbiter, "plan-arbiter", renderPrompt("plan-arbiter", { requirement: run.requirement }), {
|
|
361
|
+
kind: "review", slot, blind: true,
|
|
308
362
|
reset: async () => { await discardChanges(worktreeDir(run.id)); restorePlan(run, snap); },
|
|
309
363
|
});
|
|
364
|
+
const { r } = outcome;
|
|
310
365
|
await discardChanges(worktreeDir(run.id));
|
|
311
366
|
restorePlan(run, snap);
|
|
312
|
-
const result = r.ok ? readJsonFile(flowFile(run, "plan-arbiter.json"),
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
367
|
+
const result = r.ok ? readJsonFile(flowFile(run, "plan-arbiter.json"), ArbiterResult) : undefined;
|
|
368
|
+
const handoffError = result?.ok ? finishHandoff(run, outcome, "reviewer", { target: "plan", verdict: result.data.verdict }) : undefined;
|
|
369
|
+
if (!result?.ok || handoffError) {
|
|
370
|
+
const outputError = !r.ok ? `Agent 執行失敗:${r.summary}` : !result ? "未產生有效裁決" : result.ok ? "未產生有效裁決" : result.error;
|
|
371
|
+
const reason = handoffError ?? outputError;
|
|
372
|
+
info(run, ` ⏸️ ${arbiter} 未產生有效裁決:${reason}`);
|
|
373
|
+
return { ...run, stage: "paused", pausedStage: "plan_review", pauseReason: `${arbiter} 仲裁未產生有效裁決:${reason}` };
|
|
316
374
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
375
|
+
mkdirSync(join(runDir(run.id), "reviews"), { recursive: true });
|
|
376
|
+
renameSync(flowFile(run, "plan-arbiter.json"), join(runDir(run.id), "reviews", `plan-arbiter-${arbitrationRound}-${arbiter}.json`));
|
|
377
|
+
const verdict = result.data.verdict;
|
|
378
|
+
const notes = result.data.items.map((i) => `- ${i.criterion}:${i.note}`);
|
|
379
|
+
info(run, ` ${verdict === "approve" ? "✓" : "✗"} ${arbiter}:${verdict === "approve" ? "可以執行" : "不可執行"}`);
|
|
320
380
|
verdicts.push({ arbiter, verdict, notes });
|
|
321
381
|
}
|
|
322
382
|
rmSync(flowFile(run, "dispute.md"), { force: true });
|
|
323
383
|
const approvals = verdicts.filter((v) => v.verdict === "approve").length;
|
|
324
384
|
const unanimous = approvals === verdicts.length;
|
|
325
|
-
const decision = arbitrationDecision(verdicts.map((v) => v.verdict), cfg.tieBreak
|
|
385
|
+
const decision = arbitrationDecision(verdicts.map((v) => v.verdict), cfg.tieBreak);
|
|
386
|
+
if (decision === "invalid")
|
|
387
|
+
throw new Error("仲裁結果缺少有效裁決");
|
|
326
388
|
const summary = unanimous
|
|
327
389
|
? `${mode}一致核准`
|
|
328
390
|
: approvals === 0
|
|
@@ -363,7 +425,8 @@ async function implementStage(run) {
|
|
|
363
425
|
const key = `${task.id}:tests`;
|
|
364
426
|
info(run, `🧪 [${progress}] 撰寫測試(${agents.tests})`);
|
|
365
427
|
const before = await headCommit(repo);
|
|
366
|
-
const
|
|
428
|
+
const outcome = await agentStep(run, agents.tests, `${task.id}-tests`, renderPrompt("implement-tests", { task: taskJson, testPattern: cfg.testPattern, testCmd }), { kind: "write", reset: () => resetTo(repo, before) });
|
|
429
|
+
const { r, agent: testsAuthor } = outcome;
|
|
367
430
|
if (!r.ok) {
|
|
368
431
|
await resetTo(repo, before);
|
|
369
432
|
return retry(run, key, `Agent 執行失敗:${r.summary}`, "implement");
|
|
@@ -381,6 +444,11 @@ async function implementStage(run) {
|
|
|
381
444
|
await resetTo(repo, before);
|
|
382
445
|
return retry(run, key, "測試在功能尚未實作前就全部通過,代表測試沒有驗證到新行為。請撰寫會因功能尚未實作而失敗的測試。", "implement");
|
|
383
446
|
}
|
|
447
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
448
|
+
if (handoffError) {
|
|
449
|
+
await resetTo(repo, before);
|
|
450
|
+
return retry(run, key, handoffError, "implement");
|
|
451
|
+
}
|
|
384
452
|
writeFileSync(flowFile(run, "red-output.txt"), red.output);
|
|
385
453
|
info(run, `🔴 [${progress}] 測試如預期失敗`);
|
|
386
454
|
return { ...succeed(run, key, "implement"), taskPhase: "code", testsCommit: commit, lastTestsAuthor: testsAuthor };
|
|
@@ -392,7 +460,8 @@ async function implementStage(run) {
|
|
|
392
460
|
throw new Error("缺少 testsCommit,狀態不一致");
|
|
393
461
|
info(run, `🛠️ [${progress}] 實作(${agents.code},測試由 ${run.lastTestsAuthor ?? agents.tests} 撰寫)`);
|
|
394
462
|
const redOutput = existsSync(flowFile(run, "red-output.txt")) ? readFileSync(flowFile(run, "red-output.txt"), "utf8") : "";
|
|
395
|
-
const
|
|
463
|
+
const outcome = await agentStep(run, agents.code, `${task.id}-code`, renderPrompt("implement-code", { task: taskJson, testCmd, redOutput: tail(redOutput, 3000) }), { kind: "write", reset: () => resetTo(repo, testsCommit) });
|
|
464
|
+
const { r, agent: codeAuthor } = outcome;
|
|
396
465
|
if (!r.ok)
|
|
397
466
|
return retry(run, key, `Agent 執行失敗:${r.summary}`, "implement");
|
|
398
467
|
await commitAll(repo, `feat(${task.id}): ${task.title} [${codeAuthor}]`);
|
|
@@ -406,6 +475,11 @@ async function implementStage(run) {
|
|
|
406
475
|
info(run, ` ✗ 測試仍未通過${logHint(run, green.seq)}`);
|
|
407
476
|
return retry(run, key, `測試仍未通過:\n\n\`\`\`\n${tail(green.output)}\n\`\`\``, "implement");
|
|
408
477
|
}
|
|
478
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
479
|
+
if (handoffError) {
|
|
480
|
+
await resetTo(repo, testsCommit);
|
|
481
|
+
return retry(run, key, handoffError, "implement");
|
|
482
|
+
}
|
|
409
483
|
info(run, `🟢 [${progress}] 完成`);
|
|
410
484
|
return {
|
|
411
485
|
...succeed(run, key, "implement"),
|
|
@@ -454,10 +528,11 @@ async function fixStage(run) {
|
|
|
454
528
|
const repo = worktreeDir(run.id);
|
|
455
529
|
const feedback = readFeedback(run);
|
|
456
530
|
const before = await headCommit(repo);
|
|
457
|
-
const
|
|
531
|
+
const outcome = await agentStep(run, agent, "fix", renderPrompt("fix", { testPattern: cfg.testPattern }), {
|
|
458
532
|
kind: "write",
|
|
459
533
|
reset: () => resetTo(repo, before),
|
|
460
534
|
});
|
|
535
|
+
const { r, agent: actual } = outcome;
|
|
461
536
|
if (!r.ok)
|
|
462
537
|
return retry(run, "fix", `${feedback}\n\n(上次修正時 Agent 執行失敗:${r.summary})`, "fix");
|
|
463
538
|
await commitAll(repo, `fix: ${why} [${actual}]`);
|
|
@@ -467,6 +542,11 @@ async function fixStage(run) {
|
|
|
467
542
|
await resetTo(repo, before);
|
|
468
543
|
return retry(run, "fix", `${feedback}\n\n另外:不可刪除測試檔來讓檢查通過,已還原:${deleted.join(", ")}`, "fix");
|
|
469
544
|
}
|
|
545
|
+
const handoffError = finishHandoff(run, outcome, "writer");
|
|
546
|
+
if (handoffError) {
|
|
547
|
+
await resetTo(repo, before);
|
|
548
|
+
return retry(run, "fix", handoffError, "fix");
|
|
549
|
+
}
|
|
470
550
|
// 修正者成為新的作者,下一輪審查會換成別人
|
|
471
551
|
return { ...to(run, "verify"), lastWriter: actual };
|
|
472
552
|
}
|
|
@@ -479,18 +559,22 @@ async function reviewStage(run) {
|
|
|
479
559
|
.map((s) => s.slice(1, -1));
|
|
480
560
|
const issues = [];
|
|
481
561
|
let firstObjector;
|
|
482
|
-
for (const reviewer of panel) {
|
|
562
|
+
for (const [slot, reviewer] of panel.entries()) {
|
|
483
563
|
info(run, `👀 程式碼審查(${reviewer})`);
|
|
484
564
|
rmSync(flowFile(run, "review.json"), { force: true });
|
|
485
|
-
const
|
|
486
|
-
kind: "review",
|
|
565
|
+
const outcome = await agentStep(run, reviewer, "review", renderPrompt("review", { reviewer, authors: authors.join("、") || "未知" }), {
|
|
566
|
+
kind: "review", slot,
|
|
487
567
|
});
|
|
568
|
+
const { r } = outcome;
|
|
488
569
|
await discardChanges(repo); // 審查者不可改程式碼
|
|
489
570
|
if (!r.ok)
|
|
490
571
|
return retry(run, "review-run", `Agent 執行失敗:${r.summary}`, "review");
|
|
491
572
|
const review = readJsonFile(flowFile(run, "review.json"), ReviewResult);
|
|
492
573
|
if (!review.ok)
|
|
493
574
|
return retry(run, "review-run", review.error, "review");
|
|
575
|
+
const handoffError = finishHandoff(run, outcome, "reviewer", { target: "code", verdict: review.data.verdict });
|
|
576
|
+
if (handoffError)
|
|
577
|
+
return retry(run, "review-run", handoffError, "review");
|
|
494
578
|
renameSync(flowFile(run, "review.json"), flowFile(run, `review-${reviewer}.json`));
|
|
495
579
|
if (review.data.verdict === "approve") {
|
|
496
580
|
info(run, ` ✓ ${reviewer} 核准`);
|
|
@@ -513,6 +597,9 @@ async function reviewStage(run) {
|
|
|
513
597
|
};
|
|
514
598
|
}
|
|
515
599
|
async function prStage(run) {
|
|
600
|
+
const pending = openActions(readHandoff(run.id));
|
|
601
|
+
if (pending.length)
|
|
602
|
+
return { ...run, stage: "failed", failedStage: "pr", failureReason: `仍有未結交接事項:${pending.map((item) => item.id).join("、")}` };
|
|
516
603
|
const repo = worktreeDir(run.id);
|
|
517
604
|
const remotes = (await git(repo, "remote")).split("\n").filter(Boolean);
|
|
518
605
|
if (!remotes.includes("origin")) {
|
|
@@ -546,6 +633,7 @@ const STAGES = {
|
|
|
546
633
|
/** 一路推進到需要人介入(awaiting_approval、paused)或結束(done / failed)為止;每一步都寫回檔案,中斷後可接續 */
|
|
547
634
|
export async function advance(initial) {
|
|
548
635
|
let run = initial;
|
|
636
|
+
recoverHandoff(run.id);
|
|
549
637
|
for (;;) {
|
|
550
638
|
if (["done", "failed", "awaiting_approval", "paused"].includes(run.stage))
|
|
551
639
|
return run;
|
package/dist/handoff.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { flowDir, handoffPath, runDir } from "./paths.js";
|
|
6
|
+
import { HandoffLedger, HandoffResponse, HandoffSource } from "./schemas.js";
|
|
7
|
+
import { readJsonFile } from "./util.js";
|
|
8
|
+
const responsePath = (id) => join(flowDir(id), "handoff-response.json");
|
|
9
|
+
const receiptsDir = (id) => join(runDir(id), "handoff-receipts");
|
|
10
|
+
const keyHash = (key) => createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
11
|
+
const receiptPath = (id, key) => join(receiptsDir(id), `${keyHash(key)}.json`);
|
|
12
|
+
const Receipt = z.object({ callKey: z.string(), source: HandoffSource, response: HandoffResponse, role: z.enum(["writer", "reviewer"]) });
|
|
13
|
+
function writeAtomic(path, content) {
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
15
|
+
const tmp = `${path}.tmp`;
|
|
16
|
+
writeFileSync(tmp, content);
|
|
17
|
+
renameSync(tmp, path);
|
|
18
|
+
}
|
|
19
|
+
export function readHandoff(id) {
|
|
20
|
+
const path = handoffPath(id);
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return { version: 1, issues: [] };
|
|
23
|
+
return HandoffLedger.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
24
|
+
}
|
|
25
|
+
export function openActions(ledger, target) {
|
|
26
|
+
return ledger.issues.filter((item) => item.kind === "action"
|
|
27
|
+
&& (item.status === "open" || item.status === "proposed_resolved")
|
|
28
|
+
&& (!target || item.targetStage === target));
|
|
29
|
+
}
|
|
30
|
+
export function reviewHandoffGate(ledger, target, verdict) {
|
|
31
|
+
if (verdict !== "approve")
|
|
32
|
+
return undefined;
|
|
33
|
+
const pending = openActions(ledger, target);
|
|
34
|
+
return pending.length ? `審查核准與未結交接事項矛盾:${pending.map((item) => item.id).join("、")}` : undefined;
|
|
35
|
+
}
|
|
36
|
+
export function previewHandoff(ledger, callKey, source, response, role) {
|
|
37
|
+
source = HandoffSource.parse(source);
|
|
38
|
+
response = HandoffResponse.parse(response);
|
|
39
|
+
if (source.callKey !== callKey)
|
|
40
|
+
throw new Error("交接呼叫識別碼不一致");
|
|
41
|
+
if (ledger.appliedCalls?.includes(callKey))
|
|
42
|
+
return ledger;
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
const issues = ledger.issues.map((item) => ({ ...item }));
|
|
45
|
+
for (const [index, item] of response.newIssues.entries()) {
|
|
46
|
+
const id = createHash("sha256").update(`${callKey}:${index}`).digest("hex").slice(0, 16);
|
|
47
|
+
if (issues.some((existing) => existing.id === id))
|
|
48
|
+
throw new Error(`交接事項 id 重複:${id}`);
|
|
49
|
+
issues.push({ id, source, ...item, status: "open", updatedAt: now });
|
|
50
|
+
}
|
|
51
|
+
for (const disposition of response.dispositions) {
|
|
52
|
+
const issue = issues.find((item) => item.id === disposition.id);
|
|
53
|
+
if (!issue)
|
|
54
|
+
throw new Error(`找不到交接事項:${disposition.id}`);
|
|
55
|
+
if (issue.kind !== "action")
|
|
56
|
+
throw new Error(`參考資訊不可結案:${disposition.id}`);
|
|
57
|
+
if (issue.status === "resolved" || issue.status === "accepted")
|
|
58
|
+
throw new Error(`交接事項已結案:${disposition.id}`);
|
|
59
|
+
if (role === "writer" && disposition.status !== "proposed_resolved")
|
|
60
|
+
throw new Error("作者只能提出已修正,不能自行結案");
|
|
61
|
+
if (role === "reviewer" && disposition.status === "proposed_resolved")
|
|
62
|
+
throw new Error("審查者須明確結案或接受風險");
|
|
63
|
+
issue.status = disposition.status;
|
|
64
|
+
issue.resolution = { agent: source.agent, reason: disposition.reason, evidence: disposition.evidence };
|
|
65
|
+
issue.updatedAt = now;
|
|
66
|
+
}
|
|
67
|
+
return HandoffLedger.parse({ version: 1, issues, appliedCalls: [...(ledger.appliedCalls ?? []), callKey] });
|
|
68
|
+
}
|
|
69
|
+
export function mergeHandoff(id, callKey, source, response, role) {
|
|
70
|
+
const next = previewHandoff(readHandoff(id), callKey, source, response, role);
|
|
71
|
+
writeAtomic(handoffPath(id), JSON.stringify(next, null, 2));
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
/** 只把目前步驟需要處理的事項投影給 agent。 */
|
|
75
|
+
export function prepareHandoff(id, _callKey, target, blind) {
|
|
76
|
+
const items = readHandoff(id).issues.filter((item) => item.targetStage === target && (item.kind === "info" || item.status === "open" || item.status === "proposed_resolved"));
|
|
77
|
+
const lines = items.map((item) => {
|
|
78
|
+
const source = blind ? "" : `\n來源:${item.source.stage}/${item.source.agent}`;
|
|
79
|
+
const resolution = item.resolution ? `\n處理理由:${item.resolution.reason}\n處理證據:${item.resolution.evidence}` : "";
|
|
80
|
+
return `## ${item.id}:${item.summary}\n證據:${item.evidence}\n狀態:${item.status}${resolution}${source}`;
|
|
81
|
+
});
|
|
82
|
+
mkdirSync(flowDir(id), { recursive: true });
|
|
83
|
+
writeFileSync(join(flowDir(id), "handoff-context.md"), `# 待處理交接事項\n\n${lines.length ? lines.join("\n\n") : "目前沒有待處理事項。"}\n`);
|
|
84
|
+
rmSync(responsePath(id), { force: true });
|
|
85
|
+
}
|
|
86
|
+
export function validateHandoffResponse(id) {
|
|
87
|
+
return readJsonFile(responsePath(id), HandoffResponse);
|
|
88
|
+
}
|
|
89
|
+
/** 已通過原有關卡的回覆先記收據,再合併;中斷後可重播。 */
|
|
90
|
+
export function acceptHandoff(id, callKey, source, response, role) {
|
|
91
|
+
const checked = Receipt.parse({ callKey, source, response, role });
|
|
92
|
+
previewHandoff(readHandoff(id), callKey, source, response, role);
|
|
93
|
+
const path = receiptPath(id, callKey);
|
|
94
|
+
writeAtomic(path, JSON.stringify(checked));
|
|
95
|
+
mergeHandoff(id, callKey, source, response, role);
|
|
96
|
+
rmSync(path, { force: true });
|
|
97
|
+
}
|
|
98
|
+
export function recoverHandoff(id) {
|
|
99
|
+
const dir = receiptsDir(id);
|
|
100
|
+
if (!existsSync(dir))
|
|
101
|
+
return;
|
|
102
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith(".json"))) {
|
|
103
|
+
const path = join(dir, name);
|
|
104
|
+
const receipt = Receipt.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
105
|
+
mergeHandoff(id, receipt.callKey, receipt.source, receipt.response, receipt.role);
|
|
106
|
+
rmSync(path);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=handoff.js.map
|
package/dist/paths.js
CHANGED
|
@@ -17,6 +17,7 @@ export const agentflowctlDir = () => join(projectRoot(), ".agentflowctl");
|
|
|
17
17
|
export const runsDir = () => join(agentflowctlDir(), "runs");
|
|
18
18
|
export const runDir = (id) => join(runsDir(), id);
|
|
19
19
|
export const logDir = (id) => join(runDir(id), "logs");
|
|
20
|
+
export const handoffPath = (id) => join(runDir(id), "handoff.json");
|
|
20
21
|
/** 每個 run 一個 git worktree,Agent 只在這裡工作,不碰你正在編輯的檔案 */
|
|
21
22
|
export const worktreesDir = () => join(agentflowctlDir(), "worktrees");
|
|
22
23
|
export const worktreeDir = (id) => join(worktreesDir(), id);
|
package/dist/schemas.js
CHANGED
|
@@ -14,6 +14,50 @@ export const Stage = z.enum([
|
|
|
14
14
|
"done",
|
|
15
15
|
"failed",
|
|
16
16
|
]);
|
|
17
|
+
export const HandoffSource = z.object({
|
|
18
|
+
stage: Stage,
|
|
19
|
+
step: z.string().min(1),
|
|
20
|
+
agent: z.string().min(1),
|
|
21
|
+
callKey: z.string().min(1),
|
|
22
|
+
});
|
|
23
|
+
const HandoffKind = z.enum(["action", "info"]);
|
|
24
|
+
const HandoffTarget = z.enum(["plan", "code"]);
|
|
25
|
+
const HandoffStatus = z.enum(["open", "proposed_resolved", "resolved", "accepted"]);
|
|
26
|
+
export const HandoffResponse = z.object({
|
|
27
|
+
newIssues: z.array(z.object({
|
|
28
|
+
kind: HandoffKind,
|
|
29
|
+
summary: z.string().trim().min(1),
|
|
30
|
+
evidence: z.string().trim().min(1),
|
|
31
|
+
targetStage: HandoffTarget,
|
|
32
|
+
})),
|
|
33
|
+
dispositions: z.array(z.object({
|
|
34
|
+
id: z.string().min(1),
|
|
35
|
+
status: z.enum(["proposed_resolved", "resolved", "accepted"]),
|
|
36
|
+
reason: z.string().trim().min(1),
|
|
37
|
+
evidence: z.string().trim().min(1),
|
|
38
|
+
})),
|
|
39
|
+
});
|
|
40
|
+
export const HandoffIssue = z.object({
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
source: HandoffSource,
|
|
43
|
+
kind: HandoffKind,
|
|
44
|
+
summary: z.string().min(1),
|
|
45
|
+
evidence: z.string().min(1),
|
|
46
|
+
targetStage: HandoffTarget,
|
|
47
|
+
status: HandoffStatus,
|
|
48
|
+
resolution: z.object({
|
|
49
|
+
agent: z.string(),
|
|
50
|
+
reason: z.string(),
|
|
51
|
+
evidence: z.string(),
|
|
52
|
+
}).optional(),
|
|
53
|
+
updatedAt: z.string(),
|
|
54
|
+
});
|
|
55
|
+
export const HandoffLedger = z.object({
|
|
56
|
+
version: z.literal(1),
|
|
57
|
+
issues: z.array(HandoffIssue),
|
|
58
|
+
/** 用來辨識沒有新增事項的回覆是否已經合併。 */
|
|
59
|
+
appliedCalls: z.array(z.string()).optional(),
|
|
60
|
+
});
|
|
17
61
|
/** Agent 在 spec 階段產出的 .flow/acceptance.json */
|
|
18
62
|
export const AcceptanceList = z
|
|
19
63
|
.array(z.object({
|
|
@@ -39,6 +83,11 @@ export const ReviewResult = z.object({
|
|
|
39
83
|
note: z.string().default(""),
|
|
40
84
|
})),
|
|
41
85
|
});
|
|
86
|
+
/** 仲裁者可能用 reject 表示否決;讀取時正規化,保留相同的理由欄位。 */
|
|
87
|
+
export const ArbiterResult = ReviewResult.extend({
|
|
88
|
+
verdict: z.enum(["approve", "changes_requested", "reject"])
|
|
89
|
+
.transform((verdict) => verdict === "reject" ? "changes_requested" : verdict),
|
|
90
|
+
});
|
|
42
91
|
/** 一個 agent 的定義;名稱(agents 的 key)用在 cycle 裡 */
|
|
43
92
|
export const AgentDef = z.object({
|
|
44
93
|
adapter: z.enum(["claude", "codex", "gemini", "command"]),
|
package/package.json
CHANGED
package/prompts/fix.md
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<inputs>
|
|
10
21
|
- .flow/feedback.md:失敗的檢查(型別、lint、測試、建置)或審查意見
|
|
11
22
|
- .flow/spec.md 與 .flow/acceptance.json:規格
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<task>
|
|
10
21
|
```json
|
|
11
22
|
{{task}}
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<task>
|
|
10
21
|
```json
|
|
11
22
|
{{task}}
|
package/prompts/plan-arbiter.md
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<requirement>
|
|
10
21
|
{{requirement}}
|
|
11
22
|
</requirement>
|
|
@@ -36,10 +47,11 @@
|
|
|
36
47
|
```
|
|
37
48
|
|
|
38
49
|
dispute.md 裡的每一條意見都要列一筆並說明你的判斷。
|
|
50
|
+
`verdict` 只能寫 `approve` 或 `changes_requested`。若計畫有會導致錯誤結果、遺漏需求或無法驗收的問題,請寫 `changes_requested`,不要寫 `reject`。
|
|
39
51
|
</output_format>
|
|
40
52
|
|
|
41
53
|
<constraints>
|
|
42
|
-
- 只能寫入 .flow/plan-arbiter.json,不可修改規格、計畫或任何程式碼。
|
|
54
|
+
- 只能寫入 .flow/plan-arbiter.json 與 .flow/handoff-response.json,不可修改規格、計畫或任何程式碼。
|
|
43
55
|
</constraints>
|
|
44
56
|
|
|
45
57
|
<reply_format>
|
package/prompts/plan-fix.md
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<requirement>
|
|
10
21
|
{{requirement}}
|
|
11
22
|
</requirement>
|
package/prompts/plan-review.md
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<requirement>
|
|
10
21
|
{{requirement}}
|
|
11
22
|
</requirement>
|
|
@@ -46,7 +57,7 @@
|
|
|
46
57
|
</output_format>
|
|
47
58
|
|
|
48
59
|
<constraints>
|
|
49
|
-
- 只能寫入 .flow/plan-review.json,不可修改規格、計畫或任何程式碼,其他變更都會被還原。
|
|
60
|
+
- 只能寫入 .flow/plan-review.json 與 .flow/handoff-response.json,不可修改規格、計畫或任何程式碼,其他變更都會被還原。
|
|
50
61
|
</constraints>
|
|
51
62
|
|
|
52
63
|
<reply_format>
|
package/prompts/plan.md
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<inputs>
|
|
10
21
|
- .flow/spec.md
|
|
11
22
|
- .flow/acceptance.json
|
package/prompts/review.md
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<inputs>
|
|
10
21
|
- 規格:.flow/spec.md
|
|
11
22
|
- 驗收條件:.flow/acceptance.json
|
|
@@ -39,7 +50,7 @@
|
|
|
39
50
|
</output_format>
|
|
40
51
|
|
|
41
52
|
<constraints>
|
|
42
|
-
- 只能寫入 .flow/review.json,不可修改任何程式碼,其他變更都會被捨棄。
|
|
53
|
+
- 只能寫入 .flow/review.json 與 .flow/handoff-response.json,不可修改任何程式碼,其他變更都會被捨棄。
|
|
43
54
|
</constraints>
|
|
44
55
|
|
|
45
56
|
<reply_format>
|
package/prompts/spec.md
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。
|
|
7
7
|
</context>
|
|
8
8
|
|
|
9
|
+
<handoff>
|
|
10
|
+
先閱讀 .flow/handoff-context.md,處理與本階段有關的待辦事項。完成時寫入 .flow/handoff-response.json;即使沒有事項也必須寫出空陣列:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "newIssues": [], "dispositions": [] }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
9
20
|
<requirement>
|
|
10
21
|
{{requirement}}
|
|
11
22
|
</requirement>
|