狀態:核心已實作(2026-07-28)· 實作見本 repo 的
src/Backlog.md 1.48.0 已實測,本文件的欄位與行為描述已依實測結果修正。驗證細節見 §17。 實作過程中被實跑推翻或補上的設計,已回寫進對應章節並標記 ⚑。落地進度見 §18。
一個 task queue,導入多 agent 執行。把已經談定的工作固化成工單,逐一排空;每張工單由一組分工明確的 agent 產線完成。
跑在 Claude Code CLI 之上,因此使用既有的 Claude 訂閱額度,不需要額外的 API 計費。這是相對於其他 agent 編排系統的主要差異點。
- 不是看板工具(看板用現成的 Backlog.md)
- 不是聊天機器人(Discord bot 是介面的使用者,不是本體)
- 不含「agent 能力養成」機制 —— 那是獨立的另一個 project,本系統只負責產生它需要的原料
這幾條貫穿整份設計,遇到取捨時以它們為準。
| # | 原則 |
|---|---|
| 1 | 保護在機制層,不在約定層。 不可逆的動作由 runner 環境封死,不靠 prompt 拜託 agent 遵守。 |
| 2 | 入口愈開放愈好,閘門愈嚴愈好。 品質不靠限制誰能建卡來保證,靠閘門檢查。 |
| 3 | 「有沒有做到」用機械判定(測試逐條結果),可以是硬門檻;「做得好不好」用判斷力(agent),只能是參考。 |
| 4 | 同一時間只有一個 owner。 卡的所有權隨狀態轉移,因此不需要鎖或衝突仲裁。 |
| 5 | 卡的大小上限不是「機器做得完」,是「你一次看得完」。 瓶頸是人的 review 頻寬。 |
| 6 | 不猜。 填不出來的欄位留白並標記,不自行補齊。 |
┌─────────────────────────────────────────────────────┐
│ 入口(多元)—— 不屬於本系統,本系統只定義介面 │
│ Discord bot / Telegram / API / Claude Code session │
└───────────┬──────────────────────────▲──────────────┘
指令 │ │ 事件(即時)
┌───────────▼──────────────────────────┴──────────────┐
│ 常駐服務 `taskcrew serve` │
│ · 收指令(立刻跑 / 幾點跑)· 管 queue │
│ · 驅動產線 · 寫回 board · 推事件 │
└───┬──────────────────┬──────────────────────────────┘
│ │
┌───▼────┐ ┌─────────▼─────────────────────────────┐
│Backlog │ │ Redis │
│ .md │ │ LIST 指令佇列(重啟不掉) │
│ │◄──┤ LIST 派工 / 回覆(一對一,可靠) │
│卡的本體 │ │ PUB 產線事件(一對多,掉了無妨) │
│(git) │ └──┬────┬────┬────┬───────────────────────┘
└────────┘ │ │ │ │ 每個 key 前綴帶 board 命名空間
┌───▼┐ ┌─▼──┐ ┌▼───┐ ┌▼──┐
│ PM │ │jun │ │sen │ │QA │ 常駐 agent(各自獨立 process)
└────┘ └────┘ └────┘ └───┘
| 層 | 存什麼 | 為什麼放這裡 |
|---|---|---|
| Backlog.md(markdown in git) | 卡本身:內容、狀態、驗收條件、plan、回報摘要 | 純文字、可 diff、有版本、人看得懂、離線可用 |
| git 分支 ⚑ | 每張卡的成果在哪:task/<id> |
下游卡要引用上游成果,git 本來就是存這個的 |
| Redis | 指令佇列、派工與回覆、產線事件 | 常駐多 agent 需要訊息匯流排;即時回推也走這裡 |
| Postgres | 執行歷史:每次嘗試、每輪的結構化紀錄 | 需要查詢與長期保存;也是「養成」project 的原料 |
⚑ 產線運轉所需的狀態,全部在 markdown 和 git 裡 —— 兩者你都讀得懂、改得動。Postgres 存的是歷史分析,是查詢用的,不是運轉的必要條件。這條界線要守住:如果哪天產線非有 DB 才跑得動,DB 一掛整套就停,而且你沒辦法靠編輯看板把事情喬回來。
兩個身份(工作 / 個人)各自獨立:
| 身份 A(例如工作) | 身份 B(例如個人) | |
|---|---|---|
| board repo | 各自一個 | 各自一個 |
| queue | 各自一個 | 各自一個 |
| runner | 各自一支 | 各自一支 |
兩邊可以同時執行,因為負責不同專案、不同 repo,天然不干擾。隔離是目錄與 repo 層級的,不是靠規則約束。
第一版的 Redis key 是全域的。結果:背景還活著的真 agent 撿走了測試發出的派工,用真模型跑了測試資料(浪費約 $0.50)。共用一個 Redis instance 就等於共用一個匯流排,目錄隔離管不到這一層。
修法是每個 key 前綴一段 board 命名空間 —— board 絕對路徑的 sha256 前 12 碼:
tc:{a3f9c1d0e5b7}:commands tc:{a3f9c1d0e5b7}:req:junior tc:{a3f9c1d0e5b7}:events
不同 board 的訊息在 Redis 層就到不了對方,不靠任何約定。這條有自己的測試,也做過變異驗證。
加入第二個身份時是「複製一份設定」,不改架構。
開卡 → 需求討論 → 規劃中 → 設計待批准 → 待執行 → 執行中 → 執行完成回報 → 完成
↑ ↑ ↑ │
└───────────┴──────────┴────── 不滿意,拖回來再談一輪 ←──────┘
阻塞 ← 等依賴/子卡產出成果(可從「待執行」轉入)
| # | 狀態 | 球在誰手上 | 離開條件 |
|---|---|---|---|
| 1 | 開卡 |
你 / agent | 開始談 |
| 2 | 需求討論 |
你 + agent | 你確認拆解(審查點 1) |
| 3 | 規劃中 |
PM | plan 產出 |
| 4 | 設計待批准 |
你 | 你批准 plan(審查點 2)→ 過閘門 |
| 5 | 阻塞 |
— | 依賴與子卡都產出成果(見 §4.2) |
| 6 | 待執行 |
— | 你下指令,runner 取走 |
| 7 | 執行中 |
runner | 產線跑完 |
| 8 | 執行完成回報 |
你 | 你驗過 → 完成;不滿意 → 拖回 2 或 4 |
| 9 | 完成 |
— | 終點 |
- 審查點 1(需求討論) 審拆解對不對 —— 快,看標題和驗收條件就能判斷
- 審查點 2(設計待批准) 審做法對不對 —— 需要讀 plan
分開的價值:拆解錯了只浪費幾分鐘,不會浪費一整輪設計。而且「設計待批准」是看板上唯一「球在你手上、不動就全卡住」的位置,一眼看得出來。
| 卡的狀態 | owner | 誰能改 |
|---|---|---|
| 討論中 / 阻塞 / 待執行 | 你 | 你隨時可以拖回討論 |
| 規劃中 / 執行中 | agent | 對你唯讀 |
| 執行完成回報 | 你 | 結果已寫回 |
因為 runner 沒有你的指令不會執行,queue 裡的卡是靜止的 —— 隨時拖回都安全。唯一的衝突窗口只有「正在執行的那一張」,而 runner 逐筆序列執行,一次只有一張。因此不需要鎖、同步器或真實來源仲裁。
實作限制:Backlog.md 的 web UI 大概無法「鎖住」執行中的卡。到時候要嘛靠約定,要嘛 runner 偵測到被改動就拒絕採用結果。
一份需求拆成三張卡之後,有一段工作是沒有人負責的:把三張的成果合起來、跑只有合起來才驗得到的整體驗收。父卡就是負責那一段的卡。
TASK-7 父卡:整合 + 整體驗收 ← 等 7.1/7.2/7.3 都產出成果
├── TASK-7.1 子卡 A
├── TASK-7.2 子卡 B(依賴 A)
└── TASK-7.3 子卡 C(獨立)
用 Backlog.md 原生的 parent_task_id 與階層 ID,不自訂欄位。
三個機制:
| 機制 | 做什麼 | 為什麼是這樣 |
|---|---|---|
成果分支 task/<id> |
卡成功後把這個名字指到那次的 attempt 分支 | 下游要引用上游成果,需要一個不隨重試而改變的名字(attempt-1/2/3 會變) |
| 擋板檢查子卡 | 父卡在所有子卡產出成果之前一律 阻塞 |
沒有東西可整合的時候,父卡做不了它唯一的工作 |
| 開工前先合 | 父卡在 agent 動手前把子卡成果逐一 merge 進來 | 衝突留給 agent 解。解衝突本來就是整合工作的內容,不是意外 |
有依賴的卡(非父子關係)則直接從被依賴那張的成果分支長出來,而不是從 base_branch。
原本寫成 git merge --no-commit --no-ff 的迴圈。那個寫法只合得進第一張子卡:第一次合併留下未結束的合併狀態,第二個 git merge 會被 git 直接拒絕。
最糟的是它悄悄失敗 —— agent 拿到一個看起來正常、但少了東西的工作區,然後在錯的基礎上工作。父卡有兩張以上子卡就會踩到,而三張子卡是拆解的常見形狀。
你隨時可以把一張卡拖到 完成。那張卡就沒有 task/<id> 分支。這是正常情境,不是壞掉 —— 那時退回 base_branch,並發出 missing-ref 事件讓你知道 plan 可能假設了不存在的東西。悄悄失敗和直接當掉都不對。
原本的規則是「依賴要到 完成(你驗過)」。實跑才看出代價:一份拆成四張卡的需求會需要你醒來四次。晚上掛著跑,早上看到的是「做完 2 張、卡住 2 張」——跟 §7.3 寫的「跑到撞額度」直接衝突。
當初的顧慮(地基是錯的,下游白做)沒錯,錯在權重:
| 代價 | 有多確定 | |
|---|---|---|
| 繼續做,結果地基被你退回 | 一張卡的 token,分支還留著 | 不一定會發生 |
| 停下來等人 | 整晚的產能 | 一定會發生 |
現行規則:
可以讓下游開工 = status 是「完成」
或(status 是「執行完成回報」且有成果分支且沒標 require_review)
為什麼要多看一次成果分支:失敗的卡也停在「執行完成回報」——那一欄的意思是「球在你手上」,不是「成功了」。成果分支只在產線通過時才會被指過去,是唯一機械可查的證據。
「完成」則不查分支,那是你親口說的,而且手動拖過去的卡本來就沒有分支。
require_review: true(Runner Config 的選填欄位)退回舊行為。留這個開關是因為「這張是別人的地基,錯了下游全廢」確實存在,只是它是少數——不該讓多數去遷就它。
同一條規則也用在規劃階段,不只執行。一張卡要建立在別人的產出上,而那些產出還不存在的話,PM 只能瞎猜。
「依賴」和「子卡」是同一件事的兩種形狀 —— 依賴是「我要用你做出來的東西」,子卡是「我要把你們做出來的東西合起來」。兩者用同一條規則(gate.notReady()),規劃和執行共用同一份判斷。
這條是分兩次撞出來的,兩次症狀一模一樣,只是換了個入口:
| 撞到 | PM 的回報 |
|---|---|
| 父卡與子卡同批送規劃 | 「三個子模組完全不存在,請決定…」 |
| TASK-1.2 與它依賴的 TASK-1.1 同批送規劃 | 「src/parse.js 在這個分支不存在,需要決定…」 |
兩次 PM 的判斷都對,但那要花一次 Opus 呼叫($0.24 上下)才換到一句我們早就知道的話,而且卡會被退回「需求討論」,看起來像出了問題。
一份四張卡、含一條依賴鏈的需求,會收斂成三輪 規劃 → 批准 → 排空。
值得,理由是在上游真的產出東西之前,那份 plan 只能是猜的 —— 而你批准一份猜測等於沒有審查。PM 拒絕產出它,跟核心原則 6「不猜」是一致的。
注意這不影響「掛著跑一整晚」:擋的是規劃,不是執行。排空階段仍然一路跑到 queue 空或撞額度(§4.2)。
⚠️ 實測結果:Backlog.md 會在每次寫入時重寫 frontmatter,並刪除它不認識的欄位。 因此自訂欄位不能放 frontmatter,必須放內文區塊(見 §5.2 的 Runner Config)。
---
id: TASK-178 # 大寫;子卡用點號階層,如 TASK-175.1
title: webhook 重送時查去重表 # 動詞開頭,一句話
status: 設計待批准 # 九個自訂狀態之一(config.yml 可設)
assignee: []
labels:
- no-auto # 唯一有意義的 label
dependencies: [] # 要等哪些卡產出成果
parent_task_id: TASK-175 # 父卡;層數不限
ordinal: 2000 # 排序
priority: medium
created_date: '2026-07-27 06:55'
updated_date: '2026-07-27 06:56'
---| 欄位 | 說明 |
|---|---|
parent_task_id |
原生支援,層數不限。ID 自動階層化(TASK-1 → TASK-1.1 → TASK-1.1.1),終端看板畫出樹狀結構。三層的用法是「模組 → 功能 → 子任務」,機制上不需要新東西:每層只等自己的直接子卡、只整合自己的直接子卡,遞迴自然組合(實測驗證) |
dependencies |
解除條件見 §4.2 —— 預設「跑完且通過」就放行,不等人驗。要等人驗的地基另外標 require_review |
labels: no-auto |
唯一有意義的 label:「這張我要自己來」 |
status |
九個狀態在 backlog/config.yml 的 statuses 陣列定義,支援中文 |
Backlog.md 用 HTML 註解標記界定它認識的區塊(<!-- SECTION:DESCRIPTION:BEGIN -->、<!-- AC:BEGIN --> 等),未知的區塊會被完整保留(實測驗證)。所有自訂內容都放這裡。
| 區塊 | 誰寫 | 什麼時候 |
|---|---|---|
## Runner Config |
你 + agent | 需求討論(取代原本規劃的自訂 frontmatter) |
## Description |
你 + agent | 需求討論 |
## Acceptance Criteria |
你 + agent | 需求討論 |
## Implementation Plan |
PM | 規劃中 |
## Excluded Approaches |
PM(累積) | 每次換方案時追加 |
## Implementation Notes |
runner | 執行完成回報 |
## Runner Config
<!-- RUNNER:BEGIN -->
```yaml
project: ~/code/some-repo
base_branch: main
verify: "pnpm test -- webhook --reporter=json"
autonomy: propose
```
<!-- RUNNER:END -->| 欄位 | 必填 | 說明 |
|---|---|---|
project / base_branch |
✅ | 閘門檢查存在性 |
verify |
✅ | 必須產出逐條結果,不能只有 exit code —— 這是失敗分類的唯一客觀依據 |
autonomy |
✅ | none / propose / replan:N / free,見 §8.3 |
require_review ⚑ |
— | 預設 false。標 true 代表下游要等你親自驗過才准開始,見 §4.2。少數卡才需要,不該逼每張卡都寫 |
用自己的 RUNNER:BEGIN/END 標記包起來,理由跟 Backlog.md 用標記一樣:讓 runner 能精準定位並重寫這一段,不會誤傷相鄰內容。
## Description
**背景**
(為什麼要做這件事)
**要做什麼**
(具體、可驗證的描述)
**不要做什麼** ← 必填,閘門檢查此段存在
- 不要順手重構 xxx
- 不要動 yyy 模組
**已知的坑 / 已排除的做法** ← 選填但強烈建議
(防止重走白天已經否決過的路)
**參考點** ← 選填
(類似的既有 code 在哪,讓風格一致)「不要做什麼」是必填的。無人看管時,這一段擋掉的災難比任何其他欄位都多。
每一條必須對應到至少一個可執行的 test case。
## Acceptance Criteria
- [ ] 重送時,已處理過的事件 ID 不會被再次處理
→ `test/webhook.test.ts::skips-duplicate-event`
- [ ] 去重表滿了之後,最舊的紀錄被淘汰
→ `test/webhook.test.ts::evicts-oldest`閘門直接檢查「每條驗收條件有沒有掛對應的測試」,不靠判斷「這條夠不夠可驗證」。
寫不出對應測試的條件,就不是驗收條件,是願望。
## Excluded Approaches
### 方案 1:在寫入時做 upsert(attempt-1,已排除)
**為什麼不行**:測試 `evicts-oldest` 從第一輪就全掛,因為 upsert 無法表達淘汰順序
**分支**:`task/178-attempt-1`累積,不覆蓋。 兩個用途:PM 換方案時的排除依據;你判斷「要不要繼續放手」的材料。
## Implementation Notes
**方案 2(attempt-2)· 成功**
- 分支:`task/178-attempt-2`
- 動到:`src/webhook/dedupe.ts`(新增)、`src/webhook/handler.ts`(修改)
- 測試:4/4 通過
- senior 減法:移除 2 個未使用的抽象、1 個防禦性 try/catch
- QA:符合要求
- 花費:$0.84人話摘要給人看;結構化資料在 Postgres。
父卡與子卡共用同一份 schema,差別只在三處:
| 父卡 | 子卡 | |
|---|---|---|
parent |
空 | 指向父卡 |
Acceptance Criteria |
整體驗收 —— 只有合起來才驗得到的東西 | 該子任務自己的 |
Implementation Plan |
整合方案 —— 怎麼把子卡的分支併起來 | 該子任務的做法 |
父卡在所有子卡到 完成 之前處於 阻塞;解除後由 runner 執行整合 + 整體驗收。父卡的 verify 只跑「整合後才驗得到」的那些,不重複跑子卡的測試。
依模組邊界與需求本身的結構來切。沒有機械規則,也沒有預設張數。
參考訊號(命中時值得停下來想一下,但不是規則):
- 驗收條件超過 5 條
- 描述裡出現兩個以上的「而且」
- 動到兩個以上不相關的模組
- 預期 diff 會超過你一次看得完的量
context 容量在某些任務(例如全庫掃描)會成為拆解的實際限制,但依情境判斷,不寫成規則。
卡從 設計待批准 移往 待執行 時,runner 檢查七項;缺一不准過。
| # | 檢查 |
|---|---|
| 1 | project 存在 |
| 2 | base_branch 存在 |
| 3 | Description 含「不要做什麼」段落 |
| 4 | Acceptance Criteria 非空,且每條掛了對應的 test case |
| 5 | verify 非空 |
| 6 | Implementation Plan 非空 |
| 7 | dependencies 與子卡都已產出成果(見 §4.2;否則轉 阻塞,不是退回) |
| 8 | ⚑ 沒有留著 taskcrew new 的佔位文字(⟨⟩ 標記) |
| 9 | ⚑ 驗收條件引用的測試真的存在(跑一次 verify 拿真實清單比對) |
⚑ 第 9 項擋的是建卡者的手誤。 測試名是人手打進驗收條件的,打錯一個字
scopeToCard 就對不上,於是悄悄退回整套測試 —— 那張卡從此被別張卡的紅字
綁住、永遠過不了,而錯誤訊息完全看不出原因。這是建卡時就該發現的事,
不該等到執行才無聲退化。
⚑ 第 8 項擋的主要不是人,是建卡的 agent。 骨架的佔位文字自己會滿足其他檢查 —— 「不要做什麼」那段的說明裡就寫著 **不要做什麼**,驗收條件的範本裡就有 → 測試引用。第一版沒有這項,結果一張完全沒填的卡被判合格,agent 拿到「⟨具體、可驗證的描述⟩」當需求、對著一個不存在的測試名工作。樣板讓閘門變弱了,那比沒有樣板還糟。
它同時逼出正確的行為:需求裡沒講驗收怎麼測時,建卡的人(或 agent)應該把它留白讓閘門擋下來,而不是自己編一個測試名。
第 6 項不需要 approved: true 欄位 —— 卡被移出 設計待批准 這件事本身就是批准。欄位位置即狀態。
閘門不管卡從哪裡來。 入口可以是 Discord、Telegram、API、Claude Code session、web UI 手動建立、甚至別人寫腳本灌進來 —— 品質全部押在這裡。
runner 是一個一直活著的服務,不是定時喚醒的腳本。理由:多入口(Discord/TG/API)本來就需要一個常駐的東西在接收,服務式多 agent 也需要常駐 agent 互相溝通。既然常駐非有不可,就讓它把執行也一併做了。
代價:需要處理崩潰復原。撞限或崩潰時,該卡整張退回 待執行,分支留著,紀錄寫到停的那一刻。下次重新取出時從頭開始那一輪,不做中斷點續跑。
四個角色各跑一個 process,在 Redis 上等派工。這是量測過而不是估的:
| 記憶體 | |
|---|---|
| 常駐 worker × 4 | ~130 MB 每支(薄的 Node process;真正吃記憶體的 claude 行程是短暫的,~370 MB,只在做事時存在) |
| Redis | 7 MB |
| serve | ~130 MB |
| 合計 | ~782 MB(實測) |
量測機器是一台 8 GB 記憶體的桌機,沒有跑圖形介面。全部常駐之後空間仍然充裕,所以記憶體不是限制因素,不需要「用到才叫起來」那種按需啟動的複雜度。
真正的好處不是省資源,是 §8.4 的 session 延續:常駐的 agent 才有「同一段對話一直接下去」可言。
PM 是自己的一個 process,不是你正在講話的這個 session。
理由:跟你討論的 session 會被別的話題塞滿、會被清掉、會換 context。PM 需要的是對某個 repo 的穩定累積 —— 它上次為什麼那樣拆、它試過什麼、你退回過什麼。這兩種需求放在同一個 session 裡會互相破壞。
分開之後,你的對話層負責「跟你談清楚」,PM 負責「把談清楚的東西變成 plan 並且記得住」。
沒有常設排程表。每次執行都是一個明確指令。
| 指令形式 | 能表達 |
|---|---|
| 「現在跑 」 | 立刻開始 |
| 「凌晨兩點跑 」 | 指定時間 |
指令必須指定跑哪個 queue。
兩種下指令的方式,共用同一個機制:
| 方式 | 能表達 | 用在什麼時候 |
|---|---|---|
| 跟 agent 講一句 | 時間 + 哪個 queue | 要指定時間,或人不在看板前 |
| 拖看板上的控制卡 | 「現在跑」(queue 由卡所在的 board 決定) | 手機上想立刻跑,最快 |
卡進 queue 之後只是排隊,不會自己開始。
只有兩個:queue 空了,或撞到訂閱額度。
不設卡數上限、不設時間上限、不設花費上限。一律只吃訂閱額度,不使用 API 計費。
因此「撞限時乾淨停止」是承重路徑:必須確保被擋在半路的卡退回 待執行、分支留著、下次接得回去。
PM 規劃和 RD 執行必須看到同一個世界。 PM 依據 A 寫 plan、RD 站在 B 執行,那份 plan 就是憑空寫的。
原本只有執行階段會準備環境(算出 baseRef、父卡再合入子卡成果),規劃階段完全沒有 —— PM 就在 repo 當下剛好停在的分支上研究,而那是上一張卡隨機留下的狀態。
實跑撞到的樣子:PM 規劃父卡時 repo 停在 task/task-1.2-attempt-1,它看到兩個子模組、看不到第三個,於是停手回報「truncate 不存在」。它的觀察正確,錯的是我沒把它放在對的地方。
後果一層比一層嚴重:
| 問題 | |
|---|---|
| 1 | 不確定性 —— 同一張卡重跑兩次可能得到不同的 plan |
| 2 | 父卡根本規劃不了 —— 它從來看不到三個子模組同時存在的狀態 |
| 3 | PM 和 RD 不一致 —— plan 是對著一個 RD 不會站的世界寫的 |
規劃是唯讀的工作 —— PM 不寫 code,沒有理由為了讀而改變 repo 的狀態。直接 checkout 只是把「現在停在哪」的問題往後推一格。
plan: git worktree add --detach <tmp> <baseRef> → 合入子卡成果 → PM 進去研究 → 拆掉
run : git checkout -B task/<id>-attempt-N <baseRef> → 合入子卡成果 → RD 動手
兩邊的 baseRef 與合併清單由同一份程式碼算出來,這是「看到同一個世界」的唯一保證方式。
驗證方式也直接:實跑時 PM 在合併好的 worktree 裡跑了 npm test,回報「6 pass / 1 fail,唯一失敗的是 format-integration」,並自己用 node 驗過資料流。那是它真的站在整合後的狀態才說得出來的話。
這條的理由不是整潔,是跨卡污染:
git checkout -B 新分支 base在工作區有未提交變更時,會把那些變更一起帶過去。
於是一張失敗的卡留下的殘骸,會出現在下一張卡的分支上,而且沒有任何跡象。
所以每張卡結束時(通過、失敗、撞額度都一樣):先把殘骸 commit 在它自己的分支上,再 checkout 回 base_branch。
commit 殘骸同時滿足 §9.4「失敗的分支全部保留」的原意 —— 方案 A 的殘骸在方案 B 也失敗時是關鍵線索,丟掉它等於丟掉你判斷「要不要繼續放手」的依據。原本的做法(完全不 commit、留在工作區)看起來也保留了殘骸,但它保留的位置是錯的。
runner 逐筆取出、一次做一張。因此沒有並發,也就沒有一致性問題。
兩個身份的 runner 可以同時跑(不同 repo,不干擾),但各自內部序列。
| 角色 | 模型 | 職責 |
|---|---|---|
| PM | Opus | 研究 codebase,產出 Implementation Plan;換方案時重新規劃 |
| junior RD | Sonnet | 開發 |
| senior RD | Opus | 常態:對每張卡做減法 review;例外:junior 做不出來時接手開發 |
| QA | Haiku | 判定符不符合要求 —— 讓結果不只是 0 與 1 |
每個角色是獨立的 agent 定義,差別在 model 與 thinking mode(claude --model / claude --effort,兩者都是現成的 CLI 參數)。
PM → Implementation Plan → 你批准(審查點 2)
↓
junior RD → 開發
↓
測試 → 逐條結果(先確認它能動)
↓
senior RD → 減法 review(帶複雜度量測當參考)
↓
測試 → 再跑一次(確認減完還是能動)
↓
QA → 符不符合要求
減法放在第一次測試之後 —— 先讓它能動,再讓它變乾淨。順序反過來的話,senior 會在一份還不確定對不對的 code 上做減法。減完必須再測一次,那是防止減過頭的唯一保險。
模型會不自覺地加東西:多餘的抽象、沒被要求的功能、對不可能情況的防禦。目標是產出的 code graph 複雜度不要過高。
因為每張卡都經過 senior,senior 成為品質的單一控制點 —— 調它的 prompt 就等於調整整個系統的輸出風格,不用改 junior、不用改 PM、不用逐張卡下指令。
複雜度量測是參考資料,不是硬門檻。 有些情境耦合度就是高,硬拆只會造成更多悲劇。量測結果餵給 senior 讓它知道自己減得夠不夠,但要不要減、減哪裡由它判斷。
先講清楚單位:下面的金額是 Claude Code CLI 回傳的 total_cost_usd,
意思是「如果走 API 計費會是多少」。taskcrew 驅動的是使用者本機已登入的
Claude Code,吃訂閱額度、不產生 API 帳單(§7.3)。所以這些數字是
額度消耗的指標,不是支出。真正的限制是撞到訂閱上限,不是錢。
兩組實測,對比很有意思:
| 卡數 | 等價消耗 | 每張 | |
|---|---|---|---|
| e2e(合成的小題目,含父子卡整合) | 4 張 | $2.38 | ~$0.6 |
| 真實工作(Gina 的新聞彙整) | 2 張 | $5.55 | ~$2.8 |
差了快五倍,而卡數還比較少。 所以估額度不能用「幾張卡」算,要用卡的份量—— e2e 那些是幾行的純函式;真實那兩張要讀 408 KB 的測試 fixture、既有的服務程式碼, 還要判斷現成的函式能不能重用。
粗略的估法:
| 情境 | 等價消耗 |
|---|---|
| 順利的一張卡(規格清楚、一輪過) | $2.5–3 |
| junior 做不出來、senior 接手 | +$1 上下(多一輪 Opus xhigh) |
| PM 修正一次 | +$1 上下 |
| 換方案(從頭來) | 大約翻倍 |
貴的永遠是 PM 與 senior(兩個都是 Opus + xhigh)。junior(Sonnet)與 QA(Haiku) 幾乎不影響總量。真實那兩張卡裡,規劃佔了 36%,而規劃不產出任何程式碼。
n=2,這只是參考。跑過五六張真實的卡之後那個數字才站得住。
- 規格寫得愈死,PM 需要的 effort 愈低。 建卡者若已經把介面骨架和驗收測試寫好,
PM 的工作就從「設計」變成「查證」,那不需要最高檔。2026-07-29 起 PM 的
effort由xhigh調成high,就是為了測這件事——觀察指標是「plan 還抓不抓得到坑」 (例如「>寫成>=會讓排程在整點無限自我觸發」「res.ok不檢查會變成 抓取成功但 0 則新聞」這類)。抓得到就維持,開始漏了再調回去。 - 卡切小。 一張大卡失敗一輪的代價,比兩張小卡各跑一輪貴。
測試給的是 0/1;QA 給的是「符不符合要求」。這兩件事本來就該分開,Haiku 補的正是機械測試給不出來的那個維度。
| 層 | 在解決什麼 | 誰在動 |
|---|---|---|
| 內層 | 實作沒寫對 | junior → senior 接手 |
| 中層 | 做法本身不對 | PM 重新規劃,換方案 |
| 外層 | 需求或方向不對 | 你 |
輪 1 junior RD (Sonnet) → 測試 → senior 減法 → 測試 → QA
↓ 沒過
輪 2 senior RD (Opus) → 接手開發(保留 junior 的 code,在上面修)
↓ 還是沒過
中層 PM REVISE —— 同一個方案,補上沒講清楚的部分(帶著 context)
↓ 修正過還是沒過
中層 PM REPLACE —— 換一個方案(乾淨 context)
↓ 次數用盡
外層 標 failed,停在「執行完成回報」等你
三層的成本與能力同時遞增,而且每次升級都真的換了東西,不是同一個東西再試一次。
原本中層只有「換全新方案」一種。但很多失敗的成因是沒確認清楚,不是方案錯 —— 那時整份丟掉重來是浪費,而且會把方案裡本來對的部分一起丟掉。
| REVISE | REPLACE | |
|---|---|---|
| 做什麼 | 調整現有做法:補漏掉的步驟、修錯的假設 | 換一個結構不同的方案 |
| PM 的 session | 延續 —— 它記得自己為什麼那樣寫 | 重置 —— 要的正是不被原方案錨定 |
| 原本的 code | 保留 | 丟掉,從 base_branch 乾淨開始 |
| 什麼時候用 | 失敗形狀是「同一批卡住」(碰不到那批條件) | 「震盪」或「第一輪就全掛」(結構有問題) |
session 帶不帶,就是這一級和下一級唯一的實質差別,而且是設計的核心:延續給的是熟悉度,重置給的是不被錨定。兩種都需要,所以分成兩級而不是二選一。
claude --resume <sessionId> 讓一個 agent 接著上次的對話繼續。這件事有正反兩面,所以不能一律開或一律關:
| 延續 session | 不延續 | |
|---|---|---|
| 得到 | 對這個 repo 的熟悉度:知道慣例、知道測試怎麼跑、不用重新摸索 | 不被上一輪的思路綁住 |
| 失去 | 錨定:context 裡塞滿它剛寫的 code,會傾向產出原方案的變體 | 每次從零,重複付出摸索成本 |
規則:做同一件事就延續,換一件事就重置。 REVISE 延續、REPLACE 重置;PM 對同一個 repo 的日常規劃一路延續。
這裡曾經想做「把 PM 的慣例外部化成筆記檔」。那是多餘的 —— PM 的 session 不重置,慣例自然就延續了。多一層筆記只是多一個會跟事實不同步的東西。
自主程度是卡上的參數,不是全域設定 —— 因為這需要長時間磨合,磨合的前提是可調。
| 值 | 內層失敗、需要換方案時 |
|---|---|
none |
停。標 failed |
propose |
PM 產新 plan 寫回卡上,卡退回 設計待批准,不執行 |
replan:N |
PM 產新 plan 並直接執行,最多 N 次 |
free |
換到成功或撞額度 |
預設 propose。 保守的起點,而且就算不放行執行,你早上也會看到 agent 想出的新方案 —— 那本身就是磨合的素材。
verify 產出逐條結果,跨輪次比較逐條結果的變化就客觀地說明了失敗的性質:
| 跨輪的測試結果變化 | 推論 | 走哪 |
|---|---|---|
| 失敗數逐輪減少 | 實作在收斂 | 留內層,繼續修 |
| 每輪失敗的是同一批 test,數量沒動 | 卡住了,這個方案碰不到那批條件 | 升中層,換方案 |
| 修好一批、弄壞另一批(震盪) | 方案的結構有問題 | 升中層 |
| 從第一輪就幾乎全掛 | 方案根本走錯方向 | 升中層,不用等第二輪 |
四條全部機械可判定,不需要任何 agent 的主觀意見。
同一個 repo 的多張卡共用一條 verify 指令,所以整套結果裡一定會有別張卡的失敗項。不過濾的話兩張卡會互相擋住:每張卡都因為別人的測試沒過而永遠無法通過。
這不是理論上的顧慮。實跑時 senior 停下來回報:
BLOCKED: 需要決定是否把範圍擴到 src/initials.js —— 現況下我在既定範圍內無事可做,但測試也不會綠。
它的判斷是對的,錯的是我讓它面對整套測試。 而且這個壓力的方向很糟 —— 為了讓測試變綠,agent 會被推著去改不屬於它的檔案。
修法:用驗收條件裡的 → test::name 引用把結果過濾成這張卡的範圍。一條都對不上時退回整套結果並回報 matched: 0 —— 悄悄放行是最糟的選項。
修完之後那張卡從「永遠 4/6」變成「3/3 通過」。
| 訊號 | 誰發出 | 速度 | |
|---|---|---|---|
| 1 | 開發 agent 主動宣告 PLAN_INFEASIBLE: <原因> |
開發 agent | 最快 |
| 2 | QA 判定實作忠實但方案達不到驗收條件 | QA | 中 |
| 3 | 內層輪次用盡 | runner | 最慢 |
第 1 個很重要:開發 agent 常常在第一步就知道方案行不通(plan 假設的檔案不存在、API 沒有那個參數)。給它一個明確的放棄出口,比讓它硬試兩輪有效得多。
PM 在全新的 context 重新規劃,不是讓開發 agent 換做法。理由:
- 規劃本來就是獨立階段。讓執行者兼任規劃者,違反當初把兩者分開的用意
- 錨定是真實問題 —— 開發 agent 的 context 塞滿它剛寫的 code,會傾向產出原方案的變體
- 「不知道前面撞過什麼牆」這個風險可以用結構化交接消除
| 內容 | 來源 |
|---|---|
| 原本的 plan | 卡上的 Implementation Plan |
| 測試逐條結果 —— 哪幾條從頭到尾沒過 | runner |
| 失敗形狀 —— 同一批卡住 / 震盪 / 全掛 | runner 跨輪比對 |
PLAN_INFEASIBLE 說明 |
開發 agent(如果有) |
| 已排除方案清單 —— 附原因 | 累積,見 §5.2 |
排除清單天然累積:換到第三個方案時,清單上已經有兩條。換方案因此不是每次從零猜,而是逐步收斂的搜尋。
main
├── task/178-attempt-1 方案 A(失敗,留著)
├── task/178-attempt-2 方案 B(失敗,留著)
└── task/178-attempt-3 方案 C(成功 → merge 這條)
| 情境 | code 怎麼辦 |
|---|---|
| junior → senior 接手 | 保留,senior 在上面修(路是對的,實作沒做好) |
| PM 換方案 | 丟掉,從 base_branch 乾淨開始(路走錯了) |
失敗的分支全部保留,不丟。理由:方案 A 的殘骸在方案 B 也失敗時是關鍵線索;你判斷「要不要繼續放手」靠的就是這些殘骸。
「停」是把球交回給你,不是放棄。卡停在 執行完成回報,上面有完整嘗試紀錄,所有分支都留著。你決定是拖回討論、換角度重談、還是自己動手。
強制在 runner 層,不在 label 層,也不在 prompt 層。
runner 環境封死的動作:
- 只在該卡的專用分支上動作
- 擋掉
git push - 不碰
main - 不動
.env或任何憑證 - 不發送任何對外訊息
- 不 deploy
就算 agent 在無人看管時判斷錯誤,也做不出不可逆的事。
label 只有一個:no-auto(這張我要自己來)。不做三層 tier 分級 —— 中間那層想解的問題已經被 runner 層封死了,留著只增加分類時的心智負擔。
handoff 是執行模式,不是建卡協定。 目標是解放雙手:一次授權,然後放手。
- 開始前,一次問完所有需要你決定的事
- 之後不再問
「這件事的禁止清單是什麼?」
允許清單一定會漏,禁止清單不會。
| 寫法 | 結果 |
|---|---|
❌「你可以改 auth.py」 |
改完要跑測試 —— 沒被授權,得再問 |
✅「這個 repo 底下自由讀寫、可跑測試、可裝依賴。不要 push、不要動 .env、不要碰 main」 |
邊界清楚,中間不用問任何一次 |
這跟 §10 給 runner 定的安全邊界是同一個形狀:封死不可逆的,其餘全放行。
| 遇到的事 | 怎麼處理 |
|---|---|
| 可逆的(選 A 還是 B 做法、要不要加這個欄位) | 自己決定,記錄下來,繼續跑 |
| 不可逆的(push、刪除、動憑證、對外送出) | 停 |
| 前提崩了(需求本身有洞、假設不成立) | 停 |
「可逆的自己決定」是 handoff 的實質內容;「不可逆的停」不因 handoff 而放寬。
兩個審查點你已經預先批准,卡照樣經過那些狀態留下紀錄,但不停留:
一般模式 開卡 →[停]需求討論 →[停]設計待批准 → 待執行 → … →[停]執行完成回報
handoff 開卡 → 需求討論 → 設計待批准 → 待執行 → … →[停]執行完成回報
唯一保留的停留點是 執行完成回報 —— handoff 解放的是執行過程中的雙手,不是最後的驗收。
一次回報,不中途播報。四段:
- 做完了什麼
- 我自己做的決定(可逆的抉擇,逐條列)← 最重要,你要能快速掃過並發現不對的
- 停下來的地方和原因(如果有)
- 沒做到的部分
註:權限提示的機制層設定(
settings.json的允許清單 / permission mode)尚未處理,之後再談。
產出不是 diff、而是報告或知識的任務(深度掃描、效能調查、架構分析)。
一般任務的 plan 描述「要改成什麼樣」;分析任務的 plan 描述「要怎麼走一遍」:
## Implementation Plan
### 掃描範圍
(哪些目錄、排除什麼、估計檔案數)
### 方法(分階段,每階段產出餵給下一階段)
1. …
2. …
### 分析維度(報告必須回答的問題)
- …
### 產出規格
(檔案路徑、必要章節、每個發現必須附證據)你在「設計待批准」審的是它打算怎麼掃,而不是它會發現什麼(那事前不可能知道)。
| 可否事先定義 | 誰驗 | |
|---|---|---|
| 範圍覆蓋、方法執行、產出格式 | ✅ | 機械 —— 寫個檢查腳本 |
| 發現的內容本身 | ❌ | 只能你,在「執行完成回報」 |
第二類事前無法定義 —— 不能寫「必須發現 3 個問題」,因為如果系統是乾淨的,發現 0 個才是正確答案。
verify 因此跑的是結構與覆蓋檢查而非測試:
verify: "python3 scripts/check_scan_report.py reports/scan-2026-07-27.md"檢查內容例如:必要章節都存在且非空、掃描涵蓋檔案數達標、每個發現都附了 檔案:行號、沒有出現無證據措辭。
仍然完全符合原則 3 —— 客觀判定、不靠感覺。只是判定對象從「功能對不對」變成「方法有沒有執行完、產出合不合規格」。
| 角色 | 一般任務 | 分析任務 |
|---|---|---|
| PM | 產改動計畫 | 產掃描方法論 |
| junior RD | 寫 code | 執行掃描、寫分析腳本、產報告 |
| senior RD | 減法 review code | 減法 review 報告 |
| QA | 符合度判定 | 同樣適用 |
senior 的減法在這裡更重要 —— 模型寫分析報告的膨脹傾向比寫 code 更嚴重(冗長鋪陳、重複結論、無證據推測、為了完整而硬湊的章節)。對應「複雜度不要過高」的原則,在報告上就是:每一句話都要有證據,沒證據的刪掉。
卡上放人看的,這裡放機器查的。也是「養成」project 的原料,因此要記得夠完整 —— 不只記「失敗了」,要記「試了什麼、為什麼不行」。
-- 一次執行嘗試 = 一個方案
attempt (
id, card_id, attempt_no,
plan_snapshot, -- 這次用的 plan 全文
branch, -- task/178-attempt-2
outcome, -- success | plan_inadequate | infeasible | budget_stop | limit_stop
outcome_reason,
started_at, ended_at, cost_usd
)
-- 一輪 = 一次 開發→測試→減法→測試→QA
round (
id, attempt_id, round_no,
rd_role, -- junior | senior
model, effort, -- 例如 claude-sonnet-5 / high
test_results, -- JSONB:逐條測試結果
senior_reductions, -- 減法 review 刪了什麼
complexity_before, complexity_after,
qa_verdict,
qa_notes,
cost_usd, started_at, ended_at
)round.test_results 跨 round 比對即得到失敗形狀(收斂 / 卡住 / 震盪 / 全掛),那是升級決策的唯一依據。
| 元件 | release | |
|---|---|---|
| 1 | 常駐服務本體 | ✅ |
| 2 | 入口介面 —— 明確定義的 API,讓任何通道都能接進來建卡、討論、下指令 | ✅ |
| 3 | 多 agent 產線(PM / junior RD / senior RD / QA,角色可擴充) | ✅ |
| 4 | Redis + Postgres 的 schema 與資料層 | ✅ |
| 5 | 工單的資料結構定義 | ✅ |
| 6 | Backlog.md 整合 | ✅ |
| 7 | 任何具體的 bot 實作 | ❌ |
| 8 | 工單內容 | ❌ |
release 的是「核心 + 介面 + 資料結構」,不含任何邊緣的具體實作。
別人裝起來是一個空的、可運作的系統:照介面接自己的 bot 或直接打 API,有完整的資料結構但裡面沒有任何一張別人的卡。
這個形狀順帶解掉一件事:未來要在 Discord 上增加更多 agent,那些 agent 全部是介面的使用者,不用改核心。
runner 驅動的是使用者自己本機的 Claude Code,認證由使用者自行完成,本系統從頭到尾不碰認證。
| 項目 | 說明 | |
|---|---|---|
| 1 | ✅ 已解決 —— taskcrew | |
| 2 | ✅ 已解決 —— Phase 0 實測完成,見 §17 | |
| 3 | ✅ 已解決 —— parent_task_id 原生支援,看板原生畫出樹狀結構 |
|
| 7 | ✅ 已驗 —— backlog browser 綁 *:6420,可拖卡改狀態、直接編輯內文 |
|
| 4 | 權限 / harness 設定 | settings.json 的允許清單細節,之後再談 |
| 6 | agent 的 model 與 thinking mode 具體配置 | 由使用者設計。方向已定:Opus = PM、Sonnet + Opus = junior/senior RD、Haiku = QA |
| 8 | 執行歷史(Postgres) | schema 已定(§13),尚未接上。產線不依賴它,所以可以晚做 |
「養成 agent 解決問題的能力」是獨立的另一個 project。
本系統只負責產生原料(§13 的執行歷史:試過哪些方案、各自為何失敗、測試逐條結果、最後怎麼成的)。如何把這些歷史變成下次用得上的知識 —— 載體、檢索、防止累積垃圾、跨身份的 firewall —— 全部屬於那個 project。
接口就是 Postgres 的執行歷史 schema。
環境:Backlog.md 1.48.0(brew install backlog-md),macOS。測試方式:建立丟棄用的 git repo,backlog init,建父子卡,反覆 edit 觀察檔案變化。
| 項目 | 結果 | |
|---|---|---|
| ✅ | 目錄結構 | 與文件一致:backlog/{tasks,drafts,milestones,docs,decisions,completed,archive} + config.yml |
| ✅ | 九欄自訂狀態 | config.yml 的 statuses 陣列可完全自訂,支援中文 |
| ✅ | 父子任務 | --parent 原生支援;子卡 ID 自動階層化(TASK-1 → TASK-1.1);parent_task_id 欄位;終端看板原生畫出 └─ 樹狀結構 |
| ✅ | 依賴 | --depends-on / --dep 原生支援 |
| ✅ | 驗收條件 | --ac 可重複;產出帶編號的清單(#1 #2),正好可對應 test case |
| ✅ | labels | 正常,edit 後保留 |
| ✅ | 未知的內文區塊會被保留 | 反覆 task edit 後,自訂的 ## Runner Config 區塊完整存活 |
| 發現 | 對設計的影響 | |
|---|---|---|
| ❌ | 自訂 frontmatter 欄位會被吃掉 | backlog task edit 重寫 frontmatter 時,刪除所有它不認識的鍵(實測 project / base_branch / verify / autonomy 全被移除)。→ §5 已改為把自訂設定放 ## Runner Config 內文區塊 |
| 發現 | 因應 | |
|---|---|---|
沒有 JSON 輸出,只有 --plain 文字 |
runner 直接讀寫 markdown 檔,不透過 CLI | |
task view --plain 不顯示未知區塊 |
同上 —— CLI 是給人看的有損視圖,檔案才是真實來源 | |
| 區塊由 HTML 註解標記界定 | runner 寫回時必須尊重 SECTION:*/AC:* 標記,並用自己的 RUNNER:*/EXCLUDED:*/NOTES:* 標記包住自訂區塊 |
|
remote_operations 預設 true,無 remote 時每次操作會發警告 |
board repo 若不設 remote,backlog config set remoteOperations false |
|
| 讀取時會掃描本地與遠端分支以合併任務狀態 | 這行為與「一卡一方案一分支」會互動,實作時要確認不會把 attempt 分支上的舊卡狀態拉回來 |
最後一項是唯一可能還有問題的地方,Phase 1 要特別驗證。
Node 26 原生 TypeScript,88 個測試全綠,關鍵機制都做過變異驗證。
| 章節 | 狀態 | |
|---|---|---|
| ✅ | §4 看板狀態機、§5 卡片結構、§6 閘門(七項檢查) | 完成 |
| ✅ | §4.1–4.3 父子卡、成果分支、下游解除條件、規劃時機(依賴與子卡共用一條規則) | 完成 |
| ✅ | §7 常駐服務、指令佇列、排程指令 | 完成 |
| ✅ | §7.1a 四個角色常駐、§7.1b PM 獨立 | 完成 |
| ✅ | §7.5 工作環境(規劃用 worktree、每張卡收尾) | 完成 |
| ✅ | §8 產線(PM → junior → 測試 → senior 減法 → 測試 → QA) | 完成 |
| ✅ | §8.3 三層循環、REVISE/REPLACE、§8.4 session 延續 | 完成 |
| ✅ | §9 失敗形狀分類、驗收範圍過濾、分支策略 | 完成 |
| ✅ | §10 安全邊界(--disallowed-tools,機制層) |
完成 |
| ✅ | §3.3 Redis board 命名空間隔離 | 完成 |
| ✅ | 即時事件推送(taskcrew watch 同時是入口層的參考實作) |
完成 |
| ⬜ | §13 Postgres 執行歷史 | 未接。產線不依賴它 |
| ⬜ | 遠端(手機)存取 | web UI 已驗過(backlog browser,綁 *:6420),對外存取的方式未定 |
跑了兩份不同的需求,各是「父卡 + 三張子卡,其中一張依賴另一張」,用真的 Opus/Sonnet/Haiku 走完整條產線。
| 第一份(文字工具) | 第二份(CSV 工具) | |
|---|---|---|
| 驗收 | 父卡成果分支 7/7 通過 | 父卡成果分支 7/7 通過 |
| 花費 | $3.53 | $2.38 |
| 輪數 | 手動推進 | 自己收斂成 3 輪(腳本沒寫死輪數,跑到看板不再前進為止) |
| 產出 | 4 個檔、32 行 | 4 個檔、45 行 |
第二份是修完前一份撞出的所有缺口之後跑的,用來確認那些修正真的成立而不只是個案。它自己收斂成預期的形狀:
輪 1 規劃 1.1、1.3(1.2 等 1.1、父卡等三張)→ 批准 → 跑 1.1、1.3
輪 2 規劃 1.2(1.1 有成果了) → 批准 → 跑 1.2
輪 3 規劃父卡(三張都有成果了) → 批准 → 整合
輪 4 空轉一圈,確認收斂
值得記的行為:
- RD 守住了範圍。 面對整套七條測試裡多數必然是紅的,沒有任何一次去補別張卡的檔案 —— 每條分支的
git diff都只有它該動的那一個檔(依賴鏈上的卡會多出上游的檔案,那是它的起點,不是它動的)。 - PM 停手時都是對的。 它抓到我測試資料裡真實的矛盾(兩條測試對截斷長度的要求互斥);指出卡片斷言「依賴已在你的分支上」當下不成立並要求 RD 先驗證再動手、絕不自己補一份;還去讀 git log 發現測試資料剛被改過,要求 RD 只信現在的檔案。每一次停手都指向一個真的設計缺口。
- 規劃在合併後的 worktree 裡進行是看得出來的。 PM 在裡面跑了
npm test並回報「6 pass / 1 fail,唯一失敗的是整合那條」,還自己用 node 驗過資料流 —— 那是它真的站在整合後的狀態才說得出來的話。
前兩次端到端跑的都是合成題目。這次是真的:讓 Gina(Discord bot)每天 10:00 與 22:00 送一份投資向的新聞彙整。
需求被切成三塊,切的依據是「能不能機械驗證」:
| 內容 | 走 taskcrew? | |
|---|---|---|
| ① 解析 | RSS → 標題/摘要/出處 | ✅ 有測試 |
| ② 排程與發送 | 時間計算、抓取容錯、發到 Discord | ✅ 有測試 |
| ③ 寫作規則 | 選哪幾則、摘要怎麼寫 | ❌ 人自己寫的 skill |
③ 不進 taskcrew 是因為「選題選得好不好」沒有機械判定的方法。硬塞進來只會讓
驗收條件變成願望。它是一份 prompt(.claude/skills/<name>/SKILL.md),改完
立刻生效、不用重啟服務 —— 而那正是使用者事後最常調的部分。
結果:兩張卡都是一個方案、一輪過,20 條測試,等價消耗 $5.55。
值得記的是 PM 的表現。它不是照著卡片複述,而是去查證:讀 408 KB 的 fixture
數出每份的 item 數、找出 4 則沒有 <ol> 的例外、發現 Google 的 description
是雙重轉義。還主動去讀既有的 sendChunks 並判斷不能重用(會吃掉內容、
會切壞連結),以及點出兩個關鍵陷阱:
nextRun的比較若寫成>=,整點會排在 0 毫秒後,觸發完立刻再觸發、洗版頻道httpFetch若不檢查res.ok,Google 回 4xx 錯誤頁會被當成抓取成功、 然後 parse 出 0 則新聞 —— 它的原話是「比抓失敗更難查」
junior 也做了一個對的判斷:那 4 則沒有 <ol> 的新聞丟掉照樣 10/10 綠,
但它按 PM 的建議保留並退回純文字。為了過測而砍功能,它沒有做。
| bug | 症狀 |
|---|---|
--project <值> 被當成位置參數 |
跑去目標 repo 找看板,然後失敗 |
cli.ts 一被 import 就執行整個 CLI |
測試行程被 process.exit(0) 殺掉,而該檔的斷言看起來像「通過」 |
upsertSection 用了 \z |
JS 沒有這個 escape(它是字母 z)。最後一個區塊會變成附加而非取代 —— 而 Implementation Plan/Notes 正好都是最後一個區塊 |
第三個最陰險:內容裡剛好有字母 z(英文、網址)它就意外正常,所以會間歇性 產生重複區塊而查不出規律。
兩件事是這次學到的:
- 驗收測試要先 commit。 規格不在 git 裡等於不存在 —— PM 開 worktree 規劃時 看不到未追蹤的檔案,第一次就是它發現的。
- 需求變了,守門的規則要跟著變。 中途使用者把「附上原始連結」改成「標明出處
就好」(Google News 的網址一則 500 字元,十則吃掉整個版面)。
checkDigest原本強制檢查連結,就得一起改 —— 否則它守的是一條已經不要的規則。
本文件標 ⚑ 的段落全部來自實跑,不是想出來的。共同形狀是**「我以為的邊界不是真的邊界」**。
會花錢但看得見的:
| 撞到什麼 | 代價 | 真正的問題 |
|---|---|---|
| Redis key 全域 | 背景 agent 撿走測試派工,跑了真模型 ~$0.50 | 目錄隔離管不到匯流排 |
| verify 跑整套測試 | senior 卡死,回報「範圍內無事可做但測試不會綠」 | 一個 repo 多張卡會互相擋,而且壓力方向是推 agent 越界 |
QA 回 **符合要求** 而我只比對純文字 |
白跑一輪 Opus xhigh($0.44) | 比對 agent 輸出時必須先剝掉 markdown 裝飾 |
| 逾時的 Redis 請求留在佇列裡 | 晚起來的 agent 撿去執行($0.11) | 請求需要 deadline,過期就丟 |
我自己修上一條時把 _ 也剝掉 |
被單元測試擋下 | 剝裝飾只能剝 * 和反引號 —— IMPLEMENTATION_BUG 裡的底線是內容 |
| PM 規劃父卡時子卡還沒做 | $0.24 換到一句早就知道的話 | 擋板只做在執行階段,規劃階段沒有 |
| 修上一條時只補了子卡、沒補依賴 | 又一次 $0.24,症狀一模一樣 | 「依賴」和「子卡」是同一件事,卻寫成兩段程式 |
不花錢但更危險的 —— 會無聲地產生錯誤結果:
| 撞到什麼 | 症狀 | 真正的問題 |
|---|---|---|
merge --no-commit 迴圈 |
父卡只合得進第一張子卡 | 未結束的合併狀態讓下一個 merge 被拒絕,而失敗被吞掉 |
| 未提交的殘骸 | 一張失敗的卡污染下一張的分支 | checkout -B 會把工作區的變更一起帶走 |
| 規劃階段沒有工作環境 | PM 對著一個 RD 不會站的世界寫 plan | 只有執行階段準備了環境 |
| 依賴要等人驗 | 一份四張卡的需求要人醒來四次 | 拿「可能發生、代價有界」的風險換「必然發生、代價是整晚」的損失 |
還有一條沒有金錢代價但更值得記:一條升級測試在 shouldEscalate 永遠回 false 的情況下仍然通過。它斷言的是「有升級」,而不是「因為失敗形狀而升級」—— 輪次用盡也會升級,所以它測不到任何東西。同一次全面變異驗證還抓出另外兩條空測試(一條斷言的是永遠成立的 root commit,一條被 macOS 的 /var symlink 騙過去)。變異測試是唯一抓得出這類測試的方法。
用 git worktree add /tmp/x main 測合併時,那個 worktree 是簽在 main 上的,於是那次 merge 直接推進了 main。
值得記在這裡的原因:DENIED_TOOLS 裡就有 Bash(git checkout main*),agent 做不到我剛才做的事。§10 說「保護在機制層,不在約定層」——我是在那層保護之外手動操作才出事的。