用 launchd 跑 Autonomous Agent:一台 Mac、兩個班、兩週 17 個 PR
這是 QA Pipeline 系列的第 4 篇。前面三篇講了 pipeline 結構、spec-driven 哲學、跨 session learned-patterns。這些都是「agent 層」的決策。這篇要講底層:整套 pipeline 怎麼被觸發、怎麼排程、怎麼避免互相撞、怎麼在出問題時叫醒人類。
答案很反潮流:一台 MacBook、macOS 原生的 launchd、幾百行 bash。
為什麼不用雲
看過太多 agent 系統第一步就上雲:K8s CronJob、AWS ECS Scheduled Task、GitHub Actions 排程 workflow、專門的 agent-hosting 服務。對一個 solo developer 或小團隊來說,這些都是過度工程。
四個實際成本:
- 雙倍開銷:Claude API 的 token 費已經不便宜,還要疊 cloud infra 費
- Debug 距離變長:logs 在 CloudWatch / Datadog / Loki,每次回看都要 round-trip
- Credential 管理:本地
.env.test一行搞定的事、雲上要 IAM role + secret manager - Lock-in:哪天想換環境、工作流程綁死在某個 provider
對「每天跑兩次、修 frontend bug」這個需求,單機 launchd 的 dev loop 快得誇張。改 agent prompt → 存檔 → 下一班次就生效、不用 deploy、不用等 pod 起來。
launchd 101
launchd 是 macOS 原生的 init + cron 混合體。相較於傳統 cron 有幾個優點:
- StartCalendarInterval:多時段、UTC-safe、格式明確
- StandardOutPath / StandardErrorPath:log 直接寫檔,不用自己
>> file.log - EnvironmentVariables:可以在 plist 裡注入(重要,因為 launchd 的 PATH 跟你 shell 的不一樣)
- WorkingDirectory:免
cd樣板 - KeepAlive:crash 自動重啟(這個 pipeline 沒開,因為我要知道它掛)
兩班制排程:com.example.qa-shift.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.qa-shift</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/me/Project/my-app/scripts/qa-shift.sh</string>
</array>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key><integer>8</integer>
<key>Minute</key><integer>0</integer>
</dict>
<dict>
<key>Hour</key><integer>0</integer>
<key>Minute</key><integer>0</integer>
</dict>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/me/.local/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
</dict>
<key>WorkingDirectory</key>
<string>/Users/me/Project/my-app</string>
<key>StandardOutPath</key>
<string>/Users/me/Project/my-app/reports/launchd-qa-shift-stdout.log</string>
</dict>
</plist>
08:00 AM shift、00:00 night shift。早班在我進辦公室前把昨晚的 bug 分類好、issue 開好;晚班在我下班後繼續修、發 PR。人睡的時間 agent 上班。
為什麼不是一班或四班?兩班是實際跑過之後的甜蜜點:一班 coverage 不足(一天累積 12h 的 bug 才處理、delay 太長)、四班以上會撞到 API rate limit 跟自己的 git worktree 管理複雜度。
Lock file:讓重疊的 shift 不會互相打架
這段是真實 qa-shift.sh 裡的 lock 處理,每行都有講究:
LOCK_FILE="$PROJECT_DIR/reports/.lock"
MAX_LOCK_AGE=14400 # 4 hours — 理論天花板 buffer
if [ -f "$LOCK_FILE" ]; then
IFS='|' read -r LOCK_PID LOCK_TIME < "$LOCK_FILE"
NOW=$(date +%s)
AGE=$(( NOW - LOCK_TIME ))
if kill -0 "$LOCK_PID" 2>/dev/null && [ "$AGE" -lt "$MAX_LOCK_AGE" ]; then
log "ERROR: Another run in progress (PID=$LOCK_PID, age=${AGE}s). Aborting."
send_slack "⚠️ QA $QA_SESSION shift skipped — previous run still in progress"
exit 1
fi
log "WARNING: Stale lock found (PID=$LOCK_PID, age=${AGE}s). Removing."
rm -f "$LOCK_FILE"
fi
echo "$$|$(date +%s)" > "$LOCK_FILE"
cleanup() { rm -f "$LOCK_FILE"; }
trap cleanup EXIT INT TERM HUP
關鍵四點:
- Lock 帶 PID + epoch timestamp(用
|分隔)— 不只看檔案存在、還要確認 PID 真的活著 kill -0檢查活體:這個命令不真的殺、只回傳「這個 PID 是否存在」- Stale 保護(
MAX_LOCK_AGE=14400):活著但超過 4 小時也判定異常(可能是 pipeline 卡死) trap EXIT INT TERM HUP:正常結束、Ctrl+C、kill、掛斷,任一情況都清 lock — 確保不會留 stale lock 讓下一班 skip
為什麼 MAX_LOCK_AGE 設 4 小時?實測正常 shift median 37 分鐘、目前最長 66 分鐘。4 小時是 paranoid 天花板 — 真的需要這麼久通常是 collector 卡在某個 hang 的 E2E test 上、那個 session 已經該人工介入了。
Health file:讓 agent 會對你求救
HEALTH_FILE="$PROJECT_DIR/reports/.health.json"
每個 agent 跑完寫入 status。實際內容長這樣(2026-04-14 AM shift):
{
"date": "2026-04-14",
"session": "am",
"shift_start": "2026-04-14T08:00:05+08:00",
"shift_end": "2026-04-14T08:43:36+08:00",
"agents": {
"qa-planner": { "status": "ok", "duration_s": 527 },
"qa-collector": { "status": "ok", "duration_s": 454 },
"qa-analyzer": { "status": "ok", "duration_s": 415 },
"qa-issue-filer": { "status": "ok", "duration_s": 323 },
"qa-fixer": { "status": "ok", "duration_s": 777, "rounds": 1 },
"qa-explorer": { "status": "skipped", "reason": "qa-fixer ran instead" },
"qa-reviewer": { "status": "skipped", "reason": "no open qa-bot PRs" },
"qa-reporter": { "status": "ok", "duration_s": 94 }
}
}
搭配 Slack webhook:
send_slack() {
local msg="$1"
if [ -n "$SLACK_E2E_WEBHOOK_URL" ]; then
curl -s -o /dev/null -X POST \
-H "Content-Type: application/json" \
-d "{\"text\": \"$msg\"}" \
"$SLACK_E2E_WEBHOOK_URL" 2>/dev/null || true
fi
}
任一 agent 跑失敗 / shift 被 skip 都會推 Slack — 我手機會響、知道該去看 log。訊息分成兩類:lock 衝突導致 skip(輕微)vs agent 真的 exit 非零(嚴重)。
Single-agent rerun mode
./scripts/qa-shift.sh # 跑整條
./scripts/qa-shift.sh qa-fixer # 只跑 fixer
實作:
SINGLE_AGENT="${1:-}"
if [ -z "$SINGLE_AGENT" ] || [ "$SINGLE_AGENT" = "all" ]; then
# full pipeline with lock
echo "$$|$(date +%s)" > "$LOCK_FILE"
fi
意義:某個 agent prompt 改完、我不想等下一班才驗證、也不想重跑整條 pipeline(median 37 分鐘、最糟可達數小時)。./scripts/qa-shift.sh qa-fixer 直接重跑 fixer。
這個能力從 day 1 就該做。前兩週沒做、每次改 prompt 都等下一班、痛了一週才補進來。
.env.test — 秘密管理的「夠好」版
if [ -f "$PROJECT_DIR/.env.test" ]; then
set -a
source "$PROJECT_DIR/.env.test"
set +a
fi
LINEAR_API_KEY、SLACK_E2E_WEBHOOK_URL、ANTHROPIC_API_KEY 全放這。檔名進 .gitignore、權限 chmod 600。單機場景這樣就足夠。雲端才需要 secret manager。
踩過的坑
幾個 launchd 特有的陷阱:
- plist 放好不會自動 load — 第一次要
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.qa-shift.plist。unload 用launchctl bootout。老文章會看到launchctl load/unload、那是 deprecated 的寫法、現在該用 bootstrap/bootout。 - launchd 的 PATH 不是你 shell 的 PATH — 所以 plist 裡要顯式
EnvironmentVariables > PATH。沒這條claudeCLI 找不到。 - MacBook 睡著 launchd 不跑 — 解法:caffeinate、或設成「闔蓋不睡眠」(Preventing Sleep)、或接電源時才跑。我選最後一個。
LaunchAgents跟LaunchDaemons別混 — Agents 是使用者 session 內、Daemons 是系統全域。這個需要登入 session(Claude CLI 要讀 keychain),所以是 Agents。- log 不會自動輪替 —
launchd-qa-shift-stdout.log兩週已經 153KB、半年後會爆。我沒補、但該寫個 cron logrotate。
這個架構什麼時候會變負擔
誠實講,當團隊或規模達到這些條件、launchd 單機就過時了:
- 需要 7x24 可用:MacBook 會睡、會重開、會搬家
- 多人共用觸發:launchd 綁本機、其他人不能手動 trigger
- Compliance / SOC2:單機 credential 管理過不了稽核
- 要跨 repo 跑:git worktree 管理變複雜、記憶體吃緊
到那天、遷到 cloud 是對的:GitHub Actions scheduled workflow、EC2、ECS Scheduled Task。但在那之前:單機 launchd 是 dev loop 無敵快的選擇。
一個更大的觀點
Agent 系統的 90% 價值來自 agent 本身(prompt、tool use、feedback loop、跨 session memory),10% 來自 infra。不要先把 10% 做到 100 分。
很多團隊落入這個陷阱:花兩個月蓋 agent orchestration infra、agent 本身只跑 hello world。我看到的成功案例幾乎都反過來:先把一個 agent 跑到真的有用、再逐步 harden infra。launchd 是這條路上的最低成本起點。
下一篇 #5 講另一個類似哲學的選擇 — 不要為 agent 造新的 work queue,用 Linear(你的團隊已經在用的那個)。
如果你也在跑自家的 agent pipeline、想看看別的 single-machine 設定、或者 launchd plist 有哪些我沒踩到的坑,Twitter @waiting7777 交換心得。
Waiting7777
WoW Arena 冠軍轉前端,用電競 meta 思維拆解技術趨勢。
相關文章
AI Agents開發者工具+1Orca 七個月拿下 10 萬顆星:我拆了它的 GitHub 數據,看到 agent 開發真正的瓶頸
一個由 agent 寫出來、拿來管 agent 的開源 IDE。19,000 個 commit、979 次 release 背後,最值得看的數字是 2,525 個放了一個月還沒人處理的 PR。
2026年10月20日 · Waiting7777 · 7 分鐘閱讀
繼續閱讀 →
AI Agents自動化測試+1跑了兩週 Agent QA Pipeline — 真實數字、踩到的坑、重來我會怎麼做
這是 post-mortem,不是 case study。做對的三件事、做錯的三件事、以及為什麼這套東西接下來要抽成 `claude-qa-pipeline` OSS。
2026年10月20日 · Waiting7777 · 6 分鐘閱讀
繼續閱讀 →
自動化測試開發者工具你的 100% Coverage 是假的 — 用 test-audit 抓出沒被真正測到的分支
AI 寫的 code 跑出漂亮的覆蓋率數字,但 error path 根本沒人測過,這工具專門抓這個。
2026年10月20日 · Waiting7777 · 3 分鐘閱讀
繼續閱讀 →這篇文章對你有幫助嗎?
每週一篇 — 技術趨勢背後的商業邏輯
AI 產業在變什麼、工程師該注意什麼——拆清楚寄到你的信箱。