BridgeCraft logobridgecraft
週報作品集關於聯絡登入
聯絡

bridgecraft

從資料視覺化到 AI Agent,一直在找下一個 meta

文章主題指南週報Labs作品集系統架構關於聯絡RSS

© 2026 BridgeCraft — Waiting7777. All rights reserved.

← 所有文章

用 launchd 跑 Autonomous Agent:一台 Mac、兩個班、兩週 17 個 PR

2026年10月20日 · Waiting7777 · 5 分鐘閱讀

AI Agents自動化測試Claude
📂 AI Agents 系列📂 自動化測試 系列📂 Claude 系列

這是 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 或小團隊來說,這些都是過度工程。

四個實際成本:

  1. 雙倍開銷:Claude API 的 token 費已經不便宜,還要疊 cloud infra 費
  2. Debug 距離變長:logs 在 CloudWatch / Datadog / Loki,每次回看都要 round-trip
  3. Credential 管理:本地 .env.test 一行搞定的事、雲上要 IAM role + secret manager
  4. 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

關鍵四點:

  1. Lock 帶 PID + epoch timestamp(用 | 分隔)— 不只看檔案存在、還要確認 PID 真的活著
  2. kill -0 檢查活體:這個命令不真的殺、只回傳「這個 PID 是否存在」
  3. Stale 保護(MAX_LOCK_AGE=14400):活著但超過 4 小時也判定異常(可能是 pipeline 卡死)
  4. 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 特有的陷阱:

  1. plist 放好不會自動 load — 第一次要 launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.qa-shift.plist。unload 用 launchctl bootout。老文章會看到 launchctl load/unload、那是 deprecated 的寫法、現在該用 bootstrap/bootout。
  2. launchd 的 PATH 不是你 shell 的 PATH — 所以 plist 裡要顯式 EnvironmentVariables > PATH。沒這條 claude CLI 找不到。
  3. MacBook 睡著 launchd 不跑 — 解法:caffeinate、或設成「闔蓋不睡眠」(Preventing Sleep)、或接電源時才跑。我選最後一個。
  4. LaunchAgents 跟 LaunchDaemons 別混 — Agents 是使用者 session 內、Daemons 是系統全域。這個需要登入 session(Claude CLI 要讀 keychain),所以是 Agents。
  5. 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 思維拆解技術趨勢。

關於作者

相關文章

Orca 七個月拿下 10 萬顆星:我拆了它的 GitHub 數據,看到 agent 開發真正的瓶頸AI Agents開發者工具+1

Orca 七個月拿下 10 萬顆星:我拆了它的 GitHub 數據,看到 agent 開發真正的瓶頸

一個由 agent 寫出來、拿來管 agent 的開源 IDE。19,000 個 commit、979 次 release 背後,最值得看的數字是 2,525 個放了一個月還沒人處理的 PR。

2026年10月20日 · Waiting7777 · 7 分鐘閱讀

繼續閱讀 →
跑了兩週 Agent QA Pipeline — 真實數字、踩到的坑、重來我會怎麼做AI Agents自動化測試+1

跑了兩週 Agent QA Pipeline — 真實數字、踩到的坑、重來我會怎麼做

這是 post-mortem,不是 case study。做對的三件事、做錯的三件事、以及為什麼這套東西接下來要抽成 `claude-qa-pipeline` OSS。

2026年10月20日 · Waiting7777 · 6 分鐘閱讀

繼續閱讀 →
你的 100% Coverage 是假的 — 用 test-audit 抓出沒被真正測到的分支自動化測試開發者工具

你的 100% Coverage 是假的 — 用 test-audit 抓出沒被真正測到的分支

AI 寫的 code 跑出漂亮的覆蓋率數字,但 error path 根本沒人測過,這工具專門抓這個。

2026年10月20日 · Waiting7777 · 3 分鐘閱讀

繼續閱讀 →

這篇文章對你有幫助嗎?

每週一篇 — 技術趨勢背後的商業邏輯

AI 產業在變什麼、工程師該注意什麼——拆清楚寄到你的信箱。