Skip to content

Latest commit

 

History

History
1140 lines (783 loc) · 57.5 KB

File metadata and controls

1140 lines (783 loc) · 57.5 KB

Task Queue with Multi-Agent — 設計文件

狀態:核心已實作(2026-07-28)· 實作見本 repo 的 src/ Backlog.md 1.48.0 已實測,本文件的欄位與行為描述已依實測結果修正。驗證細節見 §17。 實作過程中被實跑推翻或補上的設計,已回寫進對應章節並標記 ⚑。落地進度見 §18。


1. 這是什麼

一個 task queue,導入多 agent 執行。把已經談定的工作固化成工單,逐一排空;每張工單由一組分工明確的 agent 產線完成。

跑在 Claude Code CLI 之上,因此使用既有的 Claude 訂閱額度,不需要額外的 API 計費。這是相對於其他 agent 編排系統的主要差異點。

不是什麼

  • 不是看板工具(看板用現成的 Backlog.md)
  • 不是聊天機器人(Discord bot 是介面的使用者,不是本體)
  • 不含「agent 能力養成」機制 —— 那是獨立的另一個 project,本系統只負責產生它需要的原料

2. 核心原則

這幾條貫穿整份設計,遇到取捨時以它們為準。

# 原則
1 保護在機制層,不在約定層。 不可逆的動作由 runner 環境封死,不靠 prompt 拜託 agent 遵守。
2 入口愈開放愈好,閘門愈嚴愈好。 品質不靠限制誰能建卡來保證,靠閘門檢查。
3 「有沒有做到」用機械判定(測試逐條結果),可以是硬門檻;「做得好不好」用判斷力(agent),只能是參考。
4 同一時間只有一個 owner。 卡的所有權隨狀態轉移,因此不需要鎖或衝突仲裁。
5 卡的大小上限不是「機器做得完」,是「你一次看得完」。 瓶頸是人的 review 頻寬。
6 不猜。 填不出來的欄位留白並標記,不自行補齊。

3. 系統架構

3.1 元件

  ┌─────────────────────────────────────────────────────┐
  │  入口(多元)—— 不屬於本系統,本系統只定義介面           │
  │  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)
              └────┘ └────┘ └────┘ └───┘

3.2 資料層,各司其職

存什麼 為什麼放這裡
Backlog.md(markdown in git) 卡本身:內容、狀態、驗收條件、plan、回報摘要 純文字、可 diff、有版本、人看得懂、離線可用
git 分支 每張卡的成果在哪:task/<id> 下游卡要引用上游成果,git 本來就是存這個的
Redis 指令佇列、派工與回覆、產線事件 常駐多 agent 需要訊息匯流排;即時回推也走這裡
Postgres 執行歷史:每次嘗試、每輪的結構化紀錄 需要查詢與長期保存;也是「養成」project 的原料

產線運轉所需的狀態,全部在 markdown 和 git 裡 —— 兩者你都讀得懂、改得動。Postgres 存的是歷史分析,是查詢用的,不是運轉的必要條件。這條界線要守住:如果哪天產線非有 DB 才跑得動,DB 一掛整套就停,而且你沒辦法靠編輯看板把事情喬回來。

3.3 多身份與 firewall

兩個身份(工作 / 個人)各自獨立:

身份 A(例如工作) 身份 B(例如個人)
board repo 各自一個 各自一個
queue 各自一個 各自一個
runner 各自一支 各自一支

兩邊可以同時執行,因為負責不同專案、不同 repo,天然不干擾。隔離是目錄與 repo 層級的,不是靠規則約束。

⚑ Redis 也必須隔離,而且是實跑撞出來的

第一版的 Redis key 是全域的。結果:背景還活著的真 agent 撿走了測試發出的派工,用真模型跑了測試資料(浪費約 $0.50)。共用一個 Redis instance 就等於共用一個匯流排,目錄隔離管不到這一層。

