agentflowctl 0.5.0 → 0.7.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.
@@ -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/logs.js CHANGED
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { ADAPTERS } from "./agents/index.js";
4
4
  import { str, tryJson } from "./agents/types.js";
5
+ import { parseResultMeta } from "./resultMeta.js";
5
6
  /**
6
7
  * 每次執行 agent 或專案指令都寫成一份 log:
7
8
  * - 檔名:<三位數序號>-<階段>-<步驟>-<agent>.log,專案指令的 agent 欄位是 cmd
@@ -96,35 +97,57 @@ function findError(ev) {
96
97
  const hit = walk(ev, 0);
97
98
  return hit ? { message: clip(pick(hit)), fatal: false } : undefined;
98
99
  }
99
- function renderEvent(ev) {
100
+ /** worktree 絕對路徑改成相對路徑:`<worktree>/x` → `x`,單獨的 `<worktree>` → `.` */
101
+ const relativeToWorktree = (s) => s.replace(/[^\s'"=]*\/\.agentflowctl\/worktrees\/[^/\s'"]+(\/)?/g, (_m, slash) => (slash ? "" : "."));
102
+ /** 去掉 codex 這類 CLI 包在指令外面的 `/bin/zsh -lc '...'` */
103
+ function unwrapShell(cmd) {
104
+ const m = cmd.match(/^\/bin\/(?:ba|z)?sh\s+-l?c\s+(['"])([\s\S]*)\1$/);
105
+ if (!m)
106
+ return cmd;
107
+ const [, quote, inner] = m;
108
+ // 單引號包裝裡的 '"'"' 或 '\'' 是被跳脫的單引號,內容太複雜時保留原樣
109
+ if (quote === "'" && inner.includes("'"))
110
+ return cmd;
111
+ if (quote === '"' && /(^|[^\\])"/.test(inner))
112
+ return cmd;
113
+ return inner;
114
+ }
115
+ /** 精簡顯示的工具內容:只留第一行,並註明原本有幾行 */
116
+ function compactDetail(detail) {
117
+ const lines = relativeToWorktree(unwrapShell(detail.trim())).split("\n");
118
+ const first = clip(lines[0], 200);
119
+ return lines.length > 1 ? `${first} …(共 ${lines.length} 行)` : first;
120
+ }
121
+ function renderEvent(ev, full, lastText) {
100
122
  switch (ev.kind) {
101
123
  case "text":
102
124
  return `💬 ${indent(ev.text.trim())}`;
103
- case "tool":
104
- return `🔧 ${ev.name}${ev.detail ? `: ${indent(ev.detail.trim())}` : ""}`;
125
+ case "tool": {
126
+ const detail = ev.detail?.trim();
127
+ if (!detail)
128
+ return `🔧 ${ev.name}`;
129
+ return `🔧 ${ev.name}: ${full ? indent(detail) : compactDetail(detail)}`;
130
+ }
105
131
  case "usage":
106
132
  return `📊 用量 input ${ev.inputTokens ?? "?"} / output ${ev.outputTokens ?? "?"} tokens`;
107
- case "done":
108
- return `🏁 ${ev.ok ? "完成" : "失敗"}${ev.summary ? `:${indent(ev.summary.trim())}` : ""}`;
133
+ case "done": {
134
+ const summary = ev.summary?.trim();
135
+ // 最後一則回覆通常就是 summary,精簡模式不再重印一次
136
+ const show = summary && (full || summary !== lastText);
137
+ return `🏁 ${ev.ok ? "完成" : "失敗"}${show ? `:${indent(summary)}` : ""}`;
138
+ }
109
139
  }
110
140
  }
111
- /** 把一份 log 轉成人看得懂的版本;最後附上錯誤整理,讓失敗原因一眼可見 */
112
- export function renderLog(text, file = "") {
141
+ function analyzeLog(text, full) {
113
142
  const { header, footer, body, stderr } = parseLog(text);
114
- const out = [];
143
+ const lines = [];
115
144
  const errors = [];
116
- const title = header
117
- ? `${header.stage} / ${header.step} / ${header.agent}${header.adapter && header.adapter !== header.agent ? `(adapter ${header.adapter})` : ""}`
118
- : "(沒有檔頭)";
119
- out.push(`${logSeq(file) ? `#${logSeq(file)} ` : ""}${title}`);
120
- out.push(`開始 ${localTime(header?.startedAt)} ` +
121
- (footer ? `結束 ${localTime(footer.endedAt)} 結束碼 ${footer.code} ${footer.ok ? "✓ 成功" : "✗ 失敗"}` : "… 沒有結束紀錄(還在執行或被中斷)"));
122
- if (file)
123
- out.push(`檔案 ${file}`);
124
- out.push("");
145
+ let lastText;
146
+ let doneSummary;
125
147
  const adapter = header?.adapter ? ADAPTERS[header.adapter] : undefined;
126
148
  if (!adapter || header?.agent === CMD_AGENT) {
127
- out.push(...body);
149
+ lines.push(...body);
150
+ lastText = body.slice(-15).join("\n").trim() || undefined;
128
151
  }
129
152
  else {
130
153
  let skipped = 0;
@@ -134,9 +157,13 @@ export function renderLog(text, file = "") {
134
157
  const events = adapter.parse(line);
135
158
  if (events.length) {
136
159
  for (const ev of events) {
137
- const s = renderEvent(ev);
160
+ const s = renderEvent(ev, full, lastText);
138
161
  if (s)
139
- out.push(s);
162
+ lines.push(s);
163
+ if (ev.kind === "text")
164
+ lastText = ev.text.trim();
165
+ if (ev.kind === "done")
166
+ doneSummary = ev.summary?.trim() || undefined;
140
167
  if (ev.kind === "done" && !ev.ok)
141
168
  errors.push(`agent 回報失敗${ev.summary ? `:${indent(ev.summary.trim())}` : ""}`);
142
169
  }
@@ -144,7 +171,7 @@ export function renderLog(text, file = "") {
144
171
  }
145
172
  const json = tryJson(line);
146
173
  if (!json) {
147
- out.push(`📄 ${line}`);
174
+ lines.push(`📄 ${line}`);
148
175
  continue;
149
176
  }
150
177
  const err = findError(json);
@@ -152,22 +179,51 @@ export function renderLog(text, file = "") {
152
179
  skipped++;
153
180
  continue;
154
181
  }
155
- out.push(`${err.fatal ? "❌" : "⚠️ "} ${indent(err.message)}`);
182
+ lines.push(`${err.fatal ? "❌" : "⚠️ "} ${indent(err.message)}`);
156
183
  if (err.fatal)
157
184
  errors.push(`錯誤事件:${indent(err.message)}`);
158
185
  }
159
186
  if (skipped)
160
- out.push(`(另有 ${skipped} 行其他事件未顯示,可用 --raw 查看)`);
187
+ lines.push(`(另有 ${skipped} 行其他事件未顯示,可用 --raw 查看)`);
161
188
  }
162
189
  if (footer && footer.code !== 0)
163
190
  errors.unshift(`結束碼 ${footer.code}`);
164
191
  if (footer && !footer.ok && !errors.length)
165
192
  errors.push("執行未通過(沒有更多錯誤訊息)");
166
193
  const stderrText = stderr.length ? `stderr:\n ${indent(clip(stderr.join("\n"), 4000))}` : undefined;
194
+ return { header, footer, lines, errors, stderrText, lastText, doneSummary };
195
+ }
196
+ const logTitle = (header) => header
197
+ ? `${header.stage} / ${header.step} / ${header.agent}${header.adapter && header.adapter !== header.agent ? `(adapter ${header.adapter})` : ""}`
198
+ : "(沒有檔頭)";
199
+ /** 把一份 log 轉成人看得懂的版本;最後附上錯誤整理,讓失敗原因一眼可見。預設精簡工具內容,opts.full 時完整顯示 */
200
+ export function renderLog(text, file = "", opts = {}) {
201
+ const { header, footer, lines, errors, stderrText } = analyzeLog(text, opts.full ?? false);
202
+ const out = [];
203
+ out.push(`${logSeq(file) ? `#${logSeq(file)} ` : ""}${logTitle(header)}`);
204
+ out.push(`開始 ${localTime(header?.startedAt)} ` +
205
+ (footer ? `結束 ${localTime(footer.endedAt)} 結束碼 ${footer.code} ${footer.ok ? "✓ 成功" : "✗ 失敗"}` : "… 沒有結束紀錄(還在執行或被中斷)"));
206
+ if (file)
207
+ out.push(`檔案 ${file}`);
208
+ out.push("", ...lines);
167
209
  if (errors.length)
168
210
  out.push("", "── 錯誤 ──", ...errors, ...(stderrText ? [stderrText] : []));
169
211
  else if (stderrText)
170
212
  out.push("", "── 其他輸出 ──", stderrText);
171
213
  return out.join("\n");
172
214
  }
215
+ /** 一份 log 的結果摘要:給 run 停下時直接印在終端機,不用再另外查 log */
216
+ export function summarizeLog(text) {
217
+ const a = analyzeLog(text, false);
218
+ const reply = a.doneSummary ?? a.lastText;
219
+ const meta = reply ? parseResultMeta(reply) ?? (a.lastText ? parseResultMeta(a.lastText) : undefined) : undefined;
220
+ const failed = a.footer ? !a.footer.ok : false;
221
+ return {
222
+ title: logTitle(a.header),
223
+ ok: a.footer?.ok,
224
+ meta,
225
+ text: meta ? undefined : reply && clip(reply, 600),
226
+ errors: failed ? [...a.errors, ...(a.stderrText ? [a.stderrText] : [])] : [],
227
+ };
228
+ }
173
229
  //# sourceMappingURL=logs.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);
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ /** Agent 回覆結尾的 <result> 中繼資料(格式定義在各 prompt 的 <reply_format>) */
3
+ export const ResultMeta = z.object({
4
+ status: z.enum(["done", "blocked"]),
5
+ summary: z.string().default(""),
6
+ filesChanged: z.array(z.string()).default([]),
7
+ concerns: z.string().default(""),
8
+ });
9
+ const tagText = (xml, tag) => xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim();
10
+ /** 取回覆中最後一個 <result> 區塊;沒有或格式不合時回傳 undefined,不影響關卡判斷 */
11
+ export function parseResultMeta(text) {
12
+ const block = [...text.matchAll(/<result>([\s\S]*?)<\/result>/g)].at(-1)?.[1];
13
+ if (block === undefined)
14
+ return undefined;
15
+ const files = tagText(block, "files_changed") ?? "";
16
+ const parsed = ResultMeta.safeParse({
17
+ status: tagText(block, "status"),
18
+ summary: tagText(block, "summary"),
19
+ filesChanged: [...files.matchAll(/<file>([\s\S]*?)<\/file>/g)].map((m) => m[1].trim()).filter(Boolean),
20
+ concerns: tagText(block, "concerns"),
21
+ });
22
+ return parsed.success ? parsed.data : undefined;
23
+ }
24
+ //# sourceMappingURL=resultMeta.js.map
package/dist/runner.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { z } from "zod";
4
3
  import { ADAPTERS } from "./agents/index.js";
5
4
  import { projectRoot, runDir } from "./paths.js";
6
5
  import { config } from "./config.js";
7
6
  import { CMD_AGENT, footerLine, headerLine, logSeq, STDERR_MARK } from "./logs.js";
8
7
  import { exec, execShell } from "./proc.js";
9
8
  import { tail } from "./util.js";
9
+ import { parseResultMeta } from "./resultMeta.js";
10
10
  /**
11
11
  * 判斷 agent 失敗是否因為方案額度或速率限制。
12
12
  * 各家的錯誤訊息會隨版本變動,這裡用寬鬆的樣式比對,且只在執行失敗時才檢查,避免誤判正常輸出。
@@ -22,28 +22,7 @@ const QUOTA_PATTERNS = [
22
22
  /\b429\b/,
23
23
  ];
24
24
  export const isQuotaError = (text) => QUOTA_PATTERNS.some((p) => p.test(text));
25
- /** Agent 回覆結尾的 <result> 中繼資料(格式定義在各 prompt 的 <reply_format>) */
26
- export const ResultMeta = z.object({
27
- status: z.enum(["done", "blocked"]),
28
- summary: z.string().default(""),
29
- filesChanged: z.array(z.string()).default([]),
30
- concerns: z.string().default(""),
31
- });
32
- const tagText = (xml, tag) => xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim();
33
- /** 取回覆中最後一個 <result> 區塊;沒有或格式不合時回傳 undefined,不影響關卡判斷 */
34
- export function parseResultMeta(text) {
35
- const block = [...text.matchAll(/<result>([\s\S]*?)<\/result>/g)].at(-1)?.[1];
36
- if (block === undefined)
37
- return undefined;
38
- const files = tagText(block, "files_changed") ?? "";
39
- const parsed = ResultMeta.safeParse({
40
- status: tagText(block, "status"),
41
- summary: tagText(block, "summary"),
42
- filesChanged: [...files.matchAll(/<file>([\s\S]*?)<\/file>/g)].map((m) => m[1].trim()).filter(Boolean),
43
- concerns: tagText(block, "concerns"),
44
- });
45
- return parsed.success ? parsed.data : undefined;
46
- }
25
+ export { parseResultMeta, ResultMeta } from "./resultMeta.js";
47
26
  function appendLog(file, text) {
48
27
  mkdirSync(dirname(file), { recursive: true });
49
28
  appendFileSync(file, text.endsWith("\n") ? text : `${text}\n`);
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"]),
@@ -0,0 +1,79 @@
1
+ import { join } from "node:path";
2
+ import { summarizeLog } from "./logs.js";
3
+ const pad = (s) => s.split("\n").join("\n ");
4
+ const clip = (s, max) => (s.length <= max ? s : `${s.slice(0, max)}…`);
5
+ /** 一份 log 的結果:標題、<result> 的摘要與疑慮;失敗時附上錯誤整理 */
6
+ function outcomeLines(entry, text) {
7
+ const s = summarizeLog(text);
8
+ const mark = s.ok === undefined ? "…" : s.ok ? "✓" : "✗";
9
+ const out = [` #${entry.seq} ${s.title} ${mark}`];
10
+ if (s.meta) {
11
+ if (s.meta.status === "blocked")
12
+ out.push(" 狀態:blocked");
13
+ if (s.meta.summary)
14
+ out.push(` 摘要:${pad(s.meta.summary)}`);
15
+ if (s.meta.concerns)
16
+ out.push(` 疑慮:${pad(s.meta.concerns)}`);
17
+ }
18
+ else if (s.text) {
19
+ out.push(` ${pad(s.text)}`);
20
+ }
21
+ for (const e of s.errors)
22
+ out.push(` ${pad(clip(e, 1500))}`);
23
+ return out;
24
+ }
25
+ /**
26
+ * run 停下來(Ctrl-C、失敗、暫停、等待核准)時要直接印在終端機的內容:
27
+ * 最近一步的結果、未結交接事項,以及接下來可以執行的指令。完成的 run 不印。
28
+ */
29
+ export function stopReport(i) {
30
+ const { run, logs } = i;
31
+ if (run.stage === "done")
32
+ return [];
33
+ const out = [];
34
+ const last = logs.at(-1);
35
+ const running = last && !last.footer ? last : undefined;
36
+ const finished = logs.filter((e) => e.footer);
37
+ const lastFailed = finished.filter((e) => !e.footer.ok).at(-1);
38
+ // 失敗時優先看失敗的那一步,其餘情況看最後完成的一步
39
+ const shown = run.stage === "failed" && lastFailed ? lastFailed : finished.at(-1);
40
+ if (running || shown) {
41
+ out.push("", "── 結果 ──");
42
+ if (running) {
43
+ const title = summarizeLog(i.read(running.file)).title;
44
+ out.push(` ${i.interrupted ? "中斷於" : "進行中"} #${running.seq} ${title}(沒有結束紀錄,resume 時會重跑這一步)`);
45
+ }
46
+ if (shown)
47
+ out.push(...outcomeLines(shown, i.read(shown.file)));
48
+ }
49
+ if (i.open.length) {
50
+ out.push("", "── 未結交接事項 ──");
51
+ for (const item of i.open)
52
+ out.push(` [${item.targetStage}] ${item.id} ${item.summary}(${item.status})`);
53
+ }
54
+ const cmd = (c, why) => ` ${c.padEnd(40)} ${why}`;
55
+ const actions = [];
56
+ if (run.stage === "failed") {
57
+ if (lastFailed)
58
+ actions.push(cmd(`agentflowctl logs ${run.id} ${lastFailed.seq}`, "看失敗步驟的完整 log"));
59
+ actions.push(cmd(`cd ${i.worktree}`, "需要時手動修正"));
60
+ actions.push(cmd(`agentflowctl resume ${run.id}`, `從 ${run.failedStage ?? "失敗的階段"} 重試`));
61
+ actions.push(cmd(`agentflowctl cancel ${run.id}`, "放棄這個 run"));
62
+ }
63
+ else if (run.stage === "paused") {
64
+ actions.push(cmd(`agentflowctl resume ${run.id}`, "額度恢復後接續"));
65
+ }
66
+ else if (run.stage === "awaiting_approval") {
67
+ actions.push(cmd(`less ${join(i.worktree, ".flow", "plan.md")}`, "檢視計畫"));
68
+ actions.push(cmd(`agentflowctl approve ${run.id}`, "核准並開始實作"));
69
+ }
70
+ else {
71
+ if (shown)
72
+ actions.push(cmd(`agentflowctl logs ${run.id} ${shown.seq}`, "看上一步的完整 log"));
73
+ actions.push(cmd(`agentflowctl resume ${run.id}`, `從 ${run.stage} 接續`));
74
+ actions.push(cmd(`agentflowctl cancel ${run.id}`, "放棄這個 run"));
75
+ }
76
+ out.push("", "── 下一步 ──", ...actions);
77
+ return out;
78
+ }
79
+ //# sourceMappingURL=stopReport.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentflowctl",
3
3
  "license": "MIT",
4
- "version": "0.5.0",
4
+ "version": "0.7.0",
5
5
  "description": "跨廠商 AI 開發 harness:Claude Code、Codex、Gemini 輪流實作、審查、修正",
6
6
  "keywords": [
7
7
  "ai",
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}}
@@ -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>
@@ -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>
@@ -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