zudo-paper

エージェントらには作業ログを残させると良いかもよ?というTips

Author: Takazudo | 作成: 2026/03/14

概要

Claude Codeでサブエージェントなり、エージェントチームなりを使うのであれば、そのエージェント間のやりとりのためにログファイルのようなものを残させ、それをやりとりさせた方が良いんでないの? というまとめ。

レビュー結果の報告例: 簡潔なステータスとログファイルパスだけが親エージェントに送られる

注記

この記事で紹介しているスキル、エージェント定義、スクリプトは claude-resources リポジトリで公開している。記事中のリンクは執筆時点のコミットに固定してある。最新版とは異なる場合がある。

モチベーション

Claude Codeのサブエージェントはそもそも中身が見えない。Agent Teamsで子エージェントを走らせたとき、何をやったのかの詳細は親エージェントへの SendMessage 経由でしか伝わらない。セッション中はまだしも、セッションが終わったら記録が残らない。--resume で再開する手もあるが、長いセッションのコンテキストをまるごと引き継ぐのは効率的でない。

Claude Codeのそういうサブ的なセッションがどうしているのか全然分からんなと思い、サブエージェント機能が登場したとき、何をやったのかのレポートを__inbox/yyyymmdd-{report}.mdみたいなファイルに記録させるようにしていた。reviewerは指摘内容をまとめたファイルを作る。researcherは調査結果をファイルに残す。そういうmarkdownファイルを作るという具合に。

__inbox/はとりあえずtemp的なファイルをひたすら置いておく場所で、取りあえずそこに記録を残させては、たまに確認していたり──というのを始めの頃はしていたが、Claude Code自体の進化が劇的になり、もうボコボコサブエージェントを立ち上げるだけでなく、エージェントチームみたいな機能も登場したこともあり、細かくログを見る機会はかなり少なくなっており、ただただ__inbox/が色んな所に作られているだけだった。

それでClaude Code環境を整理していて、んー見てないしこれもうやめていいかなと思っていたのだが、あ、これを親子間でやりとりしたら良くない? と考えた。

~/cclogs/ へのログ集約

というわけで、この__inbox/保存をやめ、保存先は ~/cclogs/{repo-name}/ に一元化するようにした。git worktreeを使って複数のサブエージェントを同時に走らせると、各worktreeにログが散るとどこに何があるか分からなくなる。worktreeは作業が終われば消すこともあるので、ログごと消える。一箇所に集めておけば、新しいセッションの冒頭で「直近のログを見せて」とか「直近のログを見て状況を把握して」というのが出来る。「今このレポジトリでは〜〜をしていて……」という説明をせずとも。

結果、作業をしていくと以下のようなディレクトリに、エージェントらが書いたmarkdownのログファイルが貯まっていくようにする。

~/cclogs/project-hoge/
~/cclogs/moge-app/
~/cclogs/bar-articles/

get-logdir.js: ログディレクトリの算出

中心にあるのは get-logdir.js というESMモジュールで、カレントディレクトリのgitリポジトリ名からログディレクトリのパスを算出して返す。

import { execSync } from "child_process";
import path from "path";
import fs from "fs";
import os from "os";

function sanitizeSlug(raw) {
  const cleaned = raw.replace(/[^a-zA-Z0-9._-]/g, "_");
  if (cleaned === "." || cleaned === ".." || cleaned === "") {
    return "_unnamed";
  }
  return cleaned;
}

export function getLogDir() {
  try {
    const toplevel = execSync("git rev-parse --show-toplevel", {
      encoding: "utf8",
      stdio: ["pipe", "pipe", "pipe"],
    }).trim();
    const gitPath = path.join(toplevel, ".git");
    let slug;
    if (fs.statSync(gitPath).isFile()) {
      const commonDir = execSync("git rev-parse --git-common-dir", {
        encoding: "utf8",
        stdio: ["pipe", "pipe", "pipe"],
      }).trim();
      slug = path.basename(path.dirname(path.resolve(commonDir)));
    } else {
      slug = path.basename(toplevel);
    }
    return path.join(os.homedir(), "cclogs", sanitizeSlug(slug));
  } catch {
    return path.join(os.homedir(), "cclogs", "_misc");
  }
}