修法是每個 key 前綴一段 board 命名空間 —— board 絕對路徑的 sha256 前 12 碼:

tc:{a3f9c1d0e5b7}:commands    tc:{a3f9c1d0e5b7}:req:junior    tc:{a3f9c1d0e5b7}:events

不同 board 的訊息在 Redis 層就到不了對方,不靠任何約定。這條有自己的測試,也做過變異驗證。

加入第二個身份時是「複製一份設定」,不改架構。


4. 看板狀態機

開卡 → 需求討論 → 規劃中 → 設計待批准 → 待執行 → 執行中 → 執行完成回報 → 完成
         ↑           ↑          ↑                                    │
         └───────────┴──────────┴────── 不滿意,拖回來再談一輪 ←──────┘

                          阻塞  ← 等依賴/子卡產出成果(可從「待執行」轉入)
# 狀態 球在誰手上 離開條件
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 偵測到被改動就拒絕採用結果。

⚑ 4.1 父子卡:誰負責「合起來對不對」

一份需求拆成三張卡之後,有一段工作是沒有人負責的:把三張的成果合起來、跑只有合起來才驗得到的整體驗收。父卡就是負責那一段的卡。

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

⚑ 每次合併都要 commit,這不是風格選擇

原本寫成 git merge --no-commit --no-ff 的迴圈。那個寫法只合得進第一張子卡:第一次合併留下未結束的合併狀態,第二個 git merge 會被 git 直接拒絕。

最糟的是它悄悄失敗 —— agent 拿到一個看起來正常、但少了東西的工作區,然後在錯的基礎上工作。父卡有兩張以上子卡就會踩到,而三張子卡是拆解的常見形狀。

看板是人可以編輯的,所以要處理「標成完成但從沒跑過」

你隨時可以把一張卡拖到 完成。那張卡就沒有 task/<id> 分支。這是正常情境,不是壞掉 —— 那時退回 base_branch,並發出 missing-ref 事件讓你知道 plan 可能假設了不存在的東西。悄悄失敗和直接當掉都不對。

⚑ 4.2 下游什麼時候可以開工

原本的規則是「依賴要到 完成(你驗過)」。實跑才看出代價:一份拆成四張卡的需求會需要你醒來四次。晚上掛著跑,早上看到的是「做完 2 張、卡住 2 張」——跟 §7.3 寫的「跑到撞額度」直接衝突。

當初的顧慮(地基是錯的,下游白做)沒錯,錯在權重:

代價 有多確定
繼續做,結果地基被你退回 一張卡的 token,分支還留著 不一定會發生
停下來等人 整晚的產能 一定會發生

現行規則:

可以讓下游開工  =  status 是「完成」
                 或(status 是「執行完成回報」且有成果分支且沒標 require_review)

為什麼要多看一次成果分支:失敗的卡也停在「執行完成回報」——那一欄的意思是「球在你手上」,不是「成功了」。成果分支只在產線通過時才會被指過去,是唯一機械可查的證據。

「完成」則不查分支,那是你親口說的,而且手動拖過去的卡本來就沒有分支。

require_review: true(Runner Config 的選填欄位)退回舊行為。留這個開關是因為「這張是別人的地基,錯了下游全廢」確實存在,只是它是少數——不該讓多數去遷就它。

⚑ 4.3 什麼時候輪到規劃

同一條規則也用在規劃階段,不只執行。一張卡要建立在別人的產出上,而那些產出還不存在的話,PM 只能瞎猜。

「依賴」和「子卡」是同一件事的兩種形狀 —— 依賴是「我要用你做出來的東西」,子卡是「我要把你們做出來的東西合起來」。兩者用同一條規則(gate.notReady()),規劃和執行共用同一份判斷。

這條是分兩次撞出來的,兩次症狀一模一樣,只是換了個入口:

