agentflowctl 0.7.0 → 0.8.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
@@ -103,7 +103,7 @@ agentflowctl status f-xxxx # 階段、上一步結果、未結交接事
103
103
  agentflowctl list
104
104
  agentflowctl logs f-xxxx # 列出每一份 log 的編號、結果、階段、步驟、agent
105
105
  agentflowctl logs f-xxxx 7 # 解析第 7 份 log,最後附上錯誤整理(--latest 看最新一份)
106
- agentflowctl logs f-xxxx 7 --full # 完整顯示多行指令與絕對路徑
106
+ agentflowctl logs f-xxxx 7 --full # 逐條顯示 shell 指令,完整顯示多行內容與絕對路徑
107
107
  agentflowctl logs f-xxxx 7 --raw # 原始內容(agent 的 JSON 行)
108
108
  agentflowctl resume f-xxxx # 從暫停、Ctrl-C 或失敗處接續
109
109
  agentflowctl cancel f-xxxx
@@ -183,14 +183,14 @@ agentflowctl clean --all # 清掉所有已結束的 run 與中斷留
183
183
  | 標記 | 內容 |
184
184
  |---|---|
185
185
  | 💬 | agent 的完整文字,不截斷 |
186
- | 🔧 | 工具呼叫。只顯示第一行,多行時註明共幾行;去掉 `/bin/zsh -lc '…'` 這類 shell 包裝,worktree 內的絕對路徑改成相對路徑 |
186
+ | 🔧 | 工具呼叫。連續的 shell 指令(多半是讀檔、搜尋)收成一行 `🔧 shell 指令 ×N`;其他工具只顯示第一行,多行時註明共幾行,worktree 內的絕對路徑改成相對路徑 |
187
187
  | 📊 | token 用量 |
188
188
  | 🏁 | 最後結果。與最後一則 💬 相同時不再重印 |
189
189
  | ⚠️ | 工具回報的錯誤。agent 通常會自己換方法繼續,所以不列進錯誤整理 |
190
190
  | ❌ | adapter 不認得的錯誤事件 |
191
191
  | 📄 | 不是 JSON 的輸出行 |
192
192
 
193
- 要看完整的工具內容與重複的最後結果,加 `--full`。adapter 不認得、也看不出錯誤跡象的 JSON 行不會顯示,只列出行數,要看全部請加 `--raw`。專案指令的 log 本來就是純文字,會原樣顯示。
193
+ 要逐條看 shell 指令、完整的工具內容與重複的最後結果,加 `--full`。adapter 不認得、也看不出錯誤跡象的 JSON 行不會顯示,只列出行數,要看全部請加 `--raw`。專案指令的 log 本來就是純文字,會原樣顯示。
194
194
 
195
195
  最後一段「錯誤」整理出結束碼、agent 回報的失敗、錯誤事件與 stderr:
196
196
 
@@ -200,7 +200,7 @@ agentflowctl clean --all # 清掉所有已結束的 run 與中斷留
200
200
  檔案 /repo/.agentflowctl/runs/f-xxxx/logs/003-plan-plan-codex.log
201
201
 
202
202
  💬 先讀 spec.md 與 acceptance.json
203
- 🔧 shell: cat .flow/spec.md
203
+ 🔧 shell 指令 ×1(--full 查看)
204
204
  🏁 失敗:stream disconnected before completion
205
205
 
206
206
  ── 錯誤 ──
@@ -323,9 +323,9 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
323
323
 
324
324
  人工確認計畫是為了擋住方向錯了還一路做下去。預設用三層機制取代它;加上 `--manual-plan` 時,三層都過了仍會停下來等你。
325
325
 
326
- **格式與覆蓋率。** 每次撰寫或修改計畫之後,都要重新通過 zod、任務相依、無循環、每條驗收條件都有任務負責。沒過就還原。
326
+ **格式與覆蓋率。** 每次撰寫或修改計畫之後,都要重新通過 zod、任務相依、無循環、每條驗收條件都有任務負責,而且每個任務最多對應兩條驗收條件(一次只做一件事,最多兩件)。沒過就還原。
327
327
 
328
- **跨模型審查。** 審查看需求覆蓋、驗收條件能不能測、任務大小與技術方向。審查者只能寫意見。若改了規格或計畫,檔案會被還原。修改者要在 `plan.md` 的「審查回應」逐條回覆;不同意要寫理由。
328
+ **跨模型審查。** 審查看需求覆蓋、驗收條件能不能測且一條只寫一個行為、任務是否只做一件事(最多兩件)與技術方向。審查者只能寫意見。若改了規格或計畫,檔案會被還原。修改者要在 `plan.md` 的「審查回應」逐條回覆;不同意要寫理由。
329
329
 
330
330
  **僵持時仲裁。** 兩種情況會觸發:這輪審查意見和上一輪一樣,或已達重試上限。仲裁者只判斷一件事:照這份計畫實作,能不能滿足需求。
331
331
 
@@ -346,7 +346,7 @@ agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯
346
346
  | 階段 | 負責的 agent | 程式認定通過的條件 | 失敗時 |
347
347
  |---|---|---|---|
348
348
  | spec | 隨機一位 | 檔案存在、zod 驗證、id 不重複 | 重試 |
349
- | plan | 與 spec 同一位 | zod、相依存在、無循環、每條驗收條件都有任務 | 重試 |
349
+ | plan | 與 spec 同一位 | zod、相依存在、無循環、每條驗收條件都有任務、每個任務最多兩條驗收條件 | 重試 |
350
350
  | plan_review | 計畫作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve` | 進入 plan_fix |
351
351
  | plan_fix | 依 `fixStrategy` | 修改後仍通過 plan 的格式與 DAG 檢查 | 還原並重試 |
352
352
  | 仲裁 | 見上一節 | 一致核准;分歧依 `tieBreak` | 兩家都不核准時進入 plan_fix 再審查;第三方不核准或 `tieBreak: stop` 時失敗 |
package/dist/cli.js CHANGED
@@ -369,7 +369,7 @@ program
369
369
  .command("logs <id> [seq]")
370
370
  .description("列出 log;指定編號(或 --latest)時顯示解析後的內容,最後附上錯誤整理")
371
371
  .option("--latest", "顯示最新一份 log", false)
372
- .option("--full", "完整顯示工具內容(多行指令、絕對路徑)與重複的最後回覆", false)
372
+ .option("--full", "逐條顯示 shell 指令,完整顯示工具內容(多行指令、絕對路徑)與重複的最後回覆", false)
373
373
  .option("--raw", "顯示原始內容(agent 的 JSON 行)", false)
374
374
  .action((id, seq, opts) => {
375
375
  mustGetRun(id);
package/dist/engine.js CHANGED
@@ -35,10 +35,13 @@ const exhausted = new Set();
35
35
  function handoffTarget(run) {
36
36
  return ["spec", "plan", "plan_review", "plan_fix"].includes(run.stage) ? "plan" : "code";
37
37
  }
38
- /** 同一輪重跑使用相同 key;重試次數或 panel 位置改變時使用新 key。 */
38
+ /**
39
+ * 每次執行 agent 都用新的 key。重試次數會在後面的輪次重複出現,只靠它組 key 會撞到先前已套用的呼叫,
40
+ * 讓這次的處置被當成重播而略過,所以加上這個 run 已執行 agent 的次數。
41
+ */
39
42
  function handoffKey(run, step, slot, agent) {
40
43
  const attempts = Object.entries(run.attempts).sort(([a], [b]) => a.localeCompare(b));
41
- return JSON.stringify([run.id, run.stage, step, run.taskIndex, run.taskPhase, attempts, slot, agent]);
44
+ return JSON.stringify([run.id, run.stage, step, run.taskIndex, run.taskPhase, attempts, slot, agent, agentRuns(run.id)]);
42
45
  }
43
46
  async function agentStep(run, planned, step, prompt, mode) {
44
47
  const cfg = loadRepoConfig();
package/dist/logs.js CHANGED
@@ -99,25 +99,15 @@ function findError(ev) {
99
99
  }
100
100
  /** worktree 絕對路徑改成相對路徑:`<worktree>/x` → `x`,單獨的 `<worktree>` → `.` */
101
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
102
  /** 精簡顯示的工具內容:只留第一行,並註明原本有幾行 */
116
103
  function compactDetail(detail) {
117
- const lines = relativeToWorktree(unwrapShell(detail.trim())).split("\n");
104
+ const lines = relativeToWorktree(detail.trim()).split("\n");
118
105
  const first = clip(lines[0], 200);
119
106
  return lines.length > 1 ? `${first} …(共 ${lines.length} 行)` : first;
120
107
  }
108
+ /** 各家 CLI 執行 shell 指令的工具名稱:codex 的 shell、claude 的 Bash、gemini 的 run_shell_command */
109
+ const SHELL_TOOLS = new Set(["shell", "bash", "run_shell_command"]);
110
+ const isShellTool = (ev) => ev.kind === "tool" && SHELL_TOOLS.has(ev.name.toLowerCase());
121
111
  function renderEvent(ev, full, lastText) {
122
112
  switch (ev.kind) {
123
113
  case "text":
@@ -151,12 +141,25 @@ function analyzeLog(text, full) {
151
141
  }
152
142
  else {
153
143
  let skipped = 0;
144
+ // 精簡模式下連續的 shell 指令(多半是讀檔、搜尋)收成一行,只留數量
145
+ let shells = 0;
146
+ const flushShells = () => {
147
+ if (shells)
148
+ lines.push(`🔧 shell 指令 ×${shells}(--full 查看)`);
149
+ shells = 0;
150
+ };
154
151
  for (const line of body) {
155
152
  if (!line.trim())
156
153
  continue;
157
154
  const events = adapter.parse(line);
158
155
  if (events.length) {
159
156
  for (const ev of events) {
157
+ if (!full && isShellTool(ev)) {
158
+ shells++;
159
+ continue;
160
+ }
161
+ if (ev.kind !== "usage")
162
+ flushShells();
160
163
  const s = renderEvent(ev, full, lastText);
161
164
  if (s)
162
165
  lines.push(s);
@@ -171,6 +174,7 @@ function analyzeLog(text, full) {
171
174
  }
172
175
  const json = tryJson(line);
173
176
  if (!json) {
177
+ flushShells();
174
178
  lines.push(`📄 ${line}`);
175
179
  continue;
176
180
  }
@@ -179,10 +183,12 @@ function analyzeLog(text, full) {
179
183
  skipped++;
180
184
  continue;
181
185
  }
186
+ flushShells();
182
187
  lines.push(`${err.fatal ? "❌" : "⚠️ "} ${indent(err.message)}`);
183
188
  if (err.fatal)
184
189
  errors.push(`錯誤事件:${indent(err.message)}`);
185
190
  }
191
+ flushShells();
186
192
  if (skipped)
187
193
  lines.push(`(另有 ${skipped} 行其他事件未顯示,可用 --raw 查看)`);
188
194
  }
package/dist/tasks.js CHANGED
@@ -1,3 +1,5 @@
1
+ /** 一個任務最多做兩件事:對應的驗收條件超過這個數量就要再拆 */
2
+ export const MAX_TASK_ACCEPTANCE = 2;
1
3
  /**
2
4
  * 檢查任務清單並依相依關係排序(Kahn 演算法)。
3
5
  * 回傳排序後的任務,或回傳一段可以直接回饋給 Agent 的錯誤說明。
@@ -12,6 +14,9 @@ export function orderTasks(tasks, acceptanceIds) {
12
14
  }
13
15
  const covered = new Set();
14
16
  for (const t of tasks) {
17
+ if (t.acceptance.length > MAX_TASK_ACCEPTANCE) {
18
+ errors.push(`${t.id} 對應 ${t.acceptance.length} 條驗收條件,一個任務最多 ${MAX_TASK_ACCEPTANCE} 條,請拆成更小的任務`);
19
+ }
15
20
  for (const d of t.dependsOn)
16
21
  if (!ids.has(d))
17
22
  errors.push(`${t.id} 相依的 ${d} 不存在`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentflowctl",
3
3
  "license": "MIT",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "description": "跨廠商 AI 開發 harness:Claude Code、Codex、Gemini 輪流實作、審查、修正",
6
6
  "keywords": [
7
7
  "ai",
@@ -31,6 +31,7 @@
31
31
  <output_format>
32
32
  - acceptance.json 與 tasks.json 的格式必須維持不變(見檔案內現有內容)。
33
33
  - 每一條驗收條件都至少要有一個任務負責;`dependsOn` 不可有循環。
34
+ - 一個任務只做一件事,最多兩件:`acceptance` 最多列兩條驗收條件;驗收條件一條只描述一個行為。修改時若任務變大,請拆開,不要合併。
34
35
  - 測試檔名必須符合正規表示式 `{{testPattern}}`。
35
36
  </output_format>
36
37
 
@@ -32,8 +32,8 @@
32
32
 
33
33
  <review_focus>
34
34
  1. **需求覆蓋**:規格是否完整涵蓋原始需求?有沒有遺漏、誤解,或加入需求沒要求的範圍?
35
- 2. **驗收條件**:每一條是否具體、可以用自動化測試驗證?有沒有重要的邊界情況或錯誤處理沒被列入?
36
- 3. **任務拆解**:每個任務是否小到一次 TDD 循環就能完成,而且能寫出「實作前會失敗」的測試?相依順序是否合理?
35
+ 2. **驗收條件**:每一條是否具體、可以用自動化測試驗證,而且只描述一個行為?把多個行為寫在同一條的,要求拆開。有沒有重要的邊界情況或錯誤處理沒被列入?
36
+ 3. **任務拆解**:每個任務是否只做一件事(最多兩件),小到一次 TDD 循環就能完成,而且能寫出「實作前會失敗」的測試?任務太大、一次要動很多檔案或驗證很多行為的,要求拆成更小的任務。相依順序是否合理?
37
37
  4. **技術方向**:是否符合專案既有的架構與慣例?有沒有更簡單的做法,或明顯的風險?
38
38
 
39
39
  措辭、格式這類不影響實作結果的小問題,不需要要求修改。
package/prompts/plan.md CHANGED
@@ -46,7 +46,10 @@
46
46
  </output_format>
47
47
 
48
48
  <guidelines>
49
- - 每個任務是一個可獨立測試的垂直切片,小到一次 TDD 循環就能完成。
49
+ - 一個任務只做一件事,最多兩件:`acceptance` 最多列兩條驗收條件,超過就拆成多個任務(程式會檢查,超過會被退回)。
50
+ - 每個任務是一個可獨立測試的垂直切片,小到一次 TDD 循環就能完成;只動少數幾個檔案,測試只驗證一兩個行為。
51
+ - `title` 用一句話說出這件事;需要用「並且」「以及」串起來的,就是兩個任務。
52
+ - `description` 寫清楚要動哪些檔案、測試要驗證哪個行為,以及這個任務不做什麼。
50
53
  - 每個任務都必須能寫出「在實作前會失敗」的測試;純設定或重構類工作請併入相關任務。
51
54
  - 測試檔名必須符合正規表示式 `{{testPattern}}`。
52
55
  - 每一條驗收條件都至少要有一個任務負責;`dependsOn` 不可有循環。
package/prompts/spec.md CHANGED
@@ -28,6 +28,13 @@
28
28
  4. 撰寫 .flow/acceptance.json,每一條驗收條件都必須能用自動化測試驗證。
29
29
  </steps>
30
30
 
31
+ <guidelines>
32
+ - 驗收條件要寫細:一條只描述一個可觀察的行為(一個輸入或情境,對應一個預期結果)。
33
+ - 描述裡出現「並且」「同時」「以及」,或同時涵蓋成功與失敗路徑時,拆成多條。
34
+ - 邊界情況與錯誤處理各自獨立成一條,不要附在正常路徑那一條裡。
35
+ - 寧可多幾條小的,也不要少數幾條大的;後面每個任務最多只能對應兩條驗收條件。
36
+ </guidelines>
37
+
31
38
  <output_format>
32
39
  .flow/acceptance.json 的格式:
33
40