Skip to content

Instantly share code, notes, and snippets.

@hitode909
Created November 20, 2025 03:32
Show Gist options
  • Select an option

  • Save hitode909/cb4ebec36add3aae048967ccdbdf64e4 to your computer and use it in GitHub Desktop.

Select an option

Save hitode909/cb4ebec36add3aae048967ccdbdf64e4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# frozen_string_literal: true
# Claude Codeとの会話ログからコーディングルールを抽出してグローバルCLAUDE.mdに追記するスクリプト
#
# ## 目的
# Claude Codeとの過去の会話ログを分析し、コーディング指摘やベストプラクティスを
# 自動的に抽出してルール化することで、学習効果を継続・蓄積する
#
# ## アプローチ
# 1. 現在のディレクトリから対応するClaude Codeプロジェクトの会話ログを特定
# 2. 会話ログからユーザーメッセージのみを抽出(ツール実行やシステムメッセージを除外)
# 3. 抽出したメッセージを Claude Code にワンショットで送信してルール抽出を依頼
# 4. JSON形式でレスポンスを受け取り、構造化されたルールとしてCLAUDE.mdに追記
# 5. 重複処理を避けるため、処理済みファイルをマークして管理
#
# ## 実行方法
# - 通常実行: collect-claude-rules.rb
# - テストモード(最新1件のみ): TEST_MODE=1 collect-claude-rules.rb
#
# ## 出力先
# ~/.claude/CLAUDE.md にルールが追記される
require 'json'
require 'pathname'
require 'shellwords'
require 'securerandom'
require 'logger'
# 設定
# 現在のワーキングディレクトリからプロジェクト名を動的に取得
def get_claude_project_name
cwd = Pathname.new(Dir.pwd)
# Claude Codeのプロジェクト名形式: パスの'/', '.', '_'を'-'に変換
project_name = cwd.to_s.gsub('/', '-').gsub('.', '-').gsub('_', '-')
project_name
end
CONVERSATION_DIR = Pathname.new(ENV['HOME']) / '.claude' / 'projects' / get_claude_project_name
GLOBAL_CLAUDE_MD = Pathname.new(ENV['HOME']) / '.claude' / 'CLAUDE.md'
CLAUDE_CODE_BIN = 'claude'
# スクリプト専用のセッションID(実行ごとに生成)
SCRIPT_SESSION_ID = SecureRandom.uuid
# loggerをstderrに設定
logger = Logger.new($stderr)
logger.level = Logger::INFO
logger.formatter = proc do |severity, datetime, progname, msg|
"[#{severity}] #{msg}\n"
end
# JSON構造のみでツール/サブエージェント実行をスキップするかどうか判定
def should_skip_content?(data)
# ツール実行・結果
return true if data['type'] == 'tool_use'
return true if data['type'] == 'tool_result'
# assistantメッセージで tool_use が含まれる場合
if data['type'] == 'assistant' && data['content'].is_a?(Array)
return true if data['content'].any? { |c| c['type'] == 'tool_use' }
end
# userメッセージのみを処理対象とする
message = data['message']
return true unless message && message['role'] == 'user'
# isMeta フラグがある場合はツール説明の可能性が高い
return true if data['isMeta'] == true
false
end
# 会話ログファイルを取得
conversation_files = Dir.glob(CONVERSATION_DIR / '*.jsonl')
.sort_by { |f| File.mtime(f) }
.reverse
# TEST_MODEなら最新1件に絞る
if ENV['TEST_MODE']
conversation_files = conversation_files.take(1)
end
puts "Found #{conversation_files.size} conversation logs"
# 各会話ログからルールを抽出
conversation_files.each_with_index do |file, index|
puts "\n[#{index + 1}/#{conversation_files.size}] Processing: #{File.basename(file)}"
# すでに処理済みかチェック
file_id = File.basename(file, '.jsonl')
if File.exist?(GLOBAL_CLAUDE_MD)
claude_md_content = File.read(GLOBAL_CLAUDE_MD)
if claude_md_content.include?("<!-- Extracted from conversation #{file_id} -->")
logger.info "Already processed: #{file_id}, skipping"
puts " → Already processed, skipping"
next
end
end
# 会話ログを読み込んでユーザーメッセージを抽出
begin
user_messages = []
File.foreach(file) do |line|
data = JSON.parse(line)
# JSONの構造でツール/サブエージェント関連をスキップ
next if should_skip_content?(data)
if data['message'] && data['message']['role'] == 'user'
content = data['message']['content']
# contentは文字列の場合と配列の場合がある
if content.is_a?(String)
# システムメッセージ(<で始まる)を除外
user_messages << content unless content.start_with?('<')
elsif content.is_a?(Array)
text_parts = content.select { |c| c['type'] == 'text' }.map { |c| c['text'] }
# システムメッセージを除外
filtered_parts = text_parts.reject { |t| t.start_with?('<') }
user_messages << filtered_parts.join("\n") unless filtered_parts.empty?
end
end
end
# 意味のあるメッセージがない場合はスキップ
meaningful_messages = user_messages.reject { |m|
m.strip.empty? ||
m.include?('[Request interrupted') ||
m.strip.length < 20 || # 短すぎるメッセージを除外
m =~ /^[a-zA-Z0-9\s\-_\.]*$/ || # 英数字のみのメッセージを除外
m.strip.match?(/^(ok|OK|thanks|ありがとう|Warmup|はい|yes|no)$/i) # 単語レベルの応答を除外
}
logger.info "Processing #{File.basename(file)}: #{meaningful_messages.size} messages"
meaningful_messages.each_with_index do |msg, i|
logger.info "[#{i+1}] #{msg}"
end
next if meaningful_messages.empty?
# ワンショットでClaude Codeにルール抽出を依頼(JSON形式で構造化)
prompt = <<~PROMPT
IMPORTANT: あなたは自動化スクリプトから呼ばれています。ユーザーへの質問や確認は一切不要です。
以下のユーザーメッセージ群から、コーディングルールとして抽出できそうな普遍的な指摘を見つけてください。
特に注目する指摘の種類:
- コーディングスタイルに関する指摘(変数名、制御構文、演算子の使い方など)
- 使うべき関数や定数の指摘
- テストの書き方に関する指摘
- 周辺コードとの一貫性に関する指摘
**必ず以下のJSON形式で出力してください:**
```json
{
"rules": [
{
"name": "ルール名",
"description": "説明文"
}
]
}
```
ルールが見つからない場合は以下を出力してください:
```json
{ "rules": [] }
```
前置きや説明は不要です。JSONのみ出力してください。
ユーザーメッセージ:
#{meaningful_messages.join("\n---\n")}
PROMPT
# Claude Codeをワンショットで実行
# -p: 非インタラクティブモード
# --session-id: 実行ごとに新しいセッションIDを使用(既存セッションとの衝突を避ける)
# 標準入力経由でプロンプトを渡す
session_id = SecureRandom.uuid
cmd = "#{CLAUDE_CODE_BIN} -p --session-id #{session_id} 2>&1"
logger.info "Executing: #{cmd}"
result = IO.popen(cmd, 'r+') do |io|
io.write(prompt)
io.close_write
io.read
end
logger.info "Claude response: #{result}"
# JSONレスポンスをパース
begin
# JSONブロックを抽出(```json〜```の部分)
json_match = result.match(/```json\s*\n(.*?)\n```/m)
json_str = json_match ? json_match[1] : result.strip
response_data = JSON.parse(json_str)
rules = response_data['rules'] || []
if rules.empty?
puts " → No rules found"
else
puts " → Found #{rules.size} rules, appending to #{GLOBAL_CLAUDE_MD}"
# マークダウン形式に変換
markdown_rules = rules.map { |rule| "- **#{rule['name']}**: #{rule['description']}" }.join("\n")
puts markdown_rules
# グローバルCLAUDE.mdに追記
File.open(GLOBAL_CLAUDE_MD, 'a') do |md|
md.puts "\n<!-- Extracted from conversation #{File.basename(file, '.jsonl')} -->"
md.puts markdown_rules
end
end
rescue JSON::ParserError => e
logger.error "Failed to parse JSON response: #{e.message}"
logger.error "Raw response: #{result}"
puts " → Failed to parse JSON response, skipping"
end
# このセッションのログファイルをすぐに削除(セッションの再利用を防ぐ)
session_log_file = CONVERSATION_DIR / "#{session_id}.jsonl"
if File.exist?(session_log_file)
File.delete(session_log_file)
puts " → Cleaned up session log: #{session_id}"
end
rescue JSON::ParserError => e
puts " → Skipped (JSON parse error: #{e.message})"
rescue => e
puts " → Error: #{e.message}"
end
# レート制限を考慮して少し待つ
sleep 2
end
puts "\n✅ Completed!"
puts "Updated: #{GLOBAL_CLAUDE_MD}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment