agentflowctl 0.9.0 → 0.11.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 +100 -401
- package/dist/cli.js +3 -1
- package/dist/engine.js +143 -36
- package/dist/handoff.js +14 -8
- package/dist/roles.js +23 -17
- package/dist/schemas.js +4 -1
- package/package.json +1 -1
- package/prompts/fix.md +1 -1
- package/prompts/implement-code.md +1 -1
- package/prompts/implement-tests.md +1 -1
- package/prompts/plan-arbiter.md +1 -1
- package/prompts/plan-fix.md +1 -1
- package/prompts/plan-review.md +1 -1
- package/prompts/plan.md +1 -1
- package/prompts/review.md +1 -1
- package/prompts/spec.md +1 -1
- package/prompts/task-review.md +77 -0
package/README.md
CHANGED
|
@@ -1,465 +1,164 @@
|
|
|
1
1
|
# agentflowctl
|
|
2
2
|
|
|
3
|
-
讓 Claude Code、Codex、Gemini CLI
|
|
3
|
+
讓 Claude Code、Codex、Gemini CLI 等 agent 在同一個專案裡分工:整理需求、規劃、寫測試與程式、交叉審查,最後建立 PR。agentflowctl 負責推進流程,並用檔案、測試和檢查結果決定能否進到下一步。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
需求 → spec → plan ⇄ plan_review ⇄ plan_fix →(僵持時)仲裁 → implement(測試 A → 實作 B)→ verify ⇄ fix → review ⇄ fix → pr
|
|
7
|
-
```
|
|
5
|
+
每次執行都會建立獨立的 git worktree 與 `flow/<id>` 分支,不會直接修改你目前的工作目錄。兩個 agent 就能運作;若只有一個,也能執行,但無法做到跨 agent 審查。
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
## 開始使用
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
需要 **Node.js 22 以上**、git,以及至少一個已安裝且完成登入的 agent CLI。建議先準備兩個,例如 Claude Code 與 Codex。
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
claude # 完成登入
|
|
17
|
-
codex # 完成登入
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
沒有內建的 agent,只會使用 `flow.config.json` 的 `agents` 裡設定的。用 `agent setup` 互動設定:它會先列出已設定的 agent 與 CLI 是否可以執行,再偵測本機裝了哪些支援的 CLI(目前是 `claude`、`codex`、`gemini`),只針對已安裝的逐一詢問要不要加入、名稱與 model,再設定參與的 agent;沒偵測到的只列出、不詢問。確認後才一次寫入,最後自動跑一次 `doctor`:
|
|
11
|
+
在要開發的專案根目錄執行:
|
|
21
12
|
|
|
22
13
|
```bash
|
|
23
14
|
npx agentflowctl agent setup
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
也可以不經互動,直接用指令新增:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npx agentflowctl agent add claude --adapter claude
|
|
30
|
-
npx agentflowctl agent add codex --adapter codex
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
之後改了設定或換了環境,用 `doctor` 檢查。它會列出設定的 agent 的 CLI 是否已安裝,並印出即將參與的 agent。沒有設定 `cycle` 時,依 `agents` 的順序取已安裝的;一個都沒偵測到就無法執行。
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
15
|
npx agentflowctl doctor
|
|
16
|
+
npx agentflowctl run --req "登入表單加入驗證與錯誤訊息"
|
|
37
17
|
```
|
|
38
18
|
|
|
39
|
-
|
|
19
|
+
`agent setup` 會找出本機可用的 Claude Code、Codex、Gemini CLI,讓你選擇要加入哪些 agent,並寫入專案根目錄的 `flow.config.json`。`doctor` 會檢查設定與 CLI 是否可執行。agentflowctl 沒有預設 agent,因此第一次使用要先完成設定。
|
|
40
20
|
|
|
41
|
-
|
|
21
|
+
若要使用現成的需求文件,改用:
|
|
42
22
|
|
|
43
23
|
```bash
|
|
44
|
-
npx agentflowctl run --req
|
|
24
|
+
npx agentflowctl run --req-file ./requirement.md
|
|
45
25
|
```
|
|
46
26
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
npm install -g agentflowctl
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
下面的指令都寫成 `agentflowctl`;沒有全域安裝時,在前面加上 `npx` 即可。
|
|
54
|
-
|
|
55
|
-
這次 run 在專用 git worktree(`.agentflowctl/worktrees/<id>`)裡工作,基底是你目前的分支,新分支名是 `flow/<id>`。你正在編輯的工作目錄不會被改到。
|
|
56
|
-
|
|
57
|
-
從原始碼安裝:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
git clone https://github.com/gogogohuang/agentflowctl.git
|
|
61
|
-
cd agentflowctl
|
|
62
|
-
pnpm install
|
|
63
|
-
pnpm run build
|
|
64
|
-
pnpm link --global
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## 角色分配怎麼打破同溫層
|
|
68
|
-
|
|
69
|
-
同一個模型審查自己的程式碼,容易放過自己的寫法;測試和實作出自同一個模型,也容易寫出剛好會過的測試。角色分配只有三條規則,定義在 `src/roles.ts`:
|
|
70
|
-
|
|
71
|
-
| 規則 | 效果 |
|
|
72
|
-
|---|---|
|
|
73
|
-
| 審查者永遠不是最後寫程式的 agent,多位審查者彼此不重複 | 每一輪審查都由另一家看 |
|
|
74
|
-
| 同一個任務的測試與實作由不同 agent 負責(`tddSplit`) | A 寫的測試,B 實作到通過,而且不能改測試 |
|
|
75
|
-
| 人選隨機決定,不依 `cycle` 的順序 | 各家輪流當作者、審查者、修正者,不會固定由同一家起頭 |
|
|
76
|
-
|
|
77
|
-
`cycle` 只決定哪些 agent 參與。隨機以 run id 為種子,同一個 run 的同一步驟 `resume` 後仍是同一家。寫測試的人每 N 個任務(N 為參與的家數)洗一次牌,每家各輪一次,換輪時也不會連續兩個任務由同一家寫測試。
|
|
27
|
+
下文以 `agentflowctl` 為例;未全域安裝時,在指令前加 `npx`。想全域安裝可執行 `npm install -g agentflowctl`。
|
|
78
28
|
|
|
79
|
-
|
|
29
|
+
## 執行時會發生什麼
|
|
80
30
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
審查:codex(最後作者是 claude)→ 要求修改 → claude 修正 → codex 審查(核准)
|
|
86
|
-
```
|
|
31
|
+
1. agent 整理需求與驗收條件,接著寫計畫,交給其他 agent 審查。
|
|
32
|
+
2. 依計畫逐個任務寫出會失敗的測試,再由另一位 agent 實作到測試通過;每個任務都會經過審查與驗證。
|
|
33
|
+
3. 全部任務完成後,再執行專案檢查與整體程式碼審查。未通過的項目會交回修正。
|
|
34
|
+
4. 有 `origin` 時會推送分支;若 `gh` 可用,會嘗試建立 PR。沒有 `origin` 時,完成的分支留在本機。
|
|
87
35
|
|
|
88
|
-
|
|
36
|
+
流程預設會自動往下走。想在計畫通過審查後親自確認,可加 `--manual-plan`;確認後執行 `agentflowctl approve <id>`。
|
|
89
37
|
|
|
90
|
-
|
|
38
|
+
agentflowctl 會依專案的 `packageManager`、lockfile 與 `package.json` scripts 選擇安裝、測試及檢查指令。第一次執行時,請留意終端機印出的偵測結果;需要調整可在 `flow.config.json` 指定 `install`、`test` 或 `checks`。
|
|
91
39
|
|
|
92
|
-
|
|
40
|
+
## 查看進度
|
|
93
41
|
|
|
94
|
-
|
|
42
|
+
`run` 開始時會印出 run id,例如 `f-xxxx`。執行中預設只顯示階段進度;加 `-v` 可看到 agent 文字、工具呼叫與專案指令。
|
|
95
43
|
|
|
96
44
|
```bash
|
|
97
|
-
agentflowctl
|
|
98
|
-
agentflowctl
|
|
99
|
-
agentflowctl
|
|
100
|
-
|
|
101
|
-
agentflowctl
|
|
102
|
-
agentflowctl status f-xxxx # 階段、上一步結果、未結交接事項、下一步指令、任務進度、各 agent 用量、代打紀錄
|
|
103
|
-
agentflowctl list
|
|
104
|
-
agentflowctl logs f-xxxx # 列出每一份 log 的編號、結果、階段、步驟、agent
|
|
105
|
-
agentflowctl logs f-xxxx 7 # 解析第 7 份 log,最後附上錯誤整理(--latest 看最新一份)
|
|
106
|
-
agentflowctl logs f-xxxx 7 --full # 逐條顯示 shell 指令,完整顯示多行內容與絕對路徑
|
|
107
|
-
agentflowctl logs f-xxxx 7 --raw # 原始內容(agent 的 JSON 行)
|
|
108
|
-
agentflowctl resume f-xxxx # 從暫停、Ctrl-C 或失敗處接續
|
|
109
|
-
agentflowctl cancel f-xxxx
|
|
110
|
-
agentflowctl clean f-xxxx # 移除 worktree 與 run 紀錄,分支保留
|
|
111
|
-
agentflowctl clean --all # 清掉所有已結束的 run 與中斷留下的 worktree
|
|
45
|
+
agentflowctl list # 列出 run
|
|
46
|
+
agentflowctl status f-xxxx # 看進度、結果與下一步
|
|
47
|
+
agentflowctl logs f-xxxx # 列出各步驟的 log
|
|
48
|
+
agentflowctl logs f-xxxx --latest # 看最新一份 log
|
|
49
|
+
agentflowctl resume f-xxxx # 從暫停、中斷或失敗處接續
|
|
112
50
|
```
|
|
113
51
|
|
|
114
|
-
|
|
115
|
-
|---|---|
|
|
116
|
-
| `--req` / `--req-file` | 需求文字,或從檔案讀取 |
|
|
117
|
-
| `--base` | 基底分支,預設為目前分支 |
|
|
118
|
-
| `--cycle` | 這次 run 參與的 agent,例如 `claude,codex,gemini`;順序不影響分工;建立後就固定,`resume` 沿用 |
|
|
119
|
-
| `--max-agent-runs` | 這次 run 的 agent 執行次數上限 |
|
|
120
|
-
| `--manual-plan` | 計畫通過審查後進入 `awaiting_approval`,等 `approve` 才開始實作 |
|
|
121
|
-
| `-v` / `--verbose` | 執行時印出 agent 的文字、工具呼叫與專案指令;`run`、`resume`、`approve` 都適用,也可設 `AGENTFLOWCTL_VERBOSE=1` |
|
|
122
|
-
|
|
123
|
-
`status` 會列出任務。進行中的任務會標出正在寫測試還是正在寫實作。
|
|
124
|
-
|
|
125
|
-
### 清除 worktree
|
|
126
|
-
|
|
127
|
-
`run` 建好 worktree 就會寫入 run 紀錄,所以不論在哪一步中斷(包括安裝相依套件時),都能用 `resume` 接續,或用 `clean` 清掉。
|
|
128
|
-
|
|
129
|
-
- `clean <id>`:移除該 run 的 worktree 與 `.agentflowctl/runs/<id>/`,並清掉 git 裡已失效的 worktree 登記。沒有 run 紀錄的 worktree 也能清,worktree 資料夾被手動刪掉時也一樣。
|
|
130
|
-
- `clean --all`:清掉所有 `done`、`failed` 的 run,以及沒有 run 紀錄的 worktree。進行中、`paused`、`awaiting_approval` 的不動;Ctrl-C 中斷、之後不打算接續的 run,先 `cancel` 再 `clean --all`,或直接 `clean <id>`。
|
|
131
|
-
|
|
132
|
-
兩者都保留 `flow/<id>` 分支,不需要時用 `git branch -D` 刪除。
|
|
52
|
+
`status` 會列出目前階段、未結的交接事項與下一步指令;失敗或暫停時也會顯示原因。要看某一步的詳細輸出,可用 `logs <id> <編號>`;加 `--full` 看完整工具內容,或加 `--raw` 看原始輸出。
|
|
133
53
|
|
|
134
|
-
|
|
54
|
+
執行紀錄在 `.agentflowctl/runs/<id>/`,工作分支在 `.agentflowctl/worktrees/<id>/`。不再需要某次 run 時,可用 `agentflowctl clean <id>` 清除 worktree 與紀錄;`flow/<id>` 分支會保留。
|
|
135
55
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
[f-xxxx] 🔍 執行驗證
|
|
140
|
-
[f-xxxx] ✓ typecheck
|
|
141
|
-
[f-xxxx] ✗ lint(agentflowctl logs f-xxxx 15)
|
|
142
|
-
✗ codex 執行失敗(結束碼 1),可用 agentflowctl logs f-xxxx 16 查看
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
加上 `-v` 會另外印出 agent 每一段文字的第一行、每次工具呼叫的完整指令或主要參數(不截斷,多行指令的後續行縮排對齊),以及 install、測試、verify 這些以 `$ ` 開頭的專案指令:
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
💬 [claude] 先讀現有的表單元件
|
|
149
|
-
🔧 [claude] Read: /repo/.agentflowctl/worktrees/f-xxxx/src/LoginForm.tsx
|
|
150
|
-
🔧 [claude] Bash: pnpm vitest run src/LoginForm.test.tsx
|
|
151
|
-
🔧 [codex] shell: bash -lc 'pnpm test'
|
|
152
|
-
🔧 [gemini] run_shell_command: npm run lint
|
|
153
|
-
$ pnpm install
|
|
154
|
-
```
|
|
56
|
+
## 執行停下來時怎麼做
|
|
155
57
|
|
|
156
|
-
|
|
58
|
+
先執行 `agentflowctl status <id>`,看「階段」與「原因」,再依情況處理:
|
|
157
59
|
|
|
158
|
-
|
|
60
|
+
| 狀況 | 下一步 |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| 按 Ctrl-C,或終端機意外關閉 | 執行 `agentflowctl resume <id>`;沒有結束紀錄的步驟會重跑 |
|
|
63
|
+
| `awaiting_approval`:計畫等你確認 | 閱讀 `.agentflowctl/worktrees/<id>/.flow/plan.md`,確認後執行 `agentflowctl approve <id>` |
|
|
64
|
+
| `paused`:agent 額度用完 | 等額度恢復後執行 `agentflowctl resume <id>`;審查步驟不會換 agent 代審 |
|
|
65
|
+
| `paused`:仲裁沒有產生有效裁決 | 依 `status` 的原因查看 log;若有 `.flow/plan-arbiter.json`,也檢查其內容,處理後執行 `agentflowctl resume <id>` |
|
|
66
|
+
| `failed`:測試、檢查、審查或 agent 執行失敗 | 依 `status` 提示查看失敗的 log,處理原因後執行 `agentflowctl resume <id>`;失敗階段會重試 |
|
|
67
|
+
| `failed`:已達 agent 執行次數上限 | 用 `agentflowctl resume <id> --max-agent-runs 100` 調高上限後接續,數字須大於已執行次數 |
|
|
159
68
|
|
|
160
|
-
|
|
69
|
+
例如失敗時,可照終端機列出的 log 編號查看原因:
|
|
161
70
|
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
008-implement-T1-red-cmd.log
|
|
167
|
-
015-verify-lint-cmd.log
|
|
71
|
+
```bash
|
|
72
|
+
agentflowctl status f-xxxx
|
|
73
|
+
agentflowctl logs f-xxxx 7
|
|
74
|
+
agentflowctl resume f-xxxx
|
|
168
75
|
```
|
|
169
76
|
|
|
170
|
-
|
|
77
|
+
若不打算接續,先用 `agentflowctl cancel <id>` 標記放棄,再用 `agentflowctl clean <id>` 清除 worktree 與執行紀錄。仍在執行中的 run,先在原終端機按 Ctrl-C。`clean` 會保留 `flow/<id>` 分支。
|
|
171
78
|
|
|
172
|
-
|
|
79
|
+
## 參數怎麼設定
|
|
173
80
|
|
|
174
|
-
|
|
175
|
-
# 結果 階段 步驟 agent 開始時間
|
|
176
|
-
1 ✓ setup install cmd 2026-09-26 11:29:04
|
|
177
|
-
2 ✓ spec spec claude 2026-09-26 11:29:05
|
|
178
|
-
3 ✗ plan plan codex 2026-09-26 11:31:40
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
`agentflowctl logs <id> <編號>` 會把原始 JSON 解析成易讀的格式:
|
|
81
|
+
設定分成三處:**這次執行的選項**寫在 `run` 或 `resume` 後面;**專案設定**寫在專案根目錄的 `flow.config.json`;**執行環境設定**用環境變數。先用 `agent setup` 建立 agent 設定,再視需要調整其他欄位。
|
|
182
82
|
|
|
183
|
-
|
|
184
|
-
|---|---|
|
|
185
|
-
| 💬 | agent 的完整文字,不截斷 |
|
|
186
|
-
| 🔧 | 工具呼叫。連續的 shell 指令(多半是讀檔、搜尋)收成一行 `🔧 shell 指令 ×N`;其他工具只顯示第一行,多行時註明共幾行,worktree 內的絕對路徑改成相對路徑 |
|
|
187
|
-
| 📊 | token 用量 |
|
|
188
|
-
| 🏁 | 最後結果。與最後一則 💬 相同時不再重印 |
|
|
189
|
-
| ⚠️ | 工具回報的錯誤。agent 通常會自己換方法繼續,所以不列進錯誤整理 |
|
|
190
|
-
| ❌ | adapter 不認得的錯誤事件 |
|
|
191
|
-
| 📄 | 不是 JSON 的輸出行 |
|
|
83
|
+
### 指令選項
|
|
192
84
|
|
|
193
|
-
|
|
85
|
+
| 指令或選項 | 怎麼設定 |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `run --req "..."` / `--req-file <檔案>` | 二選一,直接輸入需求或讀取檔案 |
|
|
88
|
+
| `run --manual-plan` | 計畫通過審查後等待你確認,再用 `approve <id>` 繼續 |
|
|
89
|
+
| `run --cycle <名單>` | 指定這次參與的 agent,例如 `--cycle claude,codex`;優先於設定檔的 `cycle` |
|
|
90
|
+
| `run --base <分支>` | 指定起始分支;未設定時使用目前分支 |
|
|
91
|
+
| `run --max-agent-runs <次數>` | 覆蓋這次的 `maxAgentRuns`;上限不夠時可用 `resume <id> --max-agent-runs <次數>` 調高 |
|
|
92
|
+
| `-v` / `--verbose` | 執行時顯示 agent 文字、工具呼叫與專案指令,適用於 `run`、`resume`、`approve` |
|
|
194
93
|
|
|
195
|
-
|
|
94
|
+
例如:
|
|
196
95
|
|
|
96
|
+
```bash
|
|
97
|
+
agentflowctl run --req-file ./requirement.md --cycle claude,codex --max-agent-runs 80 --manual-plan
|
|
98
|
+
agentflowctl resume f-xxxx --max-agent-runs 100
|
|
197
99
|
```
|
|
198
|
-
#3 plan / plan / codex
|
|
199
|
-
開始 2026-09-26 11:31:40 結束 2026-09-26 11:31:52 結束碼 1 ✗ 失敗
|
|
200
|
-
檔案 /repo/.agentflowctl/runs/f-xxxx/logs/003-plan-plan-codex.log
|
|
201
|
-
|
|
202
|
-
💬 先讀 spec.md 與 acceptance.json
|
|
203
|
-
🔧 shell 指令 ×1(--full 查看)
|
|
204
|
-
🏁 失敗:stream disconnected before completion
|
|
205
|
-
|
|
206
|
-
── 錯誤 ──
|
|
207
|
-
結束碼 1
|
|
208
|
-
agent 回報失敗:stream disconnected before completion
|
|
209
|
-
stderr:
|
|
210
|
-
Error: stream disconnected before completion
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
執行成功時,stderr 會放在「其他輸出」段落,不算錯誤。
|
|
214
100
|
|
|
215
|
-
###
|
|
101
|
+
### Agent 設定
|
|
216
102
|
|
|
217
|
-
|
|
103
|
+
`agent setup` 可互動選擇已安裝的 CLI。也可以用指令新增或修改;這些指令會寫入 `flow.config.json`:
|
|
218
104
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
中斷於 #20 plan_review / plan-review / codex(沒有結束紀錄,resume 時會重跑這一步)
|
|
226
|
-
#19 plan_fix / plan-fix / claude ✓
|
|
227
|
-
摘要:接受三條審查意見,拆分 T-3、T-5
|
|
228
|
-
疑慮:T-15 可能仍太大
|
|
229
|
-
|
|
230
|
-
── 未結交接事項 ──
|
|
231
|
-
[plan] c9c730b280e87178 T-3、T-5 各混合多個獨立行為(proposed_resolved)
|
|
232
|
-
|
|
233
|
-
── 下一步 ──
|
|
234
|
-
agentflowctl logs f-xxxx 19 看上一步的完整 log
|
|
235
|
-
agentflowctl resume f-xxxx 從 plan_review 接續
|
|
236
|
-
agentflowctl cancel f-xxxx 放棄這個 run
|
|
105
|
+
```bash
|
|
106
|
+
agentflowctl agent add claude --adapter claude
|
|
107
|
+
agentflowctl agent add codex --adapter codex --model 你要用的模型
|
|
108
|
+
agentflowctl agent set codex --model 另一個模型
|
|
109
|
+
agentflowctl agent list
|
|
110
|
+
agentflowctl agent cycle claude,codex
|
|
237
111
|
```
|
|
238
112
|
|
|
239
|
-
|
|
113
|
+
`agent add` 的 `--adapter` 可填 `claude`、`codex`、`gemini` 或 `command`。`--model` 指定個別 agent 的模型;`--extra-arg=--參數` 可重複使用,傳給該 CLI。使用 `command` adapter 時,把指令寫在 `--` 後,例如 `agentflowctl agent add aider --adapter command -- aider --message {prompt}`。`agent remove <名稱>` 會移除設定與參與名單;`agent cycle` 不帶名單則顯示目前參與者。
|
|
240
114
|
|
|
241
|
-
|
|
242
|
-
2. `agentflowctl logs <id> <編號>` 看那份 log 的錯誤段落。
|
|
243
|
-
3. 解析結果看不出原因時,加 `--raw` 看原始輸出。
|
|
244
|
-
4. 必要時直接在 worktree(`.agentflowctl/worktrees/<id>`)裡修正,再執行 `agentflowctl resume <id>`。
|
|
115
|
+
### 專案設定
|
|
245
116
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
專案根目錄的 `flow.config.json`。完整範例見 `examples/flow.config.json`。未提供的欄位使用內建預設;`install`、`test`、`checks` 沒寫時,會依專案現況偵測(見下方「專案指令的偵測」)。
|
|
117
|
+
你也可以直接編輯 `flow.config.json`。這是可用的最小範例;沒有寫的欄位會使用預設值:
|
|
249
118
|
|
|
250
119
|
```json
|
|
251
120
|
{
|
|
252
|
-
"cycle": ["claude", "codex"],
|
|
253
|
-
"fixStrategy": "ring",
|
|
254
|
-
"tddSplit": true,
|
|
255
|
-
"tieBreak": "proceed",
|
|
256
|
-
"defaultModels": { "claude": "Claude 模型名稱", "codex": "Codex 模型名稱", "gemini": "Gemini 模型名稱" },
|
|
257
121
|
"agents": {
|
|
258
122
|
"claude": { "adapter": "claude" },
|
|
259
|
-
"codex": { "adapter": "codex", "model": "你要用的模型" }
|
|
260
|
-
|
|
261
|
-
|
|
123
|
+
"codex": { "adapter": "codex", "model": "你要用的模型" }
|
|
124
|
+
},
|
|
125
|
+
"cycle": ["claude", "codex"],
|
|
126
|
+
"maxAgentRuns": 80
|
|
262
127
|
}
|
|
263
128
|
```
|
|
264
129
|
|
|
265
|
-
|
|
|
266
|
-
|---|---|---|
|
|
267
|
-
| `cycle` | 自動偵測 | 參與的 agent,順序不影響分工(人選隨機決定)。未設定時依 `agents` 的順序取已安裝的 CLI。同一家 CLI 可以登記成不同 agent,例如 `claude-fast` 與 `claude-strong` |
|
|
268
|
-
| `defaultModels` | `{}` | 依 `claude`、`codex`、`gemini` adapter 指定全域預設 model;agent 的 `model` 優先,兩者都沒設時使用各 CLI 的預設。`command` adapter 不套用 |
|
|
269
|
-
| `fixStrategy` | `ring` | `ring`:審查意見隨機交給審查者以外的一家;`author`:交回最後作者 |
|
|
270
|
-
| `tddSplit` | `true` | 測試與實作是否分開 |
|
|
271
|
-
| `reviewQuorum` | `1` | 程式碼需要幾位不同審查者都 `approve` |
|
|
272
|
-
| `planReviewQuorum` | `1` | 計畫需要幾位不同審查者都 `approve` |
|
|
273
|
-
| `planArbiter` | `true` | 計畫審查僵持時交付仲裁。關掉之後,僵持會直接讓 run 失敗 |
|
|
274
|
-
| `tieBreak` | `proceed` | 兩家仲裁意見分歧時:`proceed` 繼續並記錄爭議;`stop` 停下 |
|
|
275
|
-
| `maxAgentRuns` | `60` | 單一 run 最多執行幾次 agent |
|
|
276
|
-
| `install` / `test` / `checks` | 依專案偵測 | 安裝、測試與 verify 階段實際執行的指令 |
|
|
277
|
-
| `agents` | `{}` | 可用的 agent,沒有內建的。每個都要指定 adapter(`claude`、`codex`、`gemini`,或用 `command` 接上其他 CLI) |
|
|
278
|
-
|
|
279
|
-
verify 失敗(型別、lint、建置)一律交回最後作者。審查意見才依 `fixStrategy` 決定修正者。
|
|
280
|
-
|
|
281
|
-
### 專案指令的偵測
|
|
282
|
-
|
|
283
|
-
`install`、`test`、`checks` 沒寫在 `flow.config.json` 時,每次讀設定都會依專案現況推出指令,不寫檔。有寫的欄位一律照你的設定。
|
|
284
|
-
|
|
285
|
-
- 套件管理器:先看 `package.json` 的 `packageManager`,再看 lockfile(`pnpm-lock.yaml`、`yarn.lock`、`bun.lock`/`bun.lockb`、`package-lock.json`),都沒有就用 npm。
|
|
286
|
-
- `install`:`pnpm install`、`yarn install`、`bun install` 或 `npm install --no-audit --no-fund`。不鎖 lockfile,因為實作時 agent 可能新增依賴。
|
|
287
|
-
- `checks`:typecheck、lint、test、build 四項。`package.json` 有對應的 script(`typecheck`/`type-check`、`lint`、`test`、`build`)就用 `<pm> run <script>`,否則用 `tsc --noEmit`、`eslint .`、`vitest run`、`vite build`,前面加上 `npx`、`pnpm exec`、`yarn` 或 `bunx`。
|
|
288
|
-
- `test`:`vitest run`,前綴同上。
|
|
289
|
-
|
|
290
|
-
`run` 建立 worktree 後會印出這次偵測到的指令:
|
|
291
|
-
|
|
292
|
-
```
|
|
293
|
-
[f-xxxx] 🔧 依專案偵測指令:pnpm(依 package.json 的 packageManager)
|
|
294
|
-
[f-xxxx] install:pnpm install
|
|
295
|
-
[f-xxxx] checks.typecheck:pnpm run type-check
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### 用指令管理 agent
|
|
299
|
-
|
|
300
|
-
`agents` 與 `cycle` 也可以用 `agent` 指令修改,不必手動編輯 JSON。每次寫入前都會先驗證整份設定:
|
|
301
|
-
|
|
302
|
-
```bash
|
|
303
|
-
agentflowctl agent setup # 偵測已安裝的 agent CLI,互動設定與參與的 agent
|
|
304
|
-
agentflowctl agent list # 設定的 agent、是否已安裝、是否參與
|
|
305
|
-
agentflowctl agent add claude-strong --adapter claude --model opus
|
|
306
|
-
agentflowctl agent add aider --adapter command -- aider --yes-always --message {prompt}
|
|
307
|
-
agentflowctl agent set codex --model 你要用的模型 --extra-arg=--search
|
|
308
|
-
agentflowctl agent set aider --adapter gemini # 換 adapter
|
|
309
|
-
agentflowctl agent remove aider
|
|
310
|
-
agentflowctl agent cycle claude-strong,codex,gemini # 不帶參數時顯示目前參與的 agent
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
修改會連帶更新相關設定,並在終端機列出:
|
|
314
|
-
|
|
315
|
-
- `set --adapter` 換 adapter 時,會清掉舊 adapter 的 `model`、`extraArgs`、`command`,這次有重新指定的除外。
|
|
316
|
-
- `remove` 會一併從 `cycle` 移除。`cycle` 變空就刪除這個欄位,改回從 `agents` 自動偵測。
|
|
317
|
-
- `--extra-arg` 可以重複指定,會整個取代原本的 `extraArgs`。參數以 `-` 開頭時,寫成 `--extra-arg=--sandbox`。
|
|
318
|
-
- `setup` 只詢問偵測到已安裝的 CLI,一個都沒有就不變更設定。遇到已存在的名稱會先問要不要覆寫;不覆寫時保留原設定,但仍會參與。在非互動式環境(CI、管線)裡請改用 `agent add`。`command` adapter 要自己寫指令,不在 `setup` 裡。
|
|
319
|
-
|
|
320
|
-
已建立的 run 會沿用建立時參與的 agent,不受這些修改影響。
|
|
321
|
-
|
|
322
|
-
## 計畫怎麼在沒有人的情況下通過
|
|
323
|
-
|
|
324
|
-
人工確認計畫是為了擋住方向錯了還一路做下去。預設用三層機制取代它;加上 `--manual-plan` 時,三層都過了仍會停下來等你。
|
|
325
|
-
|
|
326
|
-
**格式與覆蓋率。** 每次撰寫或修改計畫之後,都要重新通過 zod、任務相依、無循環、每條驗收條件都有任務負責,而且每個任務最多對應兩條驗收條件(一次只做一件事,最多兩件)。沒過就還原。
|
|
327
|
-
|
|
328
|
-
**跨模型審查。** 審查看需求覆蓋、驗收條件能不能測且一條只寫一個行為、任務是否只做一件事(最多兩件)與技術方向。審查者只能寫意見。若改了規格或計畫,檔案會被還原。修改者要在 `plan.md` 的「審查回應」逐條回覆;不同意要寫理由。
|
|
329
|
-
|
|
330
|
-
計畫審查先核對需求與四份計畫交接檔,再查閱任務說明中要修改的既有檔案,有疑慮時才擴大範圍;審查紀錄只列會影響實作的問題,不逐條列已通過項目。計畫修訂先依 `feedback.md` 定位需要改的段落,修改驗收條件或任務時再檢查受影響的對應關係。檔案格式、任務對驗收條件的覆蓋與任務相依仍由程式驗證;原始需求的語意覆蓋由審查者判斷,以減少反覆讀取文件的 token 用量。
|
|
331
|
-
|
|
332
|
-
**僵持時仲裁。** 兩種情況會觸發:這輪審查意見和上一輪一樣,或已達重試上限。仲裁者只判斷一件事:照這份計畫實作,能不能滿足需求。
|
|
333
|
-
|
|
334
|
-
| 有幾家 | 誰來仲裁 | 結果 |
|
|
130
|
+
| 欄位 | 預設 | 設定方式與用途 |
|
|
335
131
|
| --- | --- | --- |
|
|
336
|
-
|
|
|
337
|
-
|
|
|
338
|
-
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
|
355
|
-
|
|
|
356
|
-
|
|
|
357
|
-
|
|
|
358
|
-
|
|
|
359
|
-
| fix | verify 失敗交回作者;審查意見依 `fixStrategy` | 沒有刪除測試檔,也沒有修改規格與計畫檔 | 還原並重試 |
|
|
360
|
-
| review | 作者以外隨機挑(可多位,不重複) | 所有審查者都 `approve`(審查者對程式碼與規格、計畫檔的修改一律還原) | 依 `fixStrategy` 交給他人修正 |
|
|
361
|
-
| pr | — | push 成功;有 `gh` 就開 PR | — |
|
|
362
|
-
|
|
363
|
-
驗收條件寫在 `.flow/acceptance.json`(`AC-1`…),任務寫在 `.flow/tasks.json`(`T-1`…)。
|
|
364
|
-
每個任務的寫測試與寫實作 prompt 只帶入該任務對應的驗收條件;agent 優先讀任務與相關程式碼,遇到資訊不足或矛盾才查規格、計畫的相關段落。agent 可先跑相關測試,紅燈與完整測試仍由外部流程執行與判定,減少重複讀取文件和全套測試輸出所用的 token。
|
|
365
|
-
計畫定案後(實作、修正、程式碼審查)不可修改 `.flow/` 裡的規格與計畫檔(`spec.md`、`acceptance.json`、`plan.md`、`tasks.json`、`tasks.ordered.json`);實作與修正時被改就還原並重試,因為寫出的程式碼可能依賴被改過的規格,必須重寫;審查者只交出審查結果,修改直接還原即可,不必重跑審查。對規格有疑慮要寫進交接事項。
|
|
366
|
-
程式碼審查仍逐條核對所有驗收條件,但只在 `review.json` 列出未通過或其他重要問題;先看 diff 與相關檔案,驗收條件不清楚時才查規格。修正階段先依 `feedback.md` 定位問題並執行相關檢查,完整檢查仍由後續 verify 執行,以減少反覆讀取完整文件與測試輸出。
|
|
367
|
-
|
|
368
|
-
## Prompt 結構
|
|
369
|
-
|
|
370
|
-
每個階段的 prompt 都有專屬角色:需求分析師、軟體架構師、計畫審查者、計畫修訂者、中立仲裁者、測試工程師、實作工程師、除錯工程師、程式碼審查者。內容用 XML 標籤分段:`<role>`、`<context>`、`<inputs>`、`<steps>`、`<constraints>`、`<output_format>`、`<reply_format>`。
|
|
371
|
-
|
|
372
|
-
Agent 的最後回覆要附上 XML 中繼資料:
|
|
373
|
-
|
|
374
|
-
```xml
|
|
375
|
-
<result>
|
|
376
|
-
<status>done 或 blocked</status>
|
|
377
|
-
<summary>做了什麼</summary>
|
|
378
|
-
<files_changed><file>src/form.ts</file></files_changed>
|
|
379
|
-
<concerns>對規格或測試的疑慮</concerns>
|
|
380
|
-
</result>
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
`blocked` 與 `concerns` 會印在終端機上,完整回覆留在 log,可用 `agentflowctl logs` 查看。這份中繼資料只給人看;缺少或格式錯誤都不影響流程,是否通過仍由上表的程式檢查決定。
|
|
384
|
-
|
|
385
|
-
### Agent 交接紀錄
|
|
386
|
-
|
|
387
|
-
每次 agent 執行前,程式會把與當前階段有關的未結事項寫入 `.flow/handoff-context.md`。agent 完成時必須寫 `.flow/handoff-response.json`,包含 `newIssues` 和 `dispositions` 兩個陣列;沒有事項也要明確寫成 `{ "newIssues": [], "dispositions": [] }`。缺少檔案或 JSON 格式不合法,會依該步驟的重試規則處理。
|
|
388
|
-
|
|
389
|
-
程式只在原有關卡通過後接收交接回覆,並將正式紀錄原子儲存於 `.agentflowctl/runs/<id>/handoff.json`。`action` 是需要後續處理的事項;`info` 只供參考。作者只能提出已修正並附證據,審查者才能確認結案或附理由接受。XML `<concerns>` 可以供人閱讀,但重要疑慮必須寫進交接 JSON,才能交給下一位 agent。額度代打與重試不會接收失敗呼叫的交接內容;中斷後可用 `resume` 接續。
|
|
390
|
-
|
|
391
|
-
計畫審查或程式碼審查若核准,但該階段仍有未結的 `action`,程式會視為互相矛盾的審查結果並重試。計畫定案和開 PR 前也會再檢查一次;`info` 會提供給目標階段閱讀,但不阻擋通關。審查結果本身也要一致:核准時 `items` 不可有未通過的項目,要求修改時至少要列一筆,否則視為格式錯誤並重新審查。
|
|
392
|
-
|
|
393
|
-
## Adapter
|
|
394
|
-
|
|
395
|
-
| adapter | 執行方式 | 權限 |
|
|
396
|
-
|---|---|---|
|
|
397
|
-
| `claude` | `claude -p --output-format stream-json` | acceptEdits、禁止 git 寫入、設定檔在 `.agentflowctl/runs/<id>/claude-settings.json` |
|
|
398
|
-
| `codex` | `codex exec --json --sandbox workspace-write`(prompt 走 stdin) | 只能改工作目錄,預設不能連網 |
|
|
399
|
-
| `gemini` | `gemini -p --output-format stream-json --approval-mode yolo` | 沒有細緻權限;可在 `extraArgs` 加 `--sandbox`,或放在可丟棄環境 |
|
|
400
|
-
| `command` | 任意指令;`{prompt}` 替換,或走 stdin | 取決於該工具 |
|
|
401
|
-
|
|
402
|
-
Codex 沙箱預設不能連網,所以建立 worktree 時會先跑 `install`。CLI 參數與事件格式更新得很快,第一次使用前先 `doctor`,再用一個小需求實測。
|
|
403
|
-
|
|
404
|
-
各家讀的專案說明檔不同:Claude Code 讀 `CLAUDE.md`,Codex 讀 `AGENTS.md`,Gemini 讀 `GEMINI.md`。把專案慣例寫在 `AGENTS.md`,另外兩個檔案各用一行引用它。agentflowctl 的 prompt 在 `prompts/`,不依賴任何一家的 skills 或 plugins。
|
|
405
|
-
|
|
406
|
-
## 額度與代打
|
|
407
|
-
|
|
408
|
-
上限是執行次數(`maxAgentRuns`,預設 60),不是金額。`status` 會列出各 agent 的執行次數與 token 數。
|
|
409
|
-
|
|
410
|
-
額度用完時:
|
|
411
|
-
|
|
412
|
-
| 步驟 | 行為 |
|
|
413
|
-
| --- | --- |
|
|
414
|
-
| 計畫審查、程式碼審查、仲裁 | 暫停。額度恢復後 `agentflowctl resume`。換人代審會變成作者審自己 |
|
|
415
|
-
| 規格、計畫、修改計畫、寫測試、寫實作、修正 | 隨機由另一家還有額度的 agent 代打 |
|
|
416
|
-
| 每家都用完 | 暫停 |
|
|
417
|
-
|
|
418
|
-
換人或暫停前,額度用完的 agent 留下的半成品會先清掉。代打寫在 `.agentflowctl/runs/<id>/substitutions.jsonl`,`status` 會列出。commit 結尾標的是實際執行的模型。
|
|
419
|
-
|
|
420
|
-
若寫實作的那家額度用完、改由寫測試的那家代打,這個任務的測試與實作就會出自同一家,`status` 會標註。審查步驟仍然暫停,等原本的另一家,因為那是此時剩下的交叉檢查。
|
|
421
|
-
|
|
422
|
-
額度錯誤靠錯誤訊息辨識(usage limit、rate limit、quota、429 等),只在 agent 執行失敗時判斷。辨識不到時,會當成一般失敗重試。
|
|
423
|
-
|
|
424
|
-
## 在哪裡跑
|
|
425
|
-
|
|
426
|
-
agentflowctl 本身只依賴 Node.js 與 git。專案指令透過系統 shell 執行。
|
|
427
|
-
|
|
428
|
-
| 環境 | 適合的用法 |
|
|
429
|
-
|---|---|
|
|
430
|
-
| 自己的電腦 | 自己的專案、自己寫的需求。剛開始可以加 `--manual-plan`,確認審查品質後再拿掉 |
|
|
431
|
-
| 容器、遠端開發機 | 無人值守。先在該環境內完成各家 CLI 的登入 |
|
|
432
|
-
| Claude Code、Codex 裡面 | 讓它們用 shell 執行 `npx agentflowctl` |
|
|
433
|
-
|
|
434
|
-
沒有容器隔離時,verify 會在你的電腦上執行 agent 寫出來的程式碼。Gemini 在無人值守時是 yolo 模式。處理外部 issue,或需求文字不是你自己寫的,放到可丟棄的環境。AI 審查計畫擋不住夾在需求裡的指示。
|
|
435
|
-
|
|
436
|
-
## 專案結構
|
|
132
|
+
| `agents` | `{}` | 以名稱為 key 定義 agent;每個都要有 `adapter`,可加 `model`、`extraArgs`;`command` adapter 另需 `command` 指令陣列 |
|
|
133
|
+
| `cycle` | 自動偵測 | 填 agent 名稱陣列,例如 `["claude", "codex"]`;未填時使用已設定且可執行的 agent;順序不決定角色 |
|
|
134
|
+
| `defaultModels` | `{}` | 依 adapter 設預設模型,例如 `{ "claude": "模型名稱" }`;個別 agent 的 `model` 優先 |
|
|
135
|
+
| `fixStrategy` | `"ring"` | `"ring"` 由審查者以外的 agent 修正;`"author"` 交回最後作者 |
|
|
136
|
+
| `tddSplit` | `true` | 有多位 agent 時,`true` 會把同一任務的測試與實作分給不同 agent |
|
|
137
|
+
| `reviewQuorum` | `1` | 任務與最終程式碼審查需要幾位不同審查者核准 |
|
|
138
|
+
| `planReviewQuorum` | `1` | 計畫需要幾位不同審查者核准 |
|
|
139
|
+
| `planArbiter` | `true` | 計畫審查僵持時是否啟用仲裁 |
|
|
140
|
+
| `tieBreak` | `"proceed"` | 兩位仲裁者意見分歧時,`"proceed"` 繼續、`"stop"` 停止 |
|
|
141
|
+
| `maxAgentRuns` | `60` | 一次 run 最多執行幾次 agent;可用指令選項覆蓋 |
|
|
142
|
+
| `install`、`test` | 依專案偵測 | 寫成指令字串,例如 `"install": "pnpm install"` |
|
|
143
|
+
| `checks` | 依專案偵測 | 檢查清單,例如 `[{ "name": "test", "cmd": "pnpm test" }]`;提供時會取代整份預設清單 |
|
|
144
|
+
| `testPattern` | 常見的 `.test.`、`.spec.` 檔名 | 辨識測試檔的正規表示式字串;非標準檔名時調整 |
|
|
145
|
+
|
|
146
|
+
`install`、`test`、`checks` 未設定時,會依 `packageManager`、lockfile 和 `package.json` scripts 偵測。完整範例見 [examples/flow.config.json](examples/flow.config.json)。專案設定每一步都會重新讀取,但已建立 run 的參與 agent 與執行次數上限會沿用建立時的值;要調高後者請用 `resume --max-agent-runs`。
|
|
147
|
+
|
|
148
|
+
### 環境變數
|
|
149
|
+
|
|
150
|
+
| 變數 | 預設 | 設定方式與用途 |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `AGENTFLOWCTL_MAX_ATTEMPTS` | `3` | 同一關連續失敗幾次後停止;例如 `AGENTFLOWCTL_MAX_ATTEMPTS=10 agentflowctl run --req "..."` |
|
|
153
|
+
| `AGENTFLOWCTL_VERBOSE` | 未開啟 | 設為 `1` 顯示詳細輸出,效果同 `-v` |
|
|
154
|
+
| `AGENTFLOWCTL_MAX_TURNS` | `80` | 目前程式會讀取此值,但尚未用它限制 agent 執行 |
|
|
437
155
|
|
|
438
|
-
|
|
439
|
-
src/
|
|
440
|
-
cli.ts 指令列(run、doctor、status……)
|
|
441
|
-
engine.ts 狀態機與各階段
|
|
442
|
-
roles.ts 角色分配規則(含計畫修正者與仲裁者)
|
|
443
|
-
runner.ts 執行 agent、正規化結果、執行專案指令
|
|
444
|
-
logs.ts log 檔名、檔頭檔尾、列表與解析
|
|
445
|
-
stopReport.ts run 停下時的結果、未結交接事項與下一步指令
|
|
446
|
-
agents/ claude、codex、gemini、command
|
|
447
|
-
setup.ts agent setup 互動精靈
|
|
448
|
-
git.ts worktree 與 git 操作
|
|
449
|
-
cleanup.ts clean:移除 worktree 與 run 紀錄
|
|
450
|
-
store.ts 狀態、用量、代打紀錄
|
|
451
|
-
tasks.ts 任務 DAG
|
|
452
|
-
schemas.ts zod schema
|
|
453
|
-
prompts/ 各階段 prompt
|
|
454
|
-
examples/ flow.config.json 與 GitHub Actions
|
|
455
|
-
```
|
|
156
|
+
環境變數對新啟動的 agentflowctl 程序生效。`AGENTFLOWCTL_MAX_ATTEMPTS` 是單一關卡的重試上限;`maxAgentRuns` 則是整次 run 的 agent 執行次數上限。
|
|
456
157
|
|
|
457
|
-
|
|
158
|
+
## 更多文件
|
|
458
159
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
pnpm test
|
|
462
|
-
```
|
|
160
|
+
- [完整指令、設定與流程說明](docs/reference.md):選項、角色分配、審查規則、log、額度處理與技術細節。
|
|
161
|
+
- [各階段讀寫的檔案](docs/engine-stage-files.md):`.flow/`、回饋與審查檔案如何交接。
|
|
463
162
|
|
|
464
163
|
## 授權
|
|
465
164
|
|
package/dist/cli.js
CHANGED
|
@@ -88,6 +88,8 @@ async function resolveCycle(flag) {
|
|
|
88
88
|
throw new Error(`設定的 agent(${defined.join("、")})都沒有偵測到已安裝的 CLI,可用 agentflowctl doctor 檢查`);
|
|
89
89
|
return found;
|
|
90
90
|
}
|
|
91
|
+
/** status 任務清單中,進行中任務的標記 */
|
|
92
|
+
const TASK_PHASE_MARK = { tests: "🧪", code: "🛠️ ", review: "👀", verify: "🔍", fix: "🩹" };
|
|
91
93
|
const program = new Command()
|
|
92
94
|
.name("agentflowctl")
|
|
93
95
|
.description("在專案資料夾內執行的 Agent 開發流程:規格 → 計畫 → TDD 實作 → 驗證 → 審查 → PR")
|
|
@@ -228,7 +230,7 @@ program
|
|
|
228
230
|
console.log("\n任務");
|
|
229
231
|
tasks.data.forEach((t, i) => {
|
|
230
232
|
const active = i === run.taskIndex && run.stage === "implement";
|
|
231
|
-
const mark = i < run.taskIndex ? "✅" : active ?
|
|
233
|
+
const mark = i < run.taskIndex ? "✅" : active ? TASK_PHASE_MARK[run.taskPhase] : "⬜";
|
|
232
234
|
console.log(` ${mark} ${t.id} ${t.title}`);
|
|
233
235
|
});
|
|
234
236
|
});
|
package/dist/engine.js
CHANGED
|
@@ -215,7 +215,7 @@ function announceTasks(run, ordered) {
|
|
|
215
215
|
info(run, `📋 共 ${ordered.length} 個任務:${ordered.map((t) => t.id).join(" → ")}`);
|
|
216
216
|
ordered.forEach((t, i) => {
|
|
217
217
|
const a = taskAgents(run.cycle, i, cfg.tddSplit, run.id);
|
|
218
|
-
info(run, ` ${t.id} 測試:${a.tests} 實作:${a.code}`);
|
|
218
|
+
info(run, ` ${t.id} 測試:${a.tests} 實作:${a.code} 審查:${a.review}`);
|
|
219
219
|
});
|
|
220
220
|
}
|
|
221
221
|
/** 計畫定案後:預設直接開始實作;--manual-plan 時才停下來等人 */
|
|
@@ -341,7 +341,8 @@ async function planFixStage(run) {
|
|
|
341
341
|
const handoffError = finishHandoff(run, outcome, "writer");
|
|
342
342
|
if (handoffError) {
|
|
343
343
|
restorePlan(run, snap);
|
|
344
|
-
|
|
344
|
+
// 計畫已還原,要保留原本的審查意見,否則下一次修正不知道要改什麼
|
|
345
|
+
return retry(run, "plan-fix", `${feedback}\n\n另外,交接回覆不合格,本次修改已還原:${handoffError}`, "plan_fix");
|
|
345
346
|
}
|
|
346
347
|
acceptPlan(run, ordered);
|
|
347
348
|
writeFileSync(flowFile(run, "feedback.md"), feedback); // 保留審查意見,讓下一輪審查者知道上次提了什麼
|
|
@@ -433,6 +434,12 @@ async function implementStage(run) {
|
|
|
433
434
|
if (!acceptance.ok)
|
|
434
435
|
throw new Error(acceptance.error);
|
|
435
436
|
const acceptanceJson = JSON.stringify(taskAcceptance(task, acceptance.data), null, 2);
|
|
437
|
+
if (run.taskPhase === "review")
|
|
438
|
+
return taskReviewStep(run, task, progress, taskJson, acceptanceJson);
|
|
439
|
+
if (run.taskPhase === "verify")
|
|
440
|
+
return taskVerifyStep(run, task, progress);
|
|
441
|
+
if (run.taskPhase === "fix")
|
|
442
|
+
return taskFixStep(run, task, progress);
|
|
436
443
|
const agents = taskAgents(run.cycle, run.taskIndex, cfg.tddSplit, run.id);
|
|
437
444
|
// ── 紅燈:只寫測試,而且測試必須失敗 ──
|
|
438
445
|
if (run.taskPhase === "tests") {
|
|
@@ -471,7 +478,7 @@ async function implementStage(run) {
|
|
|
471
478
|
}
|
|
472
479
|
writeFileSync(flowFile(run, "red-output.txt"), red.output);
|
|
473
480
|
info(run, `🔴 [${progress}] 測試如預期失敗`);
|
|
474
|
-
return { ...succeed(run, key, "implement"), taskPhase: "code", testsCommit: commit, lastTestsAuthor: testsAuthor };
|
|
481
|
+
return { ...succeed(run, key, "implement"), taskPhase: "code", taskBase: before, testsCommit: commit, lastTestsAuthor: testsAuthor };
|
|
475
482
|
}
|
|
476
483
|
// ── 綠燈:實作到測試通過,而且不可動測試 ──
|
|
477
484
|
const key = `${task.id}:code`;
|
|
@@ -506,28 +513,85 @@ async function implementStage(run) {
|
|
|
506
513
|
await resetTo(repo, testsCommit);
|
|
507
514
|
return retry(run, key, handoffError, "implement");
|
|
508
515
|
}
|
|
509
|
-
info(run, `🟢 [${progress}]
|
|
516
|
+
info(run, `🟢 [${progress}] 測試通過`);
|
|
517
|
+
return { ...succeed(run, key, "implement"), taskPhase: "review", lastWriter: codeAuthor };
|
|
518
|
+
}
|
|
519
|
+
// ── 任務審查:只看這個任務的變更與驗收條件 ──
|
|
520
|
+
async function taskReviewStep(run, task, progress, taskJson, acceptanceJson) {
|
|
521
|
+
const key = `${task.id}:review`;
|
|
522
|
+
const base = run.taskBase ?? (run.testsCommit && `${run.testsCommit}~1`);
|
|
523
|
+
if (!base)
|
|
524
|
+
throw new Error("缺少 taskBase,狀態不一致");
|
|
525
|
+
const result = await codeReview(run, {
|
|
526
|
+
base,
|
|
527
|
+
seed: `${run.id}:review:${task.id}:${run.attempts[key] ?? 0}`,
|
|
528
|
+
label: `[${progress}] 任務審查`,
|
|
529
|
+
step: `${task.id}-review`,
|
|
530
|
+
prompt: (reviewer, authors) => renderPrompt("task-review", { reviewer, authors, task: taskJson, acceptance: acceptanceJson }),
|
|
531
|
+
saveAs: (reviewer) => `review-${task.id}-${reviewer}.json`,
|
|
532
|
+
testAuthor: run.lastTestsAuthor,
|
|
533
|
+
// 輪流交換角色:優先由排定的審查者審查,讓各家用量平均
|
|
534
|
+
prefer: taskAgents(run.cycle, run.taskIndex, loadRepoConfig().tddSplit, run.id).review,
|
|
535
|
+
// 未結交接事項可能屬於後面的任務,由最後的整體審查把關
|
|
536
|
+
gate: false,
|
|
537
|
+
runKey: `${task.id}:review-run`,
|
|
538
|
+
backTo: "implement",
|
|
539
|
+
});
|
|
540
|
+
if ("run" in result)
|
|
541
|
+
return result.run;
|
|
542
|
+
const reviewed = succeed(run, `${task.id}:review-run`, "implement");
|
|
543
|
+
if (!result.objector)
|
|
544
|
+
return { ...succeed(reviewed, key, "implement"), taskPhase: "verify" };
|
|
545
|
+
return {
|
|
546
|
+
...retry(reviewed, key, `任務審查要求修改:\n\n${result.issues.join("\n\n")}`, "implement"),
|
|
547
|
+
taskPhase: "fix",
|
|
548
|
+
fixSource: "review",
|
|
549
|
+
lastReviewer: result.objector,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
// ── 任務驗證:通過才進入下一個任務 ──
|
|
553
|
+
async function taskVerifyStep(run, task, progress) {
|
|
554
|
+
const key = `${task.id}:verify`;
|
|
555
|
+
info(run, `🔍 [${progress}] 執行驗證`);
|
|
556
|
+
const report = await runChecks(run, `${task.id}-`);
|
|
557
|
+
if (report)
|
|
558
|
+
return { ...retry(run, key, report, "implement"), taskPhase: "fix", fixSource: "verify" };
|
|
559
|
+
info(run, `✅ [${progress}] 完成`);
|
|
510
560
|
return {
|
|
511
561
|
...succeed(run, key, "implement"),
|
|
512
562
|
taskIndex: run.taskIndex + 1,
|
|
513
563
|
taskPhase: "tests",
|
|
564
|
+
taskBase: undefined,
|
|
514
565
|
testsCommit: undefined,
|
|
515
566
|
lastTestsAuthor: undefined,
|
|
516
|
-
lastWriter: codeAuthor,
|
|
517
567
|
};
|
|
518
568
|
}
|
|
519
|
-
|
|
520
|
-
|
|
569
|
+
// ── 任務修正:修完重新審查、驗證 ──
|
|
570
|
+
async function taskFixStep(run, task, progress) {
|
|
571
|
+
const result = await applyFix(run, {
|
|
572
|
+
seed: `${run.id}:fix:${task.id}:${run.attempts[`${task.id}:review`] ?? 0}`,
|
|
573
|
+
label: `[${progress}] `,
|
|
574
|
+
step: `${task.id}-fix`,
|
|
575
|
+
commitScope: `fix(${task.id})`,
|
|
576
|
+
key: `${task.id}:fix`,
|
|
577
|
+
backTo: "implement",
|
|
578
|
+
});
|
|
579
|
+
if ("run" in result)
|
|
580
|
+
return result.run;
|
|
581
|
+
return { ...succeed(run, `${task.id}:fix`, "implement"), taskPhase: "review", lastWriter: result.agent };
|
|
582
|
+
}
|
|
583
|
+
/** 執行 install 與所有 checks,結果寫入 verify.json;有失敗時回傳給修正者的報告 */
|
|
584
|
+
async function runChecks(run, stepPrefix = "") {
|
|
521
585
|
const cfg = loadRepoConfig();
|
|
522
586
|
const results = [];
|
|
523
|
-
const install = await runCommand(target(run,
|
|
587
|
+
const install = await runCommand(target(run, `${stepPrefix}install`, CMD_AGENT), cfg.install);
|
|
524
588
|
if (!install.ok) {
|
|
525
589
|
info(run, ` ✗ install${logHint(run, install.seq)}`);
|
|
526
590
|
results.push({ name: "install", ok: false, output: tail(install.output) });
|
|
527
591
|
}
|
|
528
592
|
else {
|
|
529
593
|
for (const check of cfg.checks) {
|
|
530
|
-
const r = await runCommand(target(run, check.name
|
|
594
|
+
const r = await runCommand(target(run, `${stepPrefix}${check.name}`, CMD_AGENT), check.cmd);
|
|
531
595
|
info(run, ` ${r.ok ? "✓" : "✗"} ${check.name}${r.ok ? "" : logHint(run, r.seq)}`);
|
|
532
596
|
results.push({ name: check.name, ok: r.ok, output: tail(r.output, 3000) });
|
|
533
597
|
}
|
|
@@ -535,11 +599,18 @@ async function verifyStage(run) {
|
|
|
535
599
|
writeFileSync(flowFile(run, "verify.json"), JSON.stringify(results, null, 2));
|
|
536
600
|
const failed = results.filter((r) => !r.ok);
|
|
537
601
|
if (failed.length === 0)
|
|
602
|
+
return undefined;
|
|
603
|
+
return failed.map((f) => `## ${f.name} 失敗\n\n\`\`\`\n${f.output}\n\`\`\``).join("\n\n");
|
|
604
|
+
}
|
|
605
|
+
async function verifyStage(run) {
|
|
606
|
+
info(run, "🔍 執行驗證");
|
|
607
|
+
const report = await runChecks(run);
|
|
608
|
+
if (!report)
|
|
538
609
|
return succeed(run, "verify", "review");
|
|
539
|
-
const report = failed.map((f) => `## ${f.name} 失敗\n\n\`\`\`\n${f.output}\n\`\`\``).join("\n\n");
|
|
540
610
|
return { ...retry(run, "verify", report, "fix"), fixSource: "verify" };
|
|
541
611
|
}
|
|
542
|
-
|
|
612
|
+
/** 修正驗證錯誤或審查意見;成功時回傳實際修正者,未通過時回傳重試後的 run */
|
|
613
|
+
async function applyFix(run, opts) {
|
|
543
614
|
const cfg = loadRepoConfig();
|
|
544
615
|
const source = run.fixSource ?? "verify";
|
|
545
616
|
const agent = fixAgent(run.cycle, {
|
|
@@ -547,55 +618,74 @@ async function fixStage(run) {
|
|
|
547
618
|
strategy: cfg.fixStrategy,
|
|
548
619
|
lastWriter: run.lastWriter,
|
|
549
620
|
lastReviewer: run.lastReviewer,
|
|
550
|
-
seed:
|
|
621
|
+
seed: opts.seed,
|
|
551
622
|
});
|
|
552
623
|
const why = source === "review" ? `依 ${run.lastReviewer ?? "reviewer"} 的審查意見` : "修正驗證錯誤";
|
|
553
|
-
info(run, `🩹 ${why}(${agent})`);
|
|
624
|
+
info(run, `🩹 ${opts.label}${why}(${agent})`);
|
|
554
625
|
const repo = worktreeDir(run.id);
|
|
555
626
|
const feedback = readFeedback(run);
|
|
556
627
|
const before = await headCommit(repo);
|
|
557
628
|
const snap = snapshotPlan(run, LOCKED_FILES);
|
|
558
|
-
const outcome = await agentStep(run, agent,
|
|
629
|
+
const outcome = await agentStep(run, agent, opts.step, renderPrompt("fix", { testPattern: cfg.testPattern }), {
|
|
559
630
|
kind: "write",
|
|
560
631
|
reset: async () => { await resetTo(repo, before); restorePlan(run, snap); },
|
|
561
632
|
});
|
|
562
633
|
const { r, agent: actual } = outcome;
|
|
563
634
|
const tampered = restorePlan(run, snap);
|
|
635
|
+
const again = (reason) => ({ run: retry(run, opts.key, reason, opts.backTo) });
|
|
564
636
|
if (!r.ok)
|
|
565
|
-
return
|
|
637
|
+
return again(`${feedback}\n\n(上次修正時 Agent 執行失敗:${r.summary})`);
|
|
566
638
|
if (tampered.length) {
|
|
567
639
|
await resetTo(repo, before);
|
|
568
|
-
return
|
|
640
|
+
return again(`${feedback}\n\n另外:${planTamperedMessage(tampered)}`);
|
|
569
641
|
}
|
|
570
|
-
await commitAll(repo,
|
|
642
|
+
await commitAll(repo, `${opts.commitScope}: ${why} [${actual}]`);
|
|
571
643
|
const testRe = new RegExp(cfg.testPattern);
|
|
572
644
|
const deleted = (await changedFiles(repo, before, await headCommit(repo), "D")).filter((f) => testRe.test(f));
|
|
573
645
|
if (deleted.length) {
|
|
574
646
|
await resetTo(repo, before);
|
|
575
|
-
return
|
|
647
|
+
return again(`${feedback}\n\n另外:不可刪除測試檔來讓檢查通過,已還原:${deleted.join(", ")}`);
|
|
576
648
|
}
|
|
577
649
|
const handoffError = finishHandoff(run, outcome, "writer");
|
|
578
650
|
if (handoffError) {
|
|
579
651
|
await resetTo(repo, before);
|
|
580
|
-
return
|
|
652
|
+
return again(`${feedback}\n\n另外,交接回覆不合格,本次修正已還原:${handoffError}`);
|
|
581
653
|
}
|
|
654
|
+
return { agent: actual };
|
|
655
|
+
}
|
|
656
|
+
async function fixStage(run) {
|
|
657
|
+
const result = await applyFix(run, {
|
|
658
|
+
seed: `${run.id}:fix:${run.attempts.review ?? 0}`,
|
|
659
|
+
label: "",
|
|
660
|
+
step: "fix",
|
|
661
|
+
commitScope: "fix",
|
|
662
|
+
key: "fix",
|
|
663
|
+
backTo: "fix",
|
|
664
|
+
});
|
|
665
|
+
if ("run" in result)
|
|
666
|
+
return result.run;
|
|
582
667
|
// 修正者成為新的作者,下一輪審查會換成別人
|
|
583
|
-
return { ...to(run, "verify"), lastWriter:
|
|
668
|
+
return { ...to(run, "verify"), lastWriter: result.agent };
|
|
584
669
|
}
|
|
585
|
-
|
|
670
|
+
/**
|
|
671
|
+
* 由作者以外的審查小組審查 base 之後的變更;回傳第一位要求修改的審查者與所有意見,
|
|
672
|
+
* 審查本身未完成(執行失敗、格式錯誤、交接不合格)時回傳重試後的 run。
|
|
673
|
+
* gate 為 true 時,核准前必須結清所有程式碼類的未結交接事項。
|
|
674
|
+
*/
|
|
675
|
+
async function codeReview(run, opts) {
|
|
586
676
|
const cfg = loadRepoConfig();
|
|
587
677
|
const repo = worktreeDir(run.id);
|
|
588
|
-
const panel = reviewers(run.cycle, run.lastWriter, cfg.reviewQuorum,
|
|
589
|
-
writeFileSync(flowFile(run, "diff.patch"), await git(repo, "diff", `${
|
|
590
|
-
const authors = [...new Set((await git(repo, "log", "--format=%s", `${
|
|
678
|
+
const panel = reviewers(run.cycle, run.lastWriter, cfg.reviewQuorum, opts.seed, opts.testAuthor, opts.prefer);
|
|
679
|
+
writeFileSync(flowFile(run, "diff.patch"), await git(repo, "diff", `${opts.base}...HEAD`));
|
|
680
|
+
const authors = [...new Set((await git(repo, "log", "--format=%s", `${opts.base}..HEAD`)).match(/\[[^\]]+\]$/gm) ?? [])]
|
|
591
681
|
.map((s) => s.slice(1, -1));
|
|
592
682
|
const issues = [];
|
|
593
|
-
let
|
|
683
|
+
let objector;
|
|
594
684
|
for (const [slot, reviewer] of panel.entries()) {
|
|
595
|
-
info(run, `👀
|
|
685
|
+
info(run, `👀 ${opts.label}(${reviewer})`);
|
|
596
686
|
rmSync(flowFile(run, "review.json"), { force: true });
|
|
597
687
|
const snap = snapshotPlan(run, LOCKED_FILES);
|
|
598
|
-
const outcome = await agentStep(run, reviewer,
|
|
688
|
+
const outcome = await agentStep(run, reviewer, opts.step, opts.prompt(reviewer, authors.join("、") || "未知"), {
|
|
599
689
|
kind: "review", slot, reset: async () => { await discardChanges(repo); restorePlan(run, snap); },
|
|
600
690
|
});
|
|
601
691
|
const { r } = outcome;
|
|
@@ -604,30 +694,47 @@ async function reviewStage(run) {
|
|
|
604
694
|
if (tampered.length)
|
|
605
695
|
info(run, ` ↩️ 已還原審查者修改的檔案:${tampered.join(", ")}`);
|
|
606
696
|
if (!r.ok)
|
|
607
|
-
return retry(run,
|
|
697
|
+
return { run: retry(run, opts.runKey, `Agent 執行失敗:${r.summary}`, opts.backTo) };
|
|
608
698
|
const review = readJsonFile(flowFile(run, "review.json"), ConsistentReviewResult);
|
|
609
699
|
if (!review.ok)
|
|
610
|
-
return retry(run,
|
|
611
|
-
const
|
|
700
|
+
return { run: retry(run, opts.runKey, review.error, opts.backTo) };
|
|
701
|
+
const gate = opts.gate ? { target: "code", verdict: review.data.verdict } : undefined;
|
|
702
|
+
const handoffError = finishHandoff(run, outcome, "reviewer", gate);
|
|
612
703
|
if (handoffError)
|
|
613
|
-
return retry(run,
|
|
614
|
-
renameSync(flowFile(run, "review.json"), flowFile(run,
|
|
704
|
+
return { run: retry(run, opts.runKey, handoffError, opts.backTo) };
|
|
705
|
+
renameSync(flowFile(run, "review.json"), flowFile(run, opts.saveAs(reviewer)));
|
|
615
706
|
if (review.data.verdict === "approve") {
|
|
616
707
|
info(run, ` ✓ ${reviewer} 核准`);
|
|
617
708
|
continue;
|
|
618
709
|
}
|
|
619
710
|
info(run, ` ✗ ${reviewer} 要求修改`);
|
|
620
|
-
|
|
711
|
+
objector ??= reviewer;
|
|
621
712
|
issues.push(opinion(reviewer, review.data.items
|
|
622
713
|
.filter((i) => i.status !== "met")
|
|
623
714
|
.map((i) => reviewIssue(i.criterion, i.status, i.note))));
|
|
624
715
|
}
|
|
625
|
-
|
|
716
|
+
return { objector, issues };
|
|
717
|
+
}
|
|
718
|
+
async function reviewStage(run) {
|
|
719
|
+
const result = await codeReview(run, {
|
|
720
|
+
base: run.baseBranch,
|
|
721
|
+
seed: `${run.id}:review:${run.attempts.review ?? 0}`,
|
|
722
|
+
label: "程式碼審查",
|
|
723
|
+
step: "review",
|
|
724
|
+
prompt: (reviewer, authors) => renderPrompt("review", { reviewer, authors }),
|
|
725
|
+
saveAs: (reviewer) => `review-${reviewer}.json`,
|
|
726
|
+
gate: true,
|
|
727
|
+
runKey: "review-run",
|
|
728
|
+
backTo: "review",
|
|
729
|
+
});
|
|
730
|
+
if ("run" in result)
|
|
731
|
+
return result.run;
|
|
732
|
+
if (!result.objector)
|
|
626
733
|
return succeed(run, "review", "pr");
|
|
627
734
|
return {
|
|
628
|
-
...retry(run, "review", `程式碼審查要求修改:\n\n${issues.join("\n\n")}`, "fix"),
|
|
735
|
+
...retry(run, "review", `程式碼審查要求修改:\n\n${result.issues.join("\n\n")}`, "fix"),
|
|
629
736
|
fixSource: "review",
|
|
630
|
-
lastReviewer:
|
|
737
|
+
lastReviewer: result.objector,
|
|
631
738
|
};
|
|
632
739
|
}
|
|
633
740
|
async function prStage(run) {
|
package/dist/handoff.js
CHANGED
|
@@ -52,10 +52,9 @@ export function previewHandoff(ledger, callKey, source, response, role) {
|
|
|
52
52
|
const issue = issues.find((item) => item.id === disposition.id);
|
|
53
53
|
if (!issue)
|
|
54
54
|
throw new Error(`找不到交接事項:${disposition.id}`);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
throw new Error(`交接事項已結案:${disposition.id}`);
|
|
55
|
+
// 參考資訊沒有結案流程,已結案的事項也不需再處置:略過即可,不讓整個步驟因此重試
|
|
56
|
+
if (issue.kind !== "action" || issue.status === "resolved" || issue.status === "accepted")
|
|
57
|
+
continue;
|
|
59
58
|
if (role === "writer" && disposition.status !== "proposed_resolved")
|
|
60
59
|
throw new Error("作者只能提出已修正,不能自行結案");
|
|
61
60
|
if (role === "reviewer" && disposition.status === "proposed_resolved")
|
|
@@ -74,13 +73,20 @@ export function mergeHandoff(id, callKey, source, response, role) {
|
|
|
74
73
|
/** 只把目前步驟需要處理的事項投影給 agent。 */
|
|
75
74
|
export function prepareHandoff(id, _callKey, target, blind) {
|
|
76
75
|
const items = readHandoff(id).issues.filter((item) => item.targetStage === target && (item.kind === "info" || item.status === "open" || item.status === "proposed_resolved"));
|
|
77
|
-
const
|
|
76
|
+
const render = (item) => {
|
|
78
77
|
const source = blind ? "" : `\n來源:${item.source.stage}/${item.source.agent}`;
|
|
79
78
|
const resolution = item.resolution ? `\n處理理由:${item.resolution.reason}\n處理證據:${item.resolution.evidence}` : "";
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const status = item.kind === "action" ? `\n狀態:${item.status}` : "";
|
|
80
|
+
return `### ${item.id}:${item.summary}\n類型:${item.kind}\n證據:${item.evidence}${status}${resolution}${source}`;
|
|
81
|
+
};
|
|
82
|
+
const actions = items.filter((item) => item.kind === "action").map(render);
|
|
83
|
+
const infos = items.filter((item) => item.kind !== "action").map(render);
|
|
84
|
+
const sections = [
|
|
85
|
+
actions.length ? `## 待處理事項(action,可在 dispositions 處置)\n\n${actions.join("\n\n")}` : "",
|
|
86
|
+
infos.length ? `## 參考資訊(info,只供參考,不要放進 dispositions)\n\n${infos.join("\n\n")}` : "",
|
|
87
|
+
].filter(Boolean);
|
|
82
88
|
mkdirSync(flowDir(id), { recursive: true });
|
|
83
|
-
writeFileSync(join(flowDir(id), "handoff-context.md"), `# 待處理交接事項\n\n${
|
|
89
|
+
writeFileSync(join(flowDir(id), "handoff-context.md"), `# 待處理交接事項\n\n${sections.length ? sections.join("\n\n") : "目前沒有待處理事項。"}\n`);
|
|
84
90
|
rmSync(responsePath(id), { force: true });
|
|
85
91
|
}
|
|
86
92
|
export function validateHandoffResponse(id) {
|
package/dist/roles.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 決定每個步驟由哪個 agent 執行。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 規則只有四條:
|
|
5
5
|
* 1. 審查者不能是最後寫程式的 agent,多位審查者彼此不重複。
|
|
6
6
|
* 2. 開啟 tddSplit 時,同一個任務的測試與實作由不同 agent 負責。
|
|
7
7
|
* 3. 人選隨機決定,不依 cycle 的順序。cycle 只代表有哪些 agent 參與。
|
|
8
|
+
* 4. 任務的測試、實作、任務審查依同一個隨機順序輪流交換,讓各家用量平均。
|
|
8
9
|
*
|
|
9
10
|
* 隨機以 seed(run id 加上步驟與輪次)決定:同一個 run 的同一步驟重算時會得到同樣的人,
|
|
10
11
|
* 所以 resume、或同一步驟在不同地方重算時結果一致。
|
|
@@ -48,25 +49,30 @@ export function pick(cycle, seed, exclude = []) {
|
|
|
48
49
|
export const specAgent = (cycle, seed) => pick(cycle, `${seed}:author`);
|
|
49
50
|
export const planAgent = specAgent;
|
|
50
51
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
52
|
+
* 任務的角色輪流交換,讓各家用量平均:整個 run 用同一個洗好的順序,
|
|
53
|
+
* 第 i 個任務由 order[i] 寫測試、order[i+1] 寫實作、order[i+2] 做任務審查(都取餘數)。
|
|
54
|
+
* 三家時每三個任務各家剛好把三種角色各做一次;兩家時測試與實作每個任務互換。
|
|
53
55
|
*/
|
|
54
|
-
function taskBag(cycle, seed, bag) {
|
|
55
|
-
const order = shuffled(cycle, `${seed}:tasks:${bag}`);
|
|
56
|
-
if (bag === 0 || order.length < 2)
|
|
57
|
-
return order;
|
|
58
|
-
const prevLast = taskBag(cycle, seed, bag - 1).at(-1);
|
|
59
|
-
return order[0] === prevLast ? [...order.slice(1), order[0]] : order;
|
|
60
|
-
}
|
|
61
|
-
/** 第 i 個任務的測試作者從洗好的牌依序取;實作者隨機挑一位測試作者以外的 agent */
|
|
62
56
|
export function taskAgents(cycle, taskIndex, tddSplit, seed) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
57
|
+
const order = shuffled(cycle, `${seed}:tasks`);
|
|
58
|
+
const at = (offset) => order[(taskIndex + offset) % order.length];
|
|
59
|
+
const tests = at(0);
|
|
60
|
+
const code = tddSplit ? at(1) : tests;
|
|
61
|
+
// 兩家時沒有第三方,任務審查只能由實作者以外的測試作者負責
|
|
62
|
+
const review = order.length >= 3 ? at(tddSplit ? 2 : 1) : order.find((c) => c !== code) ?? code;
|
|
63
|
+
return { tests, code, review };
|
|
66
64
|
}
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
/**
|
|
66
|
+
* 隨機挑出 quorum 位彼此不重複、而且不是最後作者的 reviewer;任務審查優先避開測試作者。
|
|
67
|
+
* prefer 是輪到的審查者:只要不是最後作者就排第一位(任務修正後重審仍由同一位審查)。
|
|
68
|
+
*/
|
|
69
|
+
export function reviewers(cycle, lastWriter, quorum, seed, testAuthor, prefer) {
|
|
70
|
+
const candidates = shuffled(cycle.filter((c) => c !== lastWriter), seed);
|
|
71
|
+
const preferred = testAuthor ? candidates.filter((c) => c !== testAuthor) : candidates;
|
|
72
|
+
const fallback = testAuthor ? candidates.filter((c) => c === testAuthor) : [];
|
|
73
|
+
const ranked = [...preferred, ...fallback];
|
|
74
|
+
const first = prefer && ranked.includes(prefer) ? [prefer] : [];
|
|
75
|
+
const picked = [...first, ...ranked.filter((c) => c !== prefer)].slice(0, quorum);
|
|
70
76
|
return picked.length ? picked : [lastWriter ?? cycle[0]];
|
|
71
77
|
}
|
|
72
78
|
/**
|
package/dist/schemas.js
CHANGED
|
@@ -172,7 +172,10 @@ export const FlowRun = z.object({
|
|
|
172
172
|
/** 各關卡的連續失敗次數 */
|
|
173
173
|
attempts: z.record(z.string(), z.number()),
|
|
174
174
|
taskIndex: z.number().int().nonnegative(),
|
|
175
|
-
|
|
175
|
+
/** 目前任務進行到哪一步:寫測試 → 實作 → 審查 → 驗證,審查或驗證未通過時進入修正 */
|
|
176
|
+
taskPhase: z.enum(["tests", "code", "review", "verify", "fix"]),
|
|
177
|
+
/** 目前任務寫測試前的 commit,任務審查只看這之後的變更 */
|
|
178
|
+
taskBase: z.string().optional(),
|
|
176
179
|
testsCommit: z.string().optional(),
|
|
177
180
|
/** 目前任務的測試實際由誰撰寫(可能是代打) */
|
|
178
181
|
lastTestsAuthor: z.string().optional(),
|
package/package.json
CHANGED
package/prompts/fix.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<inputs>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<task>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<task>
|
package/prompts/plan-arbiter.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<requirement>
|
package/prompts/plan-fix.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<requirement>
|
package/prompts/plan-review.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<requirement>
|
package/prompts/plan.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<inputs>
|
package/prompts/review.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<inputs>
|
package/prompts/spec.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
新增事項格式:{ "kind": "action 或 info", "summary": "具體問題", "evidence": "檔案位置或檢查證據", "targetStage": "plan 或 code" }。
|
|
17
|
-
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }
|
|
17
|
+
處置格式:{ "id": "既有事項 ID", "status": "proposed_resolved、resolved 或 accepted", "reason": "具體處理理由", "evidence": "檔案、commit 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
18
|
</handoff>
|
|
19
19
|
|
|
20
20
|
<requirement>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<role>
|
|
2
|
+
你是任務審查者({{reviewer}}),負責在單一任務完成後、進入下一個任務前,獨立審查這個任務的變更。本任務的作者:{{authors}}。若你曾撰寫本任務的測試,仍須重新檢查測試是否真正驗證驗收條件;不要預設測試或實作正確。
|
|
3
|
+
</role>
|
|
4
|
+
|
|
5
|
+
<context>
|
|
6
|
+
目前的工作目錄就是專案(agentflowctl 為這次任務建立的專用 git worktree)。這個任務的測試已由外部流程確認通過;完整的自動化檢查會在你審查之後才執行,所有任務完成後還有一次整體審查。
|
|
7
|
+
</context>
|
|
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 或檢查結果" }。只有「待處理事項」(action)可以處置:撰寫者只能用 proposed_resolved 提出修正;審查者可以用 resolved 或 accepted 結案。「參考資訊」(info)只供參考,不要放進 dispositions。重要疑慮必須放在這份檔案,不能只寫在回覆的 <concerns>。
|
|
18
|
+
</handoff>
|
|
19
|
+
|
|
20
|
+
<inputs>
|
|
21
|
+
<task>
|
|
22
|
+
{{task}}
|
|
23
|
+
</task>
|
|
24
|
+
|
|
25
|
+
<acceptance>
|
|
26
|
+
{{acceptance}}
|
|
27
|
+
</acceptance>
|
|
28
|
+
|
|
29
|
+
- 本任務的變更:.flow/diff.patch(先看變更,再按需讀相關程式碼與測試)
|
|
30
|
+
- 規格與計畫:.flow/spec.md、.flow/plan.md(任務或驗收條件不清楚、互相矛盾時,才查相關段落)
|
|
31
|
+
</inputs>
|
|
32
|
+
|
|
33
|
+
<review_focus>
|
|
34
|
+
1. 逐條確認上面每個驗收條件是否真的被實作,而且有對應的測試真正驗證它(不是空洞的測試)。
|
|
35
|
+
2. 是否有明顯的錯誤、邊界情況遺漏、安全問題或效能問題。
|
|
36
|
+
3. 是否符合專案既有的架構與慣例,以及任務說明的範圍(沒有做到一半,也沒有做了其他任務的事)。
|
|
37
|
+
|
|
38
|
+
只審查本任務的變更;其他任務的驗收條件不在這次審查範圍內。先根據 diff 與驗收條件定位需要查閱的檔案,只在證據不足時讀取其他檔案。不必為了審查重跑全套檢查。
|
|
39
|
+
|
|
40
|
+
風格偏好與無關緊要的小問題不需要要求修改。
|
|
41
|
+
</review_focus>
|
|
42
|
+
|
|
43
|
+
<output_format>
|
|
44
|
+
寫入 .flow/review.json:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"verdict": "changes_requested",
|
|
49
|
+
"items": [
|
|
50
|
+
{ "criterion": "AC-1", "status": "not_met", "note": "src/form.tsx 缺少 API 失敗時的錯誤訊息,請在 catch 中顯示錯誤並補測試" }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `verdict`:逐條核對本任務的驗收條件後,全部通過且沒有嚴重問題時為 `approve`,否則為 `changes_requested`。
|
|
56
|
+
- `items` 只列未通過的驗收條件與額外發現的重要問題,每筆的 `status` 為 `not_met` 或 `partial`,`note` 請寫出具體位置與修正方向。
|
|
57
|
+
- `approve` 時 `items` 為空陣列;`changes_requested` 時至少要有一筆。兩者不一致會被視為格式錯誤並重新審查。
|
|
58
|
+
</output_format>
|
|
59
|
+
|
|
60
|
+
<constraints>
|
|
61
|
+
- 只能寫入 .flow/review.json 與 .flow/handoff-response.json,不可修改任何程式碼,其他變更都會被捨棄。
|
|
62
|
+
</constraints>
|
|
63
|
+
|
|
64
|
+
<reply_format>
|
|
65
|
+
完成後,回覆的最後必須附上以下 XML 中繼資料(只附一次,標籤名稱不可更改):
|
|
66
|
+
|
|
67
|
+
```xml
|
|
68
|
+
<result>
|
|
69
|
+
<status>done 或 blocked</status>
|
|
70
|
+
<summary>一兩句說明這次做了什麼;blocked 時說明卡在哪裡</summary>
|
|
71
|
+
<files_changed>
|
|
72
|
+
<file>每個新增或修改的檔案路徑各一行</file>
|
|
73
|
+
</files_changed>
|
|
74
|
+
<concerns>對需求、規格、計畫或測試的疑慮;沒有就留空</concerns>
|
|
75
|
+
</result>
|
|
76
|
+
```
|
|
77
|
+
</reply_format>
|