撞到 PM 的回報
父卡與子卡同批送規劃 「三個子模組完全不存在,請決定…」
TASK-1.2 與它依賴的 TASK-1.1 同批送規劃 src/parse.js 在這個分支不存在,需要決定…」

兩次 PM 的判斷都對,但那要花一次 Opus 呼叫($0.24 上下)才換到一句我們早就知道的話,而且卡會被退回「需求討論」,看起來像出了問題。

代價:依賴鏈上每一段都要一輪審查

一份四張卡、含一條依賴鏈的需求,會收斂成三輪 規劃 → 批准 → 排空

值得,理由是在上游真的產出東西之前,那份 plan 只能是猜的 —— 而你批准一份猜測等於沒有審查。PM 拒絕產出它,跟核心原則 6「不猜」是一致的。

注意這不影響「掛著跑一整晚」:擋的是規劃,不是執行。排空階段仍然一路跑到 queue 空或撞額度(§4.2)。


5. 卡片資料結構

5.1 frontmatter(Backlog.md 原生欄位)

⚠️ 實測結果: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-1TASK-1.1TASK-1.1.1),終端看板畫出樹狀結構。三層的用法是「模組 → 功能 → 子任務」,機制上不需要新東西:每層只等自己的直接子卡、只整合自己的直接子卡,遞迴自然組合(實測驗證)
dependencies 解除條件見 §4.2 —— 預設「跑完且通過」就放行,不等人驗。要等人驗的地基另外標 require_review
labels: no-auto 唯一有意義的 label:「這張我要自己來」
status 九個狀態在 backlog/config.ymlstatuses 陣列定義,支援中文

5.2 內文六個區塊

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 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

## Description

**背景**
(為什麼要做這件事)

**要做什麼**
(具體、可驗證的描述)

**不要做什麼**              ← 必填,閘門檢查此段存在
- 不要順手重構 xxx
- 不要動 yyy 模組

**已知的坑 / 已排除的做法**    ← 選填但強烈建議
(防止重走白天已經否決過的路)

**參考點**                   ← 選填
(類似的既有 code 在哪,讓風格一致)

「不要做什麼」是必填的。無人看管時,這一段擋掉的災難比任何其他欄位都多。

Acceptance Criteria

每一條必須對應到至少一個可執行的 test case。

## Acceptance Criteria

- [ ] 重送時,已處理過的事件 ID 不會被再次處理
      → `test/webhook.test.ts::skips-duplicate-event`
- [ ] 去重表滿了之後,最舊的紀錄被淘汰
      → `test/webhook.test.ts::evicts-oldest`

閘門直接檢查「每條驗收條件有沒有掛對應的測試」,不靠判斷「這條夠不夠可驗證」。

寫不出對應測試的條件,就不是驗收條件,是願望。

Excluded Approaches

## Excluded Approaches

### 方案 1:在寫入時做 upsert(attempt-1,已排除)
**為什麼不行**:測試 `evicts-oldest` 從第一輪就全掛,因為 upsert 無法表達淘汰順序
**分支**`task/178-attempt-1`

累積,不覆蓋。 兩個用途:PM 換方案時的排除依據;你判斷「要不要繼續放手」的材料。

Implementation Notes

## 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。

5.3 父卡

父卡與子卡共用同一份 schema,差別只在三處:

父卡 子卡
parent 指向父卡
Acceptance Criteria 整體驗收 —— 只有合起來才驗得到的東西 該子任務自己的
Implementation Plan 整合方案 —— 怎麼把子卡的分支併起來 該子任務的做法

父卡在所有子卡到 完成 之前處於 阻塞;解除後由 runner 執行整合 + 整體驗收。父卡的 verify 只跑「整合後才驗得到」的那些,不重複跑子卡的測試。

5.4 拆解判準

依模組邊界與需求本身的結構來切。沒有機械規則,也沒有預設張數。