設計上のポイントをいくつか。

sanitizeSlug はリポジトリ名に空白や特殊文字が含まれる場合の対策。英数字とドット、ハイフン、アンダースコア以外を _ に置換する。.. によるパストラバーサル防止も兼ねていて、... だけのスラグは _unnamed にフォールバックする。

worktree検出は .git がファイルかディレクトリかで判定している。通常のリポジトリでは .git/ はディレクトリだが、worktreeでは .git はテキストファイルで、中身は gitdir: /path/to/main-repo/.git/worktrees/branch-name のような1行になっている。fs.statSync(gitPath).isFile() でworktreeかどうかが分かる。

worktree内にいる場合、ログの保存先はworktreeではなくメインリポジトリのスラグで決める。そうしないとworktreeごとにログディレクトリが分かれてしまい、一箇所に集約するという目的が達成できない。git rev-parse --git-common-dir は共有の .git ディレクトリのパスを返すコマンドで、worktreeからでもメインリポジトリからでも同じパスが返る。ただしこのコマンドは相対パスを返す場合があるので、path.resolve() で絶対パスに変換してからスラグを取得している。

gitリポジトリ外で実行された場合は _misc にフォールバックする。

CLIモードもある。node ~/.claude/scripts/get-logdir.js で標準出力にパスを出力する。シェルスクリプトやスキルの中で以下のように使える。

LOGDIR=$(node ~/.claude/scripts/get-logdir.js)

save-file.js のプレースホルダーシステム

自分のClaude Code環境では save-file.js というスクリプトでファイル保存を行っている。引数としてファイルパスと内容を受け取り、パス中のプレースホルダーを展開してからファイルを書き込む。

プレースホルダーとして {timestamp}, {date}, {time}, {datetime}, {logdir} がある。エージェント定義ファイルからは以下のように使う。

~/.claude/scripts/save-file.js "{logdir}/{timestamp}-reviewer-{context}.md" "content"

実行時にプレースホルダーが展開されて、実際のパスはこうなる。

/Users/takazudo/cclogs/my-project/0314_1530-reviewer-auth-refactor.md

{logdir} がリポジトリのログディレクトリに、{timestamp} が日時に、{context} はエージェントが自分で判断するファイル名のサフィックスに、それぞれ展開される。

{logdir} の解決にはlazy評価を入れている。{logdir} は内部で execSync を使ってgitコマンドを実行するので、{logdir} を使わないファイル保存のときにもgitが走るのは無駄。プレースホルダーが含まれているときだけ getLogDir() を呼ぶ。

if (filePath.includes("{logdir}")) {
  replacements["{logdir}"] = getLogDir();
}

エージェント定義ファイルとスキル

Claude Codeでは ~/.claude/agents/ にmarkdownファイルを置くことでサブエージェントの振る舞いを定義できる。各エージェントのmarkdownファイルにログの保存先とフォーマットの指示を含めている。

{logdir}/ プレフィックスを使っているのは以下のファイル。

各エージェントにはファイル名のプレフィックスが決まっていて、reviewer-, research-, frontend-dev-, wt-child- などが付く。これにより /logrefer でエージェント種別ごとにフィルタできる。

save-file.jsのプレースホルダーシステムを使わないスキルもある。youtube-text-fetchcommits はCLIモードでログディレクトリのパスを取得して自前でファイルを書き込む。

LOGDIR=$(node ~/.claude/scripts/get-logdir.js)
mkdir -p "$LOGDIR"
python3 -c "..." > "$LOGDIR/youtube-VIDEO_ID.txt"

/logrefer: ログ閲覧スキル

ログを一箇所に集約しただけだとファイルがどんどん溜まるだけなので、ログを閲覧するためのスキル /logrefer も作っている。

引数パターンは以下。

  • 引数なし: 直近10件のファイル名と最初の見出しを表示
  • 数値(例: 20): 表示件数を変更
  • プレフィックス(例: reviewer): エージェント種別でフィルタ
  • read <filename>: ファイル全文を表示

