この記事で得られること
当社ではClaude Codeを「AI経営チーム」として使っていて、33個のlaunchdジョブが毎日勝手に動いています。その中核にあるのが Orchestratorパターン(指揮者エージェント+部門サブエージェント)です。
この記事では、実際に動いている設定ファイルとシェルスクリプトをそのまま引用しながら、次の3点を解説します。
- CLAUDE.mdを「薄い指揮者」に保つルールの書き方
-
.claude/agents/にサブエージェントを分離する方法 - launchd + シェルスクリプトで
claude -pを定期起動し、コストとログを管理する方法
対象はClaude Codeを使い始めて「CLAUDE.mdがどんどん長くなって困っている」人です。経営の話は最初と最後だけで、本体は実装です。
なぜ「指揮者を太らせない」のか
僕が最初にやった失敗は、CLAUDE.mdにルールを追加し続けたことでした。気づいたら1,000行を超え、ルール同士が矛盾し、AIが指示と逆の動作を始めました。「レビューしてから公開」と「承認不要で自動実行」が同じファイルの別の場所に書いてある、という状態です。
そこで方針を変えました。CLAUDE.mdは200行以内の「振り分け担当」にして、実作業の知識は全部 .claude/agents/ に逃がす。これがOrchestratorパターンの出発点です。
1. CLAUDE.md を薄い指揮者にするルール
当社のCLAUDE.mdには、指揮者が守るべき原則がこう書いてあります。
## Thin Orchestratorの原則
- **コンテキスト使用率を10-15%に維持する**
- ファイルの中身を自分のコンテキストに読み込まず、**ファイルパスだけを渡す**
- 複雑なタスクは必ず `.claude/agents/` のサブエージェントに委任する
- 自分で実作業(コーディング、文書作成等)を行わない
ポイントは2行目です。指揮者がファイルを Read した瞬間、その中身はメインセッションのコンテキストに乗ります。10部門分のSTATE.mdを読んでからサブエージェントを呼ぶ、という素朴な実装にすると、指揮者が一番太ります。読むのはサブエージェントの仕事、指揮者はパスを渡すだけにします。
委任のときに何を渡すかも、CLAUDE.mdに固定しています。
## サブエージェントへの委任方法
タスクをサブエージェントに委任する際は、以下の情報を渡す:
1. **タスクの目的** — 何を達成するか(1文で)
2. **参照ファイルパス** — 必要な入力ファイルのパス一覧
3. **成果物の出力先** — 出力ファイルのパスとフォーマット
4. **権限レベル** — read-only / draft / execute
5. **品質基準** — 完了条件と検証方法
これを書いておくと、自然言語で「記事書いて」と言っただけでも、指揮者が5項目を埋めてからサブエージェントを起動するようになります。
落とし穴:自然言語の振り分け表を書いておく
指揮者の仕事は「意図の理解と振り分け」です。当社ではコマンド名を覚えなくて済むように、対応表をCLAUDE.mdに置いています。
| CEOの発言 | 自動実行 |
|----------|---------|
| 「今日の状況は?」 | → 全部門の状態・KPI・承認待ちを表示 |
| 「○○の記事を書いて」 | → コンテンツエンジン: プラットフォーム別に品質基準に従い記事作成 |
| 「書籍の品質を確認して」 | → 出版部門: スコアリング実行 |
この表があると指揮者は「どのエージェントに投げるか」だけ判断すればよく、記事の書き方やスコアリング基準をCLAUDE.md内に持つ必要がなくなります。
2. サブエージェント定義(.claude/agents/*.md)
サブエージェントはMarkdown1枚です。フロントマターで名前・説明・使えるツールを宣言し、本文がシステムプロンプトになります。朝ダイジェスト用エージェントの実物がこれです。
---
name: ai-ceo-morning
description: 朝ダイジェストエージェント。全部門の状態を収集し、CEO向けサマリを生成する。
tools:
- Read
- Grep
---
# AI-CEO Morning Digest Agent
## 実行手順
1. `.company/approval-queue.md` を読み取り、承認待ちアイテムを収集
2. `.company/departments/*/STATE.md` を全て読み取り、各部門の状態を収集
3. `.company/products/*/STATE.md` を全て読み取り、プロダクト状態を収集
## 制約
- read-onlyエージェント。ファイルの書き込みは行わない
- 全ファイルの中身を丸ごと含めず、要約のみ出力する
- 出力はメインセッション(Orchestrator)に返す
設計上のポイントが3つあります。
-
toolsを最小にする。 朝ダイジェストはReadとGrepだけ。書き込みできないので、レポート系エージェントが勝手にファイルを壊す事故が構造的に起きません - 「要約のみ出力する」を明記する。 サブエージェントの出力はそのまま指揮者のコンテキストに戻ります。ここでファイルを丸ごと返されると、せっかく分離した意味がなくなります
- 実行手順にパスを書く。 指揮者はパスを渡すだけ、サブエージェントは自分で読みに行く、という責務分担がここで完成します
書き込みが必要なエージェントは tools を広げます。コンテンツ生成エージェントのフロントマターは次の通りです。
---
name: ai-ceo-content-engine
description: コンテンツ量産エージェント。SEO記事、Zenn書籍、LP、広告コピーを高速に量産し、オーガニック流入と売上を増やす。
tools:
- Read
- Write
- Edit
- Bash
- Grep
---
description は指揮者が「どのエージェントに投げるか」を判断する材料になるので、名詞の羅列ではなく「何を達成するエージェントか」を書いておくと振り分け精度が上がります。
落とし穴:スキルは <名前>/SKILL.md 形式
当社は .claude/skills/write-qiita.md のように単一ファイルで13個のスキルを置いていましたが、正しい形式は .claude/skills/write-qiita/SKILL.md です。これに気づくまで6ヶ月間、スキルが1つも読み込まれていませんでした。設定したつもりで動いていない、は多エージェント構成で一番怖い事故なので、後述のヘルスチェックで個数を監視しています。
3. 定期起動スクリプト:claude -p の共通ラッパー
指揮者とサブエージェントは対話セッションの話ですが、「毎日勝手に動く」ためにはlaunchdから claude -p を叩く必要があります。無人実行で困るのは、モデル混雑でジョブが落ちることと、コストが見えないことです。両方を1つの関数に閉じ込めました。
# .company/scripts/lib/claude-run.sh
# claude を無人実行するときの共通ラッパー。実コストを記録する。
#
# 使い方:
# source ~/workspace/one-ceo/.company/scripts/lib/claude-run.sh
# RESULT=$(claude_run dept-dev --effort low -p "…")
#
# 注意: --bare は認証を読まないため "Not logged in" で失敗する。使わない。
claude_run() {
local label="$1"; shift
claude --output-format json \
--fallback-model claude-sonnet-5 \
--max-budget-usd 3 \
"$@" 2>/dev/null \
| python3 "$_CLAUDE_RUN_LIB/record-cost.py" "$label"
}
-
--fallback-model: 主モデルが混雑しているときに落ちる代わりにSonnetで完走させる -
--max-budget-usd 3: 実測は1回あたり数十セントなので通常は当たらず、ループ暴走のときだけ効く安全弁 -
--output-format json: 応答にtotal_cost_usdが含まれるので、パイプ先のPythonで実測コストを積み上げる
以前は各スクリプトが自己申告で「概算 $0.02」を記録していて、実測すると桁が違いました。コストはラッパーで強制的に実測するのが正解です。
このラッパーを使う部門スクリプトは、プロンプトを渡すだけの短いものになります。出版部門の週次ジョブの核心部分です。
# .company/scripts/auto-dept-publishing.sh
# launchd: com.joinclass.auto-dept-publishing(毎週水曜 7:00 JST)
source "$HOME/workspace/one-ceo/.company/scripts/lib/claude-run.sh"
cd "$PROJECT_ROOT"
RESULT=$(claude_run dept-publishing --effort low -p "
あなたは出版部門のPublisherエージェントです。以下のタスクを実行してください。
1. 全書籍の状態を確認:
- Zenn: ~/workspace/zenn-content-company/books/ の各config.yaml
- Kindle: .company/departments/publishing/STATE.md
2. 各書籍のZenn上の情報を確認(公開状態、章数)
3. 改善提案があれば1-2個提示
" --allowedTools "Read,Bash" 2>/dev/null | tail -20)
echo "$(date): 結果: $RESULT" >> "$LOG_FILE"
if [ -n "$RESULT" ]; then
notify_slack "📚 [出版部門 週次レポート]\n${RESULT}"
fi
ここでも --allowedTools "Read,Bash" でツールを絞っています。無人実行で Write を許すのは、承認パイプラインを通した一部のジョブだけです。
4. 共通ヘルパー:ログ・リトライ・コスト上限
スクリプトが30個を超えると、ログ形式やリトライの書き方がバラバラになります。当社は common.sh に寄せています。
# .company/scripts/common.sh(抜粋)
set -euo pipefail
log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $*"; }
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; }
# エラー時Slack通知トラップ
on_error_notify() {
local exit_code=$?
local line_no=${1:-unknown}
log_error "スクリプト失敗: $SCRIPT_NAME (行:$line_no, 終了コード:$exit_code)"
if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then
curl -sf -X POST -H "Content-Type: application/json" \
-d "$(jq -n --arg text ":x: [AI-CEO] $SCRIPT_NAME 失敗 (行:$line_no, コード:$exit_code)" '{text: $text}')" \
"$SLACK_WEBHOOK_URL" >/dev/null 2>&1 || true
fi
}
trap 'on_error_notify $LINENO' ERR
# リトライ実行(最大3回)
retry() {
local max_attempts=3
local attempt=1
local cmd="$@"
while [ $attempt -le $max_attempts ]; do
if eval "$cmd"; then
return 0
fi
log_info "リトライ $attempt/$max_attempts: $cmd"
attempt=$((attempt + 1))
sleep 2
done
log_error "3回失敗: $cmd"
return 1
}
# コスト上限チェック(実行前に呼ぶ)
check_cost_limit() {
local tracker="$COMPANY_DIR/scripts/cost-tracker.json"
[ -f "$tracker" ] || return 0
local total=$(jq '.total_usd' "$tracker")
if (( $(echo "$total >= 200" | bc -l) )); then
log_error "月間コスト上限到達: \$${total}。実行を中止。"
return 1
fi
return 0
}
「3回リトライして駄目なら止める」はシェル側だけでなく、CLAUDE.md側にも同じルールを書いています。
## エラー時の振る舞い
- サブエージェントが失敗した場合: エラー内容をフィードバックして最大3回リトライ
- 3回失敗: `.company/approval-queue.md` にエスカレーションとして追加し、CEOに報告
- エラーログ: `.company/departments/{dept}/error-log.md` に追記
対話セッションでもバッチでも、失敗の扱いを揃えておくと、翌朝ログを見たときに「どこで止まったか」がすぐ分かります。
落とし穴:同時起動とレートリミット
Zennには1日の投稿数に制限があり、記事と書籍を同じ日に公開しようとしてデプロイが何度も失敗しました。対策は単純で、ロックファイルで「1日1コンテンツ」をスクリプト側で強制することです。launchdのジョブは互いの存在を知らないので、排他制御はファイルシステムでやるのが一番確実でした。
5. launchd への登録
macOSのcronはフルディスクアクセスの制約で全滅したことがあり、当社はlaunchdに全面移行しています。plistはこの形です。
<!-- ~/Library/LaunchAgents/com.joinclass.auto-dept-publishing.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.joinclass.auto-dept-publishing</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>~/workspace/one-ceo/.company/scripts/auto-dept-publishing.sh</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>~/.nvm/versions/node/v22.17.0/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>WorkingDirectory</key>
<string>~/workspace/one-ceo</string>
<key>StandardOutPath</key>
<string>~/workspace/one-ceo/.company/scripts/logs/auto-dept-publishing.log</string>
<key>StandardErrorPath</key>
<string>~/workspace/one-ceo/.company/scripts/logs/auto-dept-publishing-error.log</string>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>3</integer>
<key>Hour</key>
<integer>7</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>
ハマりどころは EnvironmentVariables の PATH です。launchdのPATHは極端に短く、nvmで入れたnode配下の claude が見つかりません。plistとスクリプト両方で明示しています(実ファイルでは ~ ではなく絶対パスです)。
ジョブが増えたら、一覧をMarkdownで棚卸しします。当社の JOBS.md の抜粋です。
## 部門エージェント
| ジョブ | スケジュール | 役割 |
|---|---|---|
| auto-dept-dev | 毎日 08:30 | 開発部門 自律実行 |
| auto-dept-marketing | 毎日 09:00 | マーケ部門 自律実行 |
| auto-dept-sales | 毎日 09:30 | 営業部門 自律実行 |
| auto-dept-publishing | 毎週水 | 出版部門 自律実行 |
この棚卸しをしたら、帳簿上17個のはずのジョブが実態は33個あり、しかもQiita公開ジョブが Disabled: True のまま5ヶ月止まっていました。ジョブ一覧は人間が読むドキュメントとして残し、月1回は実態と突き合わせることを勧めます。
6. 検証:動いているかをどう確かめるか
ログとlaunchdの状態
# ロード済みジョブの確認
launchctl list | grep com.joinclass
# 直近の実行ログ
tail -n 30 ~/workspace/one-ceo/.company/scripts/logs/auto-dept-publishing.log
# 手動で即時実行して動作確認
launchctl start com.joinclass.auto-dept-publishing
# 実測コストの累計
jq '.total_usd, .calls' ~/workspace/one-ceo/.company/scripts/cost-tracker.json
エージェント構成の月次ヘルスチェック
「設定したつもり」を検知するため、エージェント数・スキル数・ジョブ数・STATE.mdの鮮度を毎月1日にSlackへ送っています。
# .company/scripts/auto-agent-health-check.sh(抜粋)
AGENT_COUNT=$(ls "$AGENTS_DIR"/*.md 2>/dev/null | wc -l | tr -d '[:space:]')
SKILL_COUNT=$(ls "$SKILLS_DIR"/*.md 2>/dev/null | wc -l | tr -d '[:space:]')
for state in "$DEPTS_DIR"/*/STATE.md; do
dept=$(basename "$(dirname "$state")")
FILE_MOD=$(stat -f %m "$state" 2>/dev/null || echo 0)
DAYS_OLD=$(( (CURRENT_DATE - FILE_MOD) / 86400 ))
if [ "$DAYS_OLD" -gt 30 ]; then
STALE_DEPTS="${STALE_DEPTS} - ${dept}: ${DAYS_OLD}日前\n"
fi
done
LC=$(launchctl list 2>/dev/null | grep -c "com.joinclass" || true)
指揮者のコンテキスト使用率
対話セッションでは /context で使用率を見ます。当社の目安は10〜15%です。これを超えていたら、指揮者がどこかでファイルを読んでいます。CLAUDE.md の「パスだけを渡す」ルールを守れているかを、ここで定期的に確認しています。
まとめ
- CLAUDE.md は振り分け表と委任ルールだけ。 知識はサブエージェントに逃がす
-
サブエージェントは
toolsを最小化し、「要約のみ返す」と書く。 指揮者に戻る出力を細くする -
無人実行は
claude -pをラッパー関数に閉じ込める。 fallback・予算上限・実測コストを1か所で管理 - 失敗は3回リトライ → エスカレーション。 シェルとCLAUDE.mdで同じルールにする
- 棚卸しとヘルスチェックを定期ジョブにする。 止まっているのに気づかないのが最大のリスク
「AIは実行に使う。判断は人間。」が当社の方針で、Orchestratorパターンはそれを構造で実現する方法です。朝の5分で全部門の状況を把握して承認ボタンを押すだけ、という運用は、指揮者を痩せさせることで初めて成り立ちました。
さらに深く知りたい方へ
この記事の内容を、サブエージェント間の通信設計・承認パイプライン・エラー時の復旧まで含めて体系的にまとめた書籍が『Claude Codeマルチエージェント開発』です。本記事では触れなかった部門間の依存関係解決や、draft → 承認 → 実行のパイプライン実装を扱っています。
CLAUDE.md自体をどう薄く保つか、どの知識をどこに置くかに特化した『CLAUDE.md設計パターン』も、本記事の第1章の詳細版として読めます。
書籍一覧: https://zenn.dev/joinclass?tab=books
Claude Codeを使った業務自動化の導入支援も行っています。自社に合った構成で迷っている方はお気軽にご相談ください。