參考訊號(命中時值得停下來想一下,但不是規則):

  • 驗收條件超過 5 條
  • 描述裡出現兩個以上的「而且」
  • 動到兩個以上不相關的模組
  • 預期 diff 會超過你一次看得完的量

context 容量在某些任務(例如全庫掃描)會成為拆解的實際限制,但依情境判斷,不寫成規則


6. 閘門

卡從 設計待批准 移往 待執行 時,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 手動建立、甚至別人寫腳本灌進來 —— 品質全部押在這裡。


7. 執行模型

7.1 常駐服務

runner 是一個一直活著的服務,不是定時喚醒的腳本。理由:多入口(Discord/TG/API)本來就需要一個常駐的東西在接收,服務式多 agent 也需要常駐 agent 互相溝通。既然常駐非有不可,就讓它把執行也一併做了。

代價:需要處理崩潰復原。撞限或崩潰時,該卡整張退回 待執行,分支留著,紀錄寫到停的那一刻。下次重新取出時從頭開始那一輪,不做中斷點續跑。

⚑ 7.1a 四個角色全部常駐

四個角色各跑一個 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 才有「同一段對話一直接下去」可言。

⚑ 7.1b PM 獨立於你的對話

PM 是自己的一個 process,不是你正在講話的這個 session。

理由:跟你討論的 session 會被別的話題塞滿、會被清掉、會換 context。PM 需要的是對某個 repo 的穩定累積 —— 它上次為什麼那樣拆、它試過什麼、你退回過什麼。這兩種需求放在同一個 session 裡會互相破壞。

分開之後,你的對話層負責「跟你談清楚」,PM 負責「把談清楚的東西變成 plan 並且記得住」。

7.2 觸發

沒有常設排程表。每次執行都是一個明確指令。

指令形式 能表達
「現在跑 」 立刻開始
「凌晨兩點跑 」 指定時間

指令必須指定跑哪個 queue

兩種下指令的方式,共用同一個機制:

方式 能表達 用在什麼時候
跟 agent 講一句 時間 + 哪個 queue 要指定時間,或人不在看板前
拖看板上的控制卡 「現在跑」(queue 由卡所在的 board 決定) 手機上想立刻跑,最快

卡進 queue 之後只是排隊,不會自己開始。

7.3 結束條件

只有兩個:queue 空了,或撞到訂閱額度

不設卡數上限、不設時間上限、不設花費上限。一律只吃訂閱額度,不使用 API 計費。

因此「撞限時乾淨停止」是承重路徑:必須確保被擋在半路的卡退回 待執行、分支留著、下次接得回去。

⚑ 7.5 工作環境:agent 站在哪裡

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 不會站的世界寫的

規劃用暫時的 worktree,執行用 repo 本身

規劃是唯讀的工作 —— 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 驗過資料流。那是它真的站在整合後的狀態才說得出來的話。

⚑ 每張卡做完要把 repo 收回乾淨的起點

這條的理由不是整潔,是跨卡污染

git checkout -B 新分支 base 在工作區有未提交變更時,會把那些變更一起帶過去

於是一張失敗的卡留下的殘骸,會出現在下一張卡的分支上,而且沒有任何跡象。

所以每張卡結束時(通過、失敗、撞額度都一樣):先把殘骸 commit 在它自己的分支上,再 checkout 回 base_branch

commit 殘骸同時滿足 §9.4「失敗的分支全部保留」的原意 —— 方案 A 的殘骸在方案 B 也失敗時是關鍵線索,丟掉它等於丟掉你判斷「要不要繼續放手」的依據。原本的做法(完全不 commit、留在工作區)看起來也保留了殘骸,但它保留的位置是錯的。

7.4 序列執行

runner 逐筆取出、一次做一張。因此沒有並發,也就沒有一致性問題。

兩個身份的 runner 可以同時跑(不同 repo,不干擾),但各自內部序列。


8. Agent 產線

8.1 角色編制