内部では LOGDIR=$(node ~/.claude/scripts/get-logdir.js) でパスを取得し、ls -t "$LOGDIR" で新しい順にファイル一覧を取得して、各ファイルの最初のmarkdown見出しを抽出して一覧表示する。

このスキルにより、「/logrefer さっきまでやってたやつ続きやって」的なプロンプトが可能。

ディレクトリ構造

最終的にこうなる。

~/cclogs/
├── my-project/
│   ├── 0314_1530-reviewer-auth-refactor.md
│   ├── 0314_1200-research-caching-strategies.md
│   ├── 0314_0900-x-wt-teams-marker-fix.md
│   └── headless-screenshots/
│       └── screenshot-2026-03-14.png
├── another-project/
│   └── 0313_0900-frontend-dev-dashboard.md
└── _misc/
    └── 0312_1600-research-misc-topic.md

プロジェクトごとにディレクトリが分かれていて、中にはエージェント名がプレフィックスとして付いたファイルが並ぶ。gitリポジトリ外で実行された場合は _misc にフォールバックする。

ここまでがログ集約の基盤。以下は、この基盤の上に乗せたパターンの話。

エージェント間通信: 簡潔なステータス + ログファイルパス

コンテキスト圧迫の問題

マルチエージェントのワークフロー、例えば /x-wt-teams では親エージェント(マネージャー)が3〜4個の子エージェントにタスクを振る。各子エージェントは作業が終わると SendMessage で親に結果を報告する。

この報告が問題になることがある。子エージェントは真面目に、何を実装したか、どういう判断をしたか、何に注意すべきか、全部まとめて複数パラグラフで報告してくる。3〜4エージェント分のレポートが親のコンテキストウィンドウに入ると、かなりの量になる。

大量のPDFの翻訳をサブエージェントにさせたことがあったが、その時はこの親が子からのメッセージを受け取るだけでトークンが圧迫されてしまい、うまくいかないことがある。これに遭遇したときは、ルールに沿ったテキストファイルを作らせることで解決できた。普段はそんなに意識しないが、数が増えてくるとこのエージェント間のやりとりに多くのトークンを使用してしまうということの模様。

パターン: 通知 + 実体の分離

ログが ~/cclogs/ に集まっているなら、子エージェントは全部をメッセージで送る必要がない。詳細はログファイルに書いて、親には以下だけ送る。

  • ステータス(1〜2文)
  • PR URL(該当する場合)
  • ログファイルパス

GitHubの通知と実体の関係に近い。issueのコメント通知はメールで来るが、実際の差分やコードはPR上にある。通知を読んで「詳細を見る必要があるか」を判断できるし、必要なら本体を開く。

親エージェントも同じ。ステータスが「実装完了、テスト通過」なら詳細を読まずに次に進める。「一部実装できなかった」なら /logrefer read <filename> でログを読んで対応を判断する。

このパターンをworktree子エージェント、frontend-developer、reviewerサブエージェントすべてに適用している。エージェント定義やスキル定義には「SendMessageでは簡潔なステータス、PR URL、ログファイルパスだけ送ること。Do NOT send full summaries」と明記してある。“Do NOT send full summaries”をわざわざ大文字で書いているのは、エージェントは指示しないとつい全文報告をしてしまうから。

これを整備していたら、だったら親の方もログを残せば良いんでない? と思い、もうとにかく皆が皆、それぞれのログを残させるようにした。日報的な……。

開発ワークフローとレビューの組み合わせ

普段の開発では、規模に応じて2つのワークフロースキルを使い分けている。

小さめの実装には /x-as-pr を使う。ブランチを切ってdraft PRを開き、実装してレビューしてPRにするまでを1セッションで完結させるスキル。単一トピックのバグ修正や機能追加はだいたいこれで済む。

複数トピックを並行して進めたいときは /x-wt-teams を使う。親エージェントがマネージャーとして3〜4個のworktreeを作り、各worktreeに子エージェントを配置してタスクを振る。子エージェントたちが並行して実装し、完了したら親がマージしてPRにする。

レビューもこの2つに合わせて段階を変えている。

