0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

会話ログを夜中に自動スキル化する ― Claude が自分の記憶から学ぶ仕組み

0
Posted at

前回の記事 では自動生成されたスキルのライフサイクル管理(stale → archive の 2 段退避)を書きました。今回はその手前 ―― 会話ログを毎晩 3:30 に読んで、再利用できる手順だけを抽出し SKILL.md として書き出す harvest バッチの内部構造を全部さらします。

いま ~/.claude/skills/auto/ には 104 個のスキルが積み上がっています。そのほぼ全部が skill-harvest.sh が無人で生成したものです。

困りごと:3349 本のログを毎晩全部読ませたら枠が消し飛ぶ

~/Documents/my-knowledge-base/raw/conversations/ には今日時点で 3349 本の会話ログが蓄積されています。毎晩それを全部 LLM に渡せば、確かにスキル候補は拾えます。ただしコストは?

3349 本 × 1 万トークン/本(平均見積り)= 3300 万トークン。Max 枠を毎晩これで食いつぶせば、他のジョブが全滅します。

これを 2 つの仕組みで潰しています。

  1. ウォーターマーク方式:前回処理済みのファイルは二度と読まない
  2. バジェットキャップ:1 回あたりの LLM 消費を --max-budget-usd 1.20 で上限固定

2 つを組み合わせることで、毎晩の harvest を「差分 3 本・45KB 固定」に収めています。

全体パイプライン:5 フェーズ

~/.claude/scripts/skill-harvest.sh は次の流れで動きます。

1. ウォーターマーク読み込み → 差分ログを 3 本だけ取得
2. system-reminder ノイズを除去 → バイト上限でダイジェスト生成
3. claude -p ヘッドレス → ステージングに SKILL.md を書かせる
4. author:auto を保証 → AUTO ディレクトリへシェルがコピー
5. ウォーターマーク更新 → ログ追記して終了

パラメータは 4 つだけです(skill-harvest.sh 冒頭より)。

MAX_LOGS=3              # 1回で扱うログ本数
PER_LOG_BYTES=15000     # ログ1本あたりの取り込み上限バイト
BUDGET_USD=1.20         # 暴走防止コストキャップ
TIMEOUT_SEC=600         # 壁時計タイムアウト

ウォーターマーク方式で重複処理を防ぐ

~/.claude/skills/auto/.harvest-watermark が前回 harvest 完了時のタイムスタンプを記録しています。

# 前回以降に更新されたログを収集(初回は最新 MAX_LOGS 件)
if [[ -f "$WM" ]]; then
  newlogs=("${(@f)$(find "$LOGS" -name '*.md' -newer "$WM" 2>/dev/null)}")
else
  newlogs=("${(@f)$(ls -t "$LOGS"/*.md 2>/dev/null)}")
fi

# mtime 降順で上位 MAX_LOGS 件に絞る
newlogs=("${(@f)$(ls -t "${newlogs[@]}" 2>/dev/null | head -$MAX_LOGS)}")

find -newer "$WM" で「ウォーターマークより新しい」ファイルだけを抽出します。3349 本のうち昨晩から今朝に更新されたのは通常 3〜5 本 ―― 処理対象は桁違いに少なくなります。

終了直前で必ず watermark を更新します。

touch "$WM"
exit 0

実際のログで確認できます。

[2026-07-05 03:30:05] harvest start: 3 log(s), digest=   18255B
[2026-07-05 03:35:54] harvest done (exit 0, created=0)

毎晩 3 本・18KB ほどを処理しています。

$1.20 バジェットキャップで暴走コストを制御する

claude -p--max-budget-usd フラグに上限を渡します。

"$CLAUDE" -p "$PROMPT" \
  --model sonnet \
  --permission-mode acceptEdits \
  --allowedTools "Write Edit Read" \
  --max-budget-usd "$BUDGET_USD" >> "$LOG" 2>&1 < /dev/null

$BUDGET_USD=1.20 を超えた時点で claude が自動 abort します。今回は Claude Max 定額枠で回るため $1.20 が実費に直結するわけではありませんが、枠の消費量を固定して autopilot 等の他ジョブへの影響を限定する意味があります。