角色 模型 職責
PM Opus 研究 codebase,產出 Implementation Plan;換方案時重新規劃
junior RD Sonnet 開發
senior RD Opus 常態:對每張卡做減法 review;例外:junior 做不出來時接手開發
QA Haiku 判定符不符合要求 —— 讓結果不只是 0 與 1

每個角色是獨立的 agent 定義,差別在 modelthinking modeclaude --model / claude --effort,兩者都是現成的 CLI 參數)。

8.2 產線

PM               →  Implementation Plan  →  你批准(審查點 2)
   ↓
junior RD        →  開發
   ↓
測試              →  逐條結果(先確認它能動)
   ↓
senior RD        →  減法 review(帶複雜度量測當參考)
   ↓
測試              →  再跑一次(確認減完還是能動)
   ↓
QA               →  符不符合要求

減法放在第一次測試之後 —— 先讓它能動,再讓它變乾淨。順序反過來的話,senior 會在一份還不確定對不對的 code 上做減法。減完必須再測一次,那是防止減過頭的唯一保險。

senior 的減法為什麼是常態而非例外

模型會不自覺地加東西:多餘的抽象、沒被要求的功能、對不可能情況的防禦。目標是產出的 code graph 複雜度不要過高

因為每張卡都經過 senior,senior 成為品質的單一控制點 —— 調它的 prompt 就等於調整整個系統的輸出風格,不用改 junior、不用改 PM、不用逐張卡下指令。

複雜度量測是參考資料,不是硬門檻。 有些情境耦合度就是高,硬拆只會造成更多悲劇。量測結果餵給 senior 讓它知道自己減得夠不夠,但要不要減、減哪裡由它判斷。

⚑ 8.1a 額度消耗的實測基準

先講清楚單位:下面的金額是 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,這只是參考。跑過五六張真實的卡之後那個數字才站得住。

讓它變便宜的兩件事
  1. 規格寫得愈死,PM 需要的 effort 愈低。 建卡者若已經把介面骨架和驗收測試寫好, PM 的工作就從「設計」變成「查證」,那不需要最高檔。2026-07-29 起 PM 的 effortxhigh 調成 high,就是為了測這件事——觀察指標是「plan 還抓不抓得到坑」 (例如「> 寫成 >= 會讓排程在整點無限自我觸發」「res.ok 不檢查會變成 抓取成功但 0 則新聞」這類)。抓得到就維持,開始漏了再調回去。
  2. 卡切小。 一張大卡失敗一輪的代價,比兩張小卡各跑一輪貴。

QA 為什麼是 Haiku

測試給的是 0/1;QA 給的是「符不符合要求」。這兩件事本來就該分開,Haiku 補的正是機械測試給不出來的那個維度。

8.3 三層循環

在解決什麼 誰在動
內層 實作沒寫對 junior → senior 接手
中層 做法本身不對 PM 重新規劃,換方案
外層 需求或方向不對
輪 1   junior RD (Sonnet)  → 測試 → senior 減法 → 測試 → QA
       ↓ 沒過
輪 2   senior RD (Opus)    → 接手開發(保留 junior 的 code,在上面修)
       ↓ 還是沒過
中層   PM REVISE   —— 同一個方案,補上沒講清楚的部分(帶著 context)
       ↓ 修正過還是沒過
中層   PM REPLACE  —— 換一個方案(乾淨 context)
       ↓ 次數用盡
外層   標 failed,停在「執行完成回報」等你

三層的成本與能力同時遞增,而且每次升級都真的換了東西,不是同一個東西再試一次。

⚑ 中層分兩級:REVISE 在 REPLACE 之前

原本中層只有「換全新方案」一種。但很多失敗的成因是沒確認清楚,不是方案錯 —— 那時整份丟掉重來是浪費,而且會把方案裡本來對的部分一起丟掉。

