
文章同步發表在我的個人 Blog
AI 交回來的摘要寫著完成、測試通過。往下讀才看到一行:能跑的測試都過了,整合測試沒跑成。
前兩天寫的是投票頁做了什麼、怎麼實作。今天寫的是執行環境怎麼決定工單能寫什麼、誰來驗收哪一段。手機版那一關前後改了四次,是最完整的例子。
模型本身只會產生文字。真正決定它能不能動手、動到哪裡的,是外面那一層執行環境,一般叫 harness。用車子比喻,模型是引擎,harness 是車體:方向盤、煞車、輪胎、儀表板。同一顆引擎換一台車,能開去的地方不一樣。
我從官方文件整理出五個面向,後面就照這個比較:

Claude Code 的文件把工作迴圈寫成三個階段:收集脈絡、實作、驗證結果,而且隨時可以打斷。Codex CLI 通常用互動模式,也可以用 codex exec 把工單餵進去一次跑完。
這個專案兩個都用:Terminal 裡的 Claude Code,以及被它呼叫的 Codex CLI。Codex 這邊跑的模型是 gpt-6-astra,以下簡稱 Astra,負責實作。
兩個都是現成工具,我沒有自己做 Harness。但 agent 在裡面能做什麼、做到哪裡停、誰驗收哪一段,是我決定的。
這裡先講我對自己這次做法的定位。真正的 Harness Engineering 是把外圍環境工程化:自動跑測試、自動把錯誤餵回去重試、設上限避免無限迴圈。我完全沒做到那一層。我做的比較像 human-orchestrated agentic workflow,也可以說是半套 harness:環境是原廠的,我只設定邊界、脈絡和驗收順序,中間的推進還是靠人。
寫出來不是要自貶,而是這個位置的問題跟全自動化不一樣。工單怎麼寫、退件怎麼寫、哪一段交給誰,在這個階段反而是最吃經驗的部分。
下表是這個專案的設定與實測結果,不是兩個工具的完整能力比較:
Astra(codex exec,非互動) |
Claude Code(互動) | |
|---|---|---|
| 工作迴圈 | 一次跑完,中途不能插話 | 邊做邊回報,我隨時可以打斷 |
| 寫入範圍 | 指定的專案目錄 | 同上 |
| 網路 | 預設關閉,這次只有安裝 wrangler 那一輪打開 | 可用 |
| 綁定本機 port | 被擋住,listen EPERM |
可以 |
| 開瀏覽器 | 不行 | 可以,能自動操作與量座標 |
| 讀哪份說明檔 | AGENTS.md |
CLAUDE.md |
說明檔的支援版本:Claude Code 從 2.1.277 起也支援
AGENTS.md,專案裡沒有CLAUDE.md時就會改讀它,可以在/config的 Project instructions 調整(Bedrock、Vertex、Foundry 尚未支援)。這個專案兩份都有,所以各讀各的。
派工方法沿用 Day 3 的非互動指令:
codex exec --cd <project> --sandbox workspace-write - < work-order.txt
Day 3 只發現它開不了瀏覽器。這幾天跑整合測試才知道,綁定本機連接埠也被擋住,wrangler dev 在那邊起不來。
表格最後三列決定了後面兩節:工單能寫什麼,以及誰驗收哪一段。
送出去的 prompt 就沒辦法補充。它也不會中途詢問「這個需不需要做」,只會照現有內容做完。工單的四段格式 Day 4 寫過,這次調整的是最後一段:自我檢查分成兩邊寫,並要求它把已跑和沒跑的分開列出。
它能跑的:
npm run build、tsc --noEmit
它跑不動、要交給另一邊的:
Astra 每次交件都會附自我檢查結果,並標註沒跑到的項目。那些就由 Claude Code 這邊接手,因為它的環境做得到。例如:
wrangler dev,把本機的 Worker 和 D1 跑起來abc@abc、只有空白、超長的字串,測試 Server 會不會擋下來量文字有沒有被卡片切掉,是最實用的一項:
// hidden content inside each card
[...card.querySelectorAll("*")].filter(el => el.scrollHeight > el.clientHeight + 1)
安全修正那一輪就是靠這個分工看出落差的:Astra 回報 build 和 typecheck 通過、測試通過,同時標註有一批整合測試因為綁不了連接埠沒跑成。Claude Code 在自己的環境重跑,34 項檢查全通過。但如果只看摘要第一句就往下走,不會知道中間有一段是空的。
這幾天一共寫了 19 份工單。退件後重寫的修正單各自算一份,所以手機版那一關就佔了 5 份:初版加四次修正。19 份的分布是 14 份一次通過、3 份被退回後由修正單接手、1 份是唯讀的安全審查、1 份遇到模型滿載沒送出去。
我寫的工單只到產品層,例如手機版那關:橫向為主、直向顯示轉向提示、左下放虛擬搖桿、白板不要直接縮小桌機版。送出去之前,我會再補上實作限制:
| 補的條件 | 不補會怎樣 |
|---|---|
| 用觸控裝置判斷手機,不能只看視窗寬度 | 桌機把視窗拉窄就變成手機版,還跳出「請轉動手機」 |
| 搖桿和鍵盤共用同一套移動與碰撞 | 斜推搖桿會比鍵盤走得快 |
| 觸控範圍至少 44×44 | 圓點只有 22px,手指點不到 |
| 輸入框字級至少 16px | iOS 點下去會把整頁放大 |
| 注音選字的 Enter 不能當成送出 | 打中文選字時表單就送出去了 |
初版:搖桿、投票、補充情境都能使用,白板是使用桌機那套三欄縮小版,五張便利貼會擠在一起。在手機上測試的時候就發現不可行。
第一次修正:改成左右滑動的卡片。功能沒問題,但滑起來才發現,不滑到底不知道有哪幾個選項。這題的玩法是比較五個情境再分配三票,看不到全部就沒辦法比較。所以重新調整。
第二次修正:改成第一排三張、第二排兩張,五張一次看完。畫面看起來正常,量過之後發現每張卡的 scrollHeight 比 clientHeight 多 18px,剛好一行。也就是每張卡的情境文字都少顯示最後一行,要在卡片裡捲動才看得到。畫面上是「週末排了什麼課程,忘記紀錄、提」,句子就斷在那裡。所以重新調整。
退件工單節錄:
## 問題(驗收實測)
五張卡的情境文字被截斷,要在卡片內捲動才看得到最後一行。
- 844×390:五張卡都被藏 18px
- 915×412:四張卡被藏 18px
## 要求
三個尺寸下,完整情境文字都要顯示,不需要卡片內捲動
(scrollHeight 不得大於 clientHeight)。
## 調整順序(先動裝飾與間距,最後才動字級)
1. 減少卡片 padding、gap、內部留白
2. 縮小分類標籤字級
3. 重新分配卡片高度
4. 真的不夠才把內文從 14px 降到 13px,不得低於 13px
會新增調整順序,是因為擔心它直接把字級縮掉,而手機上 13px 已經是我能接受的最小尺寸。
第三次修正:文字調整成佔滿整行,圓點移到下面獨立一行,字級維持 14px。三種尺寸重新量過,被藏起來的高度都是 0。
第四次修正:我用 iPhone 打開,Safari 的網址列和分頁列吃掉一大塊,可用高度只剩 300px 左右。Day 4 寫的改用 dvh 就是這一輪的結果,驗收尺寸也從三種加到四種,多了 844×300。
三個問題來源不同:左右滑動是玩法判斷,文字會被截斷是量出來的,可用高度不足是實機測試才發現的。
現在這條線已經有固定的形狀:我定範圍、Astra 實作、Claude Code 跑 build 與測試並量畫面、退件附數字、通過才進下一關。每一關都有自己的自我檢查清單,工單也累積了一份踩坑清單,下一關直接沿用。
自動化的部分是驗收本身。build、typecheck、73 項測試、API 的併發與權限測試、四種尺寸的版面量測,這些都已經是跑一次指令就有結果,不必用眼睛看。
還沒自動化的是決策與串接:什麼時候退件、退件要寫什麼、下一關做什麼,仍然由我判斷。
下一步想做兩件事,排在投票頁收完資料之後:把驗收包成一個 verify 指令,一次跑 build、typecheck、測試和 API smoke,沒跑到的項目直接算失敗;再把派工寫成腳本,失敗自動重送,超過兩次就停下來叫人。到那時候,重試和停手才是程式在決定,也才比較接近 Harness Engineering。
投票頁已經準備部署上線了,明天寫上線的部分:正式資料庫怎麼開、上線前檢查了什麼、分享卡片的坑,然後把網址放出來,開始收資料。