バジェットキャップだけでは壁時計時間を縛れません。TIMEOUT_SEC=600 の壁時計タイムアウトは perl -e 'alarm N; exec @ARGV' で実装しています。

( cd "$STAGING" && perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" \
  "$CLAUDE" -p "$PROMPT" ... )

タイムアウト時は exit 142(SIGALRM = 128+14)で落ちます。実際のログに残っています。

[2026-07-08 03:30:05] harvest start: 3 log(s), digest=   35159B
[2026-07-08 03:40:05] harvest done (exit 142, created=0)
[2026-07-09 03:30:06] harvest start: 3 log(s), digest=   35159B
[2026-07-09 03:40:15] harvest done (exit 142, created=0)

10 分で強制終了しています。タイムアウト時も created=0 で記録されるため翌日の harvest は継続して動きます。

macOS 標準の coreutils には GNU の timeout コマンドがありません。perl -e 'alarm N; exec @ARGV' が macOS でポータブルに使える代替手段です。gtimeout(Homebrew coreutils)でも動きますが、plist の PATH に依存するためより脆い。

system-reminder ノイズを事前フィルタしてプロンプトを軽量化する

会話ログには Claude Code の <system-reminder> タグやスキル・ツール一覧行がそのまま書き込まれています。これをプロンプトに突っ込むと、手順的知識と無関係なノイズがトークンの大半を占めます。

grep -v -e 'system-reminder' -e '^- [a-z0-9].*:' "$f" 2>/dev/null | head -c $PER_LOG_BYTES

除外しているのは 2 種類です。

  • system-reminder<system-reminder> を含む行を丸ごと除去
  • '^- [a-z0-9].*:':スキル・ツール一覧の - skill-name: description 形式の行を除去

これで 1 本のログから最大 $PER_LOG_BYTES=15000 バイトだけを取り込みます。MAX_LOGS=3 と組み合わせると、1 回の harvest で LLM に渡すのは最大 **45,000 バイト(約 45KB)**に固定されます。ログが何本に増えてもプロンプトサイズは変わりません。

ステージング経由でセキュアにファイルを書き込む

~/.claude/ 配下は Claude Code 起動中に書き込み保護されることがあります。claude -p に直接 ~/.claude/skills/auto/ への書き込みをさせると permission denied で落ちます。

STAGING=$(mktemp -d -t skill-harvest-stg)
( cd "$STAGING" && perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" \
  "$CLAUDE" -p "$PROMPT" \
  --permission-mode acceptEdits \
  --allowedTools "Write Edit Read" \
  --max-budget-usd "$BUDGET_USD" >> "$LOG" 2>&1 < /dev/null )

mktemp -d/tmp に一時ディレクトリを作り、cd "$STAGING" した状態で claude -p を走らせます。プロンプトには「カレントディレクトリ直下に ./kebab-name/SKILL.md を作れ(絶対パス禁止)」と明示しています。

- ファイル: ./<kebab-name>/SKILL.md (カレントディレクトリ直下に作る。絶対パス禁止)

claude が書き終わったあと、シェル側が $STAGING を走査して AUTO ディレクトリへコピーします。

for sd in "$STAGING"/*(/N); do
  [[ -f "$sd/SKILL.md" ]] || continue
  name="${sd:t}"
  [[ "$name" == .* ]] && continue
  # author: auto を保証(無ければ frontmatter 直後に挿入)
  grep -q '^author:[[:space:]]*auto' "$sd/SKILL.md" || python3 - "$sd/SKILL.md" <<'PY'
import sys,re
p=sys.argv[1]; s=open(p).read()
if s.startswith('---'):
    s=re.sub(r'^---\n', '---\nauthor: auto\n', s, count=1)
else:
    s='---\nname: %s\nauthor: auto\nversion: 1.0.0\n---\n'%__import__("os").path.basename(__import__("os").path.dirname(p))+s
open(p,'w').write(s)
PY
  [[ -e "$AUTO/$name" ]] || cp -R "$sd" "$AUTO/$name"
done
rm -rf "$STAGING"

author: auto が frontmatter にない場合は python3 ワンライナーで挿入してから移送します。この author: auto が curate の対象判定キーになるため必須です。

launchd 登録

plist のスケジュール部分です(~/Library/LaunchAgents/com.lily.skill-harvest.plist)。

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key><integer>3</integer>
    <key>Minute</key><integer>30</integer>
</dict>

毎日 3:30 に起動します。ProcessType: Background で動き、ログは ~/.claude/logs/com.lily.skill-harvest.log に吐かれます。

登録と即時テスト:

launchctl load ~/Library/LaunchAgents/com.lily.skill-harvest.plist
launchctl start com.lily.skill-harvest

踏んだ落とし穴

  • launchd の PATH が極小:素の launchd 環境では claude が見つからない。plist の EnvironmentVariables/PATH に nvm の bin と ~/.local/bin を明示的に追加しないと即 exit する
  • ~/.claude への直書きが permission denied:Claude Code 起動中は保護される。staging + シェルコピーで回避
  • timeout コマンドが macOS 標準にないperl -e 'alarm N; exec @ARGV' で代替
  • watermark を touch し忘れると全ログを毎回再スキャン:終了直前の touch "$WM" は必ず exit 0 の直前に置く
  • exit 142created=0 と同じ見た目で記録される:タイムアウトを異常終了と区別できないため、ログの start-done の時刻差(正常: 数分、タイムアウト: 10 分ちょうど)で判別する癖をつける
  • 同名スキルが既にあると上書きできない[[ -e "$AUTO/$name" ]] でコピーをスキップしているため、既存スキルの更新(patch)はプロンプト側で「重複なら既存を Read して patch せよ」と指示して対応

実ログで見る「空振りが正常」

今週の harvest ログです。

[2026-07-05 03:30:05] harvest start: 3 log(s), digest=   18255B
この会話ログ抜粋を確認しました。INDEX.md はファイル一覧のみで
手順的知識なし。tsuzuke-ios の 2 件は既存の
xcodegen-project-yml-security-review スキルでカバー済み。
該当なし
[2026-07-05 03:35:54] harvest done (exit 0, created=0)

[2026-07-06 03:30:05] harvest start: 3 log(s), digest=   18255B
一回性レビュー+抽出基準に非該当。よってファイルは作成しませんでした。
[2026-07-06 03:30:45] harvest done (exit 0, created=0)

[2026-07-07 03:30:05] harvest start: 3 log(s), digest=   35008B
[2026-07-07 03:32:28] CREATED: note-bokumolily-factual-review
[2026-07-07 03:32:28] harvest done (exit 0, created=1)

5 日中 4 日が created=0 です。空振りは異常ではなく、抽出基準が正しく機能している証拠です。既存スキルとの重複・一度きりの個人事情・雑談はすべて弾かれます。弾く判断を LLM が自律的に行っているため、乱造になりません。

created=0 の harvest はコスト(Max 枠)をほぼ消費しません。claude -p を起動してプロンプトを読んで「該当なし」を返す分だけです。「空振りが安い」ことが夜間バッチを毎晩回せる理由でもあります。

まとめ

  • ウォーターマーク方式.harvest-watermark の mtime を基準に find -newer で差分ログだけを抽出。3349 本あっても毎晩 3 本だけ処理
  • バジェットキャップ二重--max-budget-usd 1.20 + perl alarm 600 で LLM の消費量と実行時間を両方に上限固定
  • ノイズフィルタ先出しgrep -v 'system-reminder' を前処理に入れることでプロンプトを 45KB 固定に圧縮
  • ステージング経由書き込みmktemp -d でシェルが仲介し、~/.claude への直書き競合を回避
  • 空振りが正常:抽出基準を絞ることで created=0 の日が続くのが健全な動作

スキルが 100 個を超えて curate 側の重要性が増してきた話は 前作 に書いています。次回は、この 2 つのバッチが積み上げたスキル・メモリが Claude Code の起動を重くし始めた話 ―― コンテキスト注入量の監査と削減を書く予定です。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?