REVISE REPLACE
做什麼 調整現有做法:補漏掉的步驟、修錯的假設 換一個結構不同的方案
PM 的 session 延續 —— 它記得自己為什麼那樣寫 重置 —— 要的正是不被原方案錨定
原本的 code 保留 丟掉,從 base_branch 乾淨開始
什麼時候用 失敗形狀是「同一批卡住」(碰不到那批條件) 「震盪」或「第一輪就全掛」(結構有問題)

session 帶不帶,就是這一級和下一級唯一的實質差別,而且是設計的核心:延續給的是熟悉度,重置給的是不被錨定。兩種都需要,所以分成兩級而不是二選一。

⚑ 8.4 session 延續:錨定與熟悉度的取捨

claude --resume <sessionId> 讓一個 agent 接著上次的對話繼續。這件事有正反兩面,所以不能一律開或一律關

延續 session 不延續
得到 對這個 repo 的熟悉度:知道慣例、知道測試怎麼跑、不用重新摸索 不被上一輪的思路綁住
失去 錨定:context 裡塞滿它剛寫的 code,會傾向產出原方案的變體 每次從零,重複付出摸索成本

規則:做同一件事就延續,換一件事就重置。 REVISE 延續、REPLACE 重置;PM 對同一個 repo 的日常規劃一路延續。

這裡曾經想做「把 PM 的慣例外部化成筆記檔」。那是多餘的 —— PM 的 session 不重置,慣例自然就延續了。多一層筆記只是多一個會跟事實不同步的東西。

autonomy 參數

自主程度是卡上的參數,不是全域設定 —— 因為這需要長時間磨合,磨合的前提是可調。

內層失敗、需要換方案時
none 停。標 failed
propose PM 產新 plan 寫回卡上,卡退回 設計待批准,不執行
replan:N PM 產新 plan 並直接執行,最多 N 次
free 換到成功或撞額度

預設 propose 保守的起點,而且就算不放行執行,你早上也會看到 agent 想出的新方案 —— 那本身就是磨合的素材。


9. 失敗處理

9.1 失敗的分類:用 test case,不用感覺

verify 產出逐條結果,跨輪次比較逐條結果的變化就客觀地說明了失敗的性質:

跨輪的測試結果變化 推論 走哪
失敗數逐輪減少 實作在收斂 留內層,繼續修
每輪失敗的是同一批 test,數量沒動 卡住了,這個方案碰不到那批條件 升中層,換方案
修好一批、弄壞另一批(震盪 方案的結構有問題 升中層
從第一輪就幾乎全掛 方案根本走錯方向 升中層,不用等第二輪

四條全部機械可判定,不需要任何 agent 的主觀意見。

⚑ 9.1a 驗收只看這張卡涵蓋的測試

同一個 repo 的多張卡共用一條 verify 指令,所以整套結果裡一定會有別張卡的失敗項。不過濾的話兩張卡會互相擋住:每張卡都因為別人的測試沒過而永遠無法通過。

這不是理論上的顧慮。實跑時 senior 停下來回報:

BLOCKED: 需要決定是否把範圍擴到 src/initials.js —— 現況下我在既定範圍內無事可做,但測試也不會綠。

它的判斷是對的,錯的是我讓它面對整套測試。 而且這個壓力的方向很糟 —— 為了讓測試變綠,agent 會被推著去改不屬於它的檔案。

修法:用驗收條件裡的 → test::name 引用把結果過濾成這張卡的範圍。一條都對不上時退回整套結果並回報 matched: 0 —— 悄悄放行是最糟的選項

修完之後那張卡從「永遠 4/6」變成「3/3 通過」。

9.2 升級的三個訊號

訊號 誰發出 速度
1 開發 agent 主動宣告 PLAN_INFEASIBLE: <原因> 開發 agent 最快
2 QA 判定實作忠實但方案達不到驗收條件 QA
3 內層輪次用盡 runner 最慢

第 1 個很重要:開發 agent 常常在第一步就知道方案行不通(plan 假設的檔案不存在、API 沒有那個參數)。給它一個明確的放棄出口,比讓它硬試兩輪有效得多。

9.3 換方案:乾淨 context 重新規劃

PM 在全新的 context 重新規劃,不是讓開發 agent 換做法。理由:

  1. 規劃本來就是獨立階段。讓執行者兼任規劃者,違反當初把兩者分開的用意
  2. 錨定是真實問題 —— 開發 agent 的 context 塞滿它剛寫的 code,會傾向產出原方案的變體
  3. 「不知道前面撞過什麼牆」這個風險可以用結構化交接消除

失敗交接單(固定格式)

內容 來源
原本的 plan 卡上的 Implementation Plan
測試逐條結果 —— 哪幾條從頭到尾沒過 runner
失敗形狀 —— 同一批卡住 / 震盪 / 全掛 runner 跨輪比對
PLAN_INFEASIBLE 說明 開發 agent(如果有)
已排除方案清單 —— 附原因 累積,見 §5.2

排除清單天然累積:換到第三個方案時,清單上已經有兩條。換方案因此不是每次從零猜,而是逐步收斂的搜尋。

9.4 分支:一卡一方案一分支

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 也失敗時是關鍵線索;你判斷「要不要繼續放手」靠的就是這些殘骸。

9.5 停止的定義

「停」是把球交回給你,不是放棄。卡停在 執行完成回報,上面有完整嘗試紀錄,所有分支都留著。你決定是拖回討論、換角度重談、還是自己動手。


10. 安全邊界

強制在 runner 層,不在 label 層,也不在 prompt 層。

runner 環境封死的動作:

  • 只在該卡的專用分支上動作
  • 擋掉 git push
  • 不碰 main
  • 不動 .env 或任何憑證
  • 不發送任何對外訊息
  • 不 deploy

就算 agent 在無人看管時判斷錯誤,也做不出不可逆的事。

label 只有一個:no-auto(這張我要自己來)。不做三層 tier 分級 —— 中間那層想解的問題已經被 runner 層封死了,留著只增加分類時的心智負擔。


11. handoff

handoff 是執行模式,不是建卡協定。 目標是解放雙手:一次授權,然後放手。

兩件事,第二件更重要

  1. 開始前,一次問完所有需要你決定的事
  2. 之後不再問

前置提問的核心只有一個

「這件事的禁止清單是什麼?」

允許清單一定會漏,禁止清單不會。

寫法 結果
❌「你可以改 auth.py 改完要跑測試 —— 沒被授權,得再問
✅「這個 repo 底下自由讀寫、可跑測試、可裝依賴。不要 push、不要動 .env、不要碰 main 邊界清楚,中間不用問任何一次

這跟 §10 給 runner 定的安全邊界是同一個形狀:封死不可逆的,其餘全放行。

中途遇到沒問到的事

遇到的事 怎麼處理
可逆的(選 A 還是 B 做法、要不要加這個欄位) 自己決定,記錄下來,繼續跑
不可逆的(push、刪除、動憑證、對外送出)
前提崩了(需求本身有洞、假設不成立)

「可逆的自己決定」是 handoff 的實質內容;「不可逆的停」不因 handoff 而放寬。

handoff 模式下的看板

兩個審查點你已經預先批准,卡照樣經過那些狀態留下紀錄,但不停留

一般模式    開卡 →[停]需求討論 →[停]設計待批准 → 待執行 → … →[停]執行完成回報
handoff     開卡 →    需求討論 →    設計待批准 → 待執行 → … →[停]執行完成回報

唯一保留的停留點是 執行完成回報 —— handoff 解放的是執行過程中的雙手,不是最後的驗收。

結束時的回報

一次回報,不中途播報。四段:

  • 做完了什麼
  • 我自己做的決定(可逆的抉擇,逐條列)← 最重要,你要能快速掃過並發現不對的
  • 停下來的地方和原因(如果有)
  • 沒做到的部分

註:權限提示的機制層設定(settings.json 的允許清單 / permission mode)尚未處理,之後再談。


12. 特殊任務型態:掃描與分析

產出不是 diff、而是報告或知識的任務(深度掃描、效能調查、架構分析)。

plan 的形狀不同

一般任務的 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 更嚴重(冗長鋪陳、重複結論、無證據推測、為了完整而硬湊的章節)。對應「複雜度不要過高」的原則,在報告上就是:每一句話都要有證據,沒證據的刪掉


13. Postgres 執行歷史 schema

卡上放人看的,這裡放機器查的。也是「養成」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 比對即得到失敗形狀(收斂 / 卡住 / 震盪 / 全掛),那是升級決策的唯一依據。


14. Release 邊界

元件 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,認證由使用者自行完成,本系統從頭到尾不碰認證。


15. 未決事項

項目 說明
1 service 的名字 已解決 —— taskcrew
2 Backlog.md 的實際行為未驗證 已解決 —— Phase 0 實測完成,見 §17
3 父子關係的 UI 呈現 已解決 —— parent_task_id 原生支援,看板原生畫出樹狀結構
7 web UI 的實際樣貌 已驗 —— 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),尚未接上。產線不依賴它,所以可以晚做