/local-review はOpusモデルで3つ以上のreviewerを走らせるディープレビュースキル(プロジェクト全体のフルレビューでは6 reviewers)。/x-as-pr のワークフローではこれを使う。単一トピックの実装が終わった後にかける。

/light-review はSonnetモデルで2つのreviewerを走らせる軽量版。bugs & logicとquality & structureの2名。/x-wt-teams の子エージェントが自分のworktreeでの変更をさっとチェックするのに使う。

どちらのレビュースキルも同じ通信パターンを使う。reviewerが指摘の全文をログファイルに書き、親には高優先度の項目だけ箇条書き(最大5項目)とログパスを返す。

/x-wt-teams ではこの2つが段階的に使われる。子エージェントが実装を終えたら、親に報告する前に /light-review で自己レビューする。マネージャーが全トピックをマージした後に /local-review(Opus、3 reviewers)をかける。子エージェントレベルで拾える問題は既に潰されているので、マネージャーのレビューは統合後の全体的な整合性に集中できる。安い高速フィルタをトピックレベルでかけて、高コストの徹底レビューを統合レベルでかけるという2段構成。

結果、普段の開発で ~/cclogs/ に残るログはだいたい以下のパターンになる。

  • ワークフロースキル(/x-as-pr, /x-wt-teams)のセッションレポート
  • レビュースキル(/local-review, /light-review)のreviewerごとの指摘ログ
  • 各エージェント(frontend-developer, frontend-worktree-child, researcher など)の作業ログ

開発の大半はこの2つのワークフロースキル経由で行うので、ログの網羅性はそれなりに高い。何をやったか、何が指摘されたか、どういう判断をしたかが ~/cclogs/ を見れば大体分かるという状態になる。

セッションレポートと次のセッションへの接続

上記のセッションレポートは、ワークフロー完了時に {logdir}/{timestamp}-x-as-pr-{slug}.md{logdir}/{timestamp}-x-wt-teams-{slug}.md として保存される。内容は、何を実装したか、レビューで何が見つかったか、CIの結果、PR URLなど。

GitHub issueがリンクされている場合は、このレポートをissueコメントとしても投稿する。issueを見れば「このissueに対して何が行われたか」が分かるようになる。

これが /logrefer のループにつながる。次のセッションで /logrefer を実行すると「前回、/x-wt-teams で3つのトピックを実装して、reviewerがこういう指摘をして、CIは通って、PR #42ができた」みたいなことが分かる。前のセッションのアウトプットが、次のセッションのインプットになる。

文脈の蓄積

まとめると以下のようになる。

  • ログが ~/cclogs/{repo-name}/ に集まる
  • エージェント間の通信は「簡潔な通知 + ログファイルパス」で、コンテキストを圧迫しない
  • レビュー結果はコンテキストウィンドウの中身ではなく永続的なログファイルとして残る
  • ワークフロースキルはセッションレポートを生成する
  • /logrefer が次のセッションへの入り口になる

各セッションが ~/cclogs/{repo-name}/ にログを追加していく。reviewerの指摘、researcherの調査結果、ワークフローのセッションレポート。時間が経つと、このディレクトリがプロジェクトの作業履歴になっていく。gitのコミットログとは別の粒度で、エージェントの作業記録が残る。「簡潔なステータス + ログファイルパス」のパターンにより、エージェントのコンテキストは軽く保ちつつ、詳細はいつでも参照できる。

その他、端末間での同期だったり、ログが増えすぎた場合にどうするなども考えられそうだが、取りあえずそこまで重要なファイルでもないので、ひとまずこれでやってみるかというところ。ちょっとこの環境で実装を進めてみたら、これらのエージェントからの報告がファイル名の受け渡しに変わっていることが確認できた。

余談

なお自分はClaude Code主体になってからは、GitHub issueは一時的なメモとして使っている。一連の作業の概要と進捗を記録させ、終わったら閉じる。

ドキュメントに残しておくまでもないようなことはGitHub issueにメモ的に書かせるが、一応後から探そうと思えば探せるという具合。真面目に要件をまとめてGitHub issueに書くようなことはもう無くなり、そういう重要っぽい仕様は別途ドキュメントにまとめさせるのが良いかなと考えてる。