16. 明確切出去的範圍

「養成 agent 解決問題的能力」是獨立的另一個 project。

本系統只負責產生原料(§13 的執行歷史:試過哪些方案、各自為何失敗、測試逐條結果、最後怎麼成的)。如何把這些歷史變成下次用得上的知識 —— 載體、檢索、防止累積垃圾、跨身份的 firewall —— 全部屬於那個 project。

接口就是 Postgres 的執行歷史 schema。


17. Phase 0:前提驗證結果

環境:Backlog.md 1.48.0brew install backlog-md),macOS。測試方式:建立丟棄用的 git repo,backlog init,建父子卡,反覆 edit 觀察檔案變化。

通過驗證

項目 結果
目錄結構 與文件一致:backlog/{tasks,drafts,milestones,docs,decisions,completed,archive} + config.yml
九欄自訂狀態 config.ymlstatuses 陣列可完全自訂,支援中文
父子任務 --parent 原生支援;子卡 ID 自動階層化(TASK-1TASK-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 要特別驗證。


18. 落地進度(2026-07-28)

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),對外存取的方式未定

端到端實跑(2026-07-28)

跑了兩份不同的需求,各是「父卡 + 三張子卡,其中一張依賴另一張」,用真的 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 驗過資料流 —— 那是它真的站在整合後的狀態才說得出來的話。

真實工作的第一次(2026-07-29)

前兩次端到端跑的都是合成題目。這次是真的:讓 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 的建議保留並退回純文字。為了過測而砍功能,它沒有做。

這次撞到的 taskcrew bug(都是第一次真的用才踩到)

bug 症狀
--project <值> 被當成位置參數 跑去目標 repo 找看板,然後失敗
cli.ts 一被 import 就執行整個 CLI 測試行程被 process.exit(0) 殺掉,而該檔的斷言看起來像「通過」
upsertSection 用了 \z JS 沒有這個 escape(它是字母 z)。最後一個區塊會變成附加而非取代 —— 而 Implementation Plan/Notes 正好都是最後一個區塊

第三個最陰險:內容裡剛好有字母 z(英文、網址)它就意外正常,所以會間歇性 產生重複區塊而查不出規律。

建卡者的責任比想像中大

兩件事是這次學到的:

  1. 驗收測試要先 commit。 規格不在 git 裡等於不存在 —— PM 開 worktree 規劃時 看不到未追蹤的檔案,第一次就是它發現的。
  2. 需求變了,守門的規則要跟著變。 中途使用者把「附上原始連結」改成「標明出處 就好」(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 說「保護在機制層,不在約定層」——我是在那層保護之外手動操作才出事的。