
Agent Trace × Jujutsu でたどる AI コードの来歴
Introduction
生成 AI が日常的にコードを書く時代になり、コードベースには
「人が書いたコード」と「AIが書いたコード」が混在するようになりました。
最近はコードのほとんど/全てをAIエージェントが書いている状態も珍しくなく、
レビュー時にも保守時にも、コードの履歴をどう扱うかは課題となっています。
現状のVCS(git/jujutsu等)はそれに対して明確化する機能はありません。
git blame や jj file annotate の author に出すのは「エージェントを操作した人間」であり、
どの行が AI で、どのモデルが書いて、どの対話の結果なのかを区別する仕組みがありません。
また、エージェント側のセッション履歴はプロダクトごとに仕様があり、横断的に追えません。
レビューで「ここは AI によるコードだから云々」、保守で「このコードの意図をAIとのログから確認したい」
などの要件は、今のところ手作業になります。
この課題を解決する方向性のひとつが 「Agent Trace」 です。
ThoughtWorks Technology Radar でも Assess として取り上げられました。
本記事では以下について解説します。
- Agent Trace 仕様の概要
- Claude Code の hook で trace を書く参考実装
- TUIツール trace を読む参考実装
この記事では、AI AgentはClaude Code、VCSにjujutsu(以下jj)を使います。
※jjについてはこちらなどを参照
About Agent Trace
Agent Trace は Cursor が 2026 年 1 月に公開した RFC v0.1.0 のオープンなデータ仕様で、
README によれば Cognition(Devin)や Google Jules が策定に協力したパートナーとして挙がっています。
製品ではなく純粋なデータ仕様という立場で、
「どの行を / どの AI が / どの会話で書いたか」を記録するため、
ベンダー中立なフォーマットを目指しています。
ちなみに Cursor 自身は、製品内に 「AI Share of Committed Code」というダッシュボードを持っています。
コミットされた行のうち AI が書いた割合を、行ごとの diff signature をもとに集計するもの(Team / Enterprise 向け)です。
ただしこれは Cursor 製品内に閉じた機能で、Agent Trace とは公式には別物です。
Agent Trace はそこから「行レベルで AI 帰属を記録する」という発想だけを切り出し、
特定 IDE に依存しない共通フォーマットにしたもの、と捉えると位置づけが掴みやすいです。
データモデル(TraceRecord)
TraceRecordは、「1 record = 1 回」の編集イベントです。
JSONL などに追記していく形式で、参照実装 は sidecar JSONL(.agent-trace/traces.jsonl)に書き出します。
スキーマは schemas.ts で定義されています。
{
"version": "0.1.0",
"id": "uuid",
"timestamp": "2026-06-05T14:20:00Z",
"vcs": { "type": "jj", "revision": "xqnktzmlworukplnyrropmtzylsuxxlv" },
"tool": { "name": "claude-code", "version": "2.x" },
"files": [{
"path": "src/app/navigation.rs",
"conversations": [{
"url": "https://…/session/abc123", // ← 会話への経路
"contributor": { "type": "ai", "model_id": "anthropic/claude-opus-4-8" },
"ranges": [{ "start_line": 29, "end_line": 52 }]
}]
}],
"metadata": { "com.example.custom": "…" } // ベンダー拡張
}
データモデルは以下のような属性を持ちます。(抜粋)
- contributor*:
human/ai/mixed/unknownの 4 値 + 任意のmodel_id - vcs.type:
git/jj/hg/svn - 保存場所は仕様で未定義(ローカルファイル / git notes / DB など自由)
※参照実装は sidecar JSONL - 行番号は「記録時 revision における位置」
※追跡用にcontent_hashも定義されている
構成
[agent host] [仕様] [VCS] [reader]
Claude Code / Cursor → TraceRecord 生成 →→→ revision に紐付け →→→→→→→→→ blame/annotate 的照会
(hook で編集を捕捉) (JSONL 追記) (git SHA / jj change ID) (未整備)
trace を書くのは AI エージェント側(hook)で、VCS は識別子を提供するのみ。
trace を読む(query/viewer)側は、Git AI のような個別実装はあるものの、標準ツールはまだ整っていません。
jjで実現する方法
jj 自体は agent hook を持たないため、
現状はjj リポジトリ上で動く AI エージェントに trace を書かせて
jj の識別子で紐付ける構成にします。
今回は以下のように実装してみました。
agent host の hook を使う
Claude Code を起動するリポジトリの .claude/settings.json に
PostToolUse 等の hook を登録し、ファイル編集のたびに
「trace を書き出すスクリプト」を起動します。
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "<trace 書き出しスクリプト>" }]
}],
"SessionStart": [ /* 同様 */ ],
"SessionEnd": [ /* 同様 */ ]
}
}
スクリプトがやることは、
「hook の payload から編集情報を取り出し、TraceRecord を生成して
.agent-trace/traces.jsonl に 1 行追記する」
です。Cursor 公式の参照実装 は TypeScriptですが、
言語・実装は問いません。
※本記事ではBash で最小実装したものを使用
なお、ファイル編集後に発火する hook(post-edit hook)を持つエージェントなら、同じ構成を応用できます。
jj VCS adapter
参照実装の getVcsInfo() は git rev-parse HEAD を実行する作りになっており、
jj リポジトリでは revision の紐付けが正しく取れません。
そこで スクリプトの中でjj コマンドを実行してchange ID を取り、
TraceRecord の vcs.revision に埋め込むようにします。
# working copy change の change IDを取得
REV=$(jj log -r @ --no-graph --ignore-working-copy -T 'change_id')
# → 例: "xqnktzmlworukplnyrropmtzylsuxxlv"
TraceRecord には↓のように設定されます。
"vcs": { "type": "jj", "revision": "xqnktzmlworukplnyrropmtzylsuxxlv" }
実装の具体は後述する Bash hook(REV=$(jj -R "$ROOT" log ...) の行)を参照してください。
注意点は以下。
| 項目 | 内容 |
|---|---|
| change ID を使う(commit ID ではない) | jj の commit ID は amend/rebase などで変わるが、change ID は安定している。ただし squash で change 自体が畳まれると、元の change を指す trace は孤立 |
--ignore-working-copy を付ける |
hook から jj を実行すると snapshot が走り、編集途中のファイルが意図せず commit に取り込まれる副作用を避ける |
| git fallback は非推奨 | colocated repo でも git rev-parse HEAD は jj の @ ではなく親 change(@-)の commit を指すことがあり、編集した change とずれる。rewrite でも壊れるので change ID を使うべき |
| working copy の扱い | jj では編集したものが即 @ に反映されるので、編集時点の @ の change ID を記録すれば「この change の作業中に書かれた」とわかる |
保存先は参照実装どおりのパス(<jj root>/.agent-trace/traces.jsonl)に追記していきます。
書き終わった trace を後から「この行は AI が書いたのか?どの会話なのか?」と辿るには、
jj 標準コマンドと汎用ツールで以下のように確認できます。
# 1. 気になる行 → change ID
# jj file annotate が行ごとに「最後にその行を書いた change の ID」を返す
jj file annotate src/app/navigation.rs
# 2. change ID → trace record
# annotate は短縮 ID、record はフル長なので prefix で grep
grep '"revision":"xqnktzml' .agent-trace/traces.jsonl | jq .
# 3. record の URL を開く(このデモ hook は related[].url にセッション URL を出す。
# 仕様では会話そのものを指す conversations[].url も定義されている)
# その行が生成された会話セッションにそのまま飛べる
これで「コード行 → change ID → record → 会話 URL」とたどることができます。
Use jj
jj が有利な点
Agent Trace の注意点として、参照する revision が書き換わると
行範囲や紐付けが古くなってしまうことがありますが、jj を使うことで緩和できます。
jj の change ID は rebase や amend をしても変わらないので、
AI エージェントの「生成 → 人間が手直し → rebase」というフローでも
trace の指す revision が壊れにくく、追跡できます。
(ただし jj squash のように change そのものが別の change へ畳まれるケースでは、
その change を指していた trace は孤立。後述の孤立 trace 検出で拾えます)
また、jj は working copy が常に commit(@)になっているので、
編集イベントがその場で @ の change ID に紐付きます。
git で発生する
「commit するまで編集が revision に紐付かない(hook 時点の HEAD は編集前の親)」
という状態が、jj では発生しません。
さらに、jj evolog や op log で操作履歴を追えるので、
匿名ブランチや rebase/squash が多発しても後から辿りやすくなります。
このため、jj は Agent Trace と相性が良さそうです。
注意点
jj split で change を 2 つに分割した場合、trace は分割前の change ID を指したままで、
どちらに帰属させるかは仕様で決まっていません。
change ID が定まっていても、その後の編集で行位置はずれていくので、
長期的に履歴を追うには仕様の content_hash を生成する writer が必要ですが、
現状の参照実装は値を出力していません。
同一の change ID が複数 commit を持つ状態の場合、「どの variant の行か」が曖昧になります。
ツールもまだ揃っておらず、仕様にも未確定の部分が多く残っている点は
念頭に置いておきたいところです。
jj ワークフローの流れ
trace を書く時は、jj リポジトリで Claude Code に実装させると
hook が .agent-trace/traces.jsonl に record を追記します。
(その時点の @ の change ID 付き)
そのあと jj rebase / describe で歴史を整えても、
change ID が維持される範囲では trace を追跡しやすいです。
jj squash で change を畳んだ場合は、その change の trace が孤立するので、
後述の検出で拾います。
後日、「この実装はなぜこうなってるのか?」と思った時、
jj file annotate で対象行 → change ID を引いて、trace の conversation URL から
当時の AI セッションを開いて意図を確認できます。
レビュー時も、change ごとに「AI 寄与あり/なし」「モデルは何か」が分かるので、
それに応じて人が判断可能です。
Try Agent Trace
Agent Traceの概要について解説したので、
次は jj リポジトリで trace を作成して読んでみましょう。
今回は「動かして肌触りを掴む」レベルの最小実装で確認してみます。
※仕様完全準拠するには参照実装の完全移植が必要
Claude Code の hook で trace を出力する
Claude Code の PostToolUse フックで、Write / Edit が走るたびに
.agent-trace/traces.jsonl に 1 行 JSON を追記します。
jj VCS adapter として jj log -r @ で change ID を取得し、vcs.type: "jj" で書き込みます。
.claude/hooks/agent-trace-jj.shは以下のように作成。
#!/bin/bash
# PostToolUse (Write|Edit) で実行。.agent-trace/traces.jsonl に append する
IN=$(cat)
FILE=$(echo "$IN" | jq -r '.tool_input.file_path // empty')
SESSION=$(echo "$IN" | jq -r '.session_id // empty')
[ -z "$FILE" ] && exit 0
# 編集対象が jj 管理下のリポジトリかを判定
ROOT=$(cd "$(dirname "$FILE")" 2>/dev/null && jj root 2>/dev/null) || exit 0
# 現在の working copy change の change ID (rewrite 後も安定)
# --ignore-working-copy で snapshot を回さない (編集途中の取り込みを防ぐ)
REV=$(jj -R "$ROOT" log -r @ --no-graph --ignore-working-copy \
-T 'change_id' 2>/dev/null) || exit 0
REL=${FILE#"$ROOT"/}
mkdir -p "$ROOT/.agent-trace"
jq -cn \
--arg id "$(uuidgen | tr 'A-Z' 'a-z')" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg rev "$REV" --arg file "$REL" --arg session "$SESSION" '
{
version: "0.1.0",
id: $id,
timestamp: $ts,
vcs: { type: "jj", revision: $rev },
tool: { name: "claude-code" },
files: [{
path: $file,
conversations: [{
contributor: { type: "ai", model_id: "anthropic/claude-opus-4-8" },
related: [{ type: "session", url: ("claude-code://session/" + $session) }]
}]
}]
}' >> "$ROOT/.agent-trace/traces.jsonl"
exit 0
.claude/settings.json へ以下のように登録。
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/agent-trace-jj.sh" }]
}]
}
}
この hook は Write / Edit のツール呼び出しごとに発火するので、
同じファイルを何度か編集するセッションでは同じ change ID で複数 record が並びます。
※MultiEdit も拾いたい場合は matcher を "Write|Edit|MultiEdit" とする
.gitignore に .agent-trace/ を入れ、
trace 追記で working copy が dirty にならないようにします
※jjでも .gitignore をそのまま 適用可能
なお上のスクリプトは model_id を固定値で書いています。PostToolUse の payload に
.model は無いため、実モデルを記録したい場合は payload の
transcript_path(セッションの JSONL)から最後の
assistant ターンの .message.model を取得する必要があります。
動作確認
テスト用の jj リポジトリを用意し、先程の hook を設定して Claude Code に
「src/trace/mod.rs と src/app/navigation.rs を作って」と頼んだ結果が以下。
Write のたびに .agent-trace/traces.jsonl へ record が 1 行ずつ追記されました。
# 時刻 / change ID / ファイル / モデルを確認
$ jq -r '"\(.timestamp) \(.vcs.revision[0:8]) \(.files[0].path) \(.files[0].conversations[0].contributor.model_id)"' \
.agent-trace/traces.jsonl
2026-06-10T02:02:52Z pvowmytz src/trace/mod.rs anthropic/claude-opus-4-8
2026-06-10T02:02:55Z pvowmytz src/app/navigation.rs anthropic/claude-opus-4-8
change ID がどちらも pvowmytz で同じになってます。
jj は working copy が常に @ なので、
同じ作業中に書かれた両ファイルが同じchange に紐付いています。
次に、jj file annotate で行 → change ID を見て、
その change ID で trace records を検索すれば「この行は AI が書いたのか」を確認できます。
# 行 → change ID
$ jj file annotate src/trace/mod.rs
pvowmytz nakamura 2026-06-10 1: //! トレース記録モジュール
pvowmytz nakamura 2026-06-10 2:
pvowmytz nakamura 2026-06-10 3: /// 1件のトレースイベント
...
# change ID → trace record
# annotate 表示は短縮形、record はフル長なので prefix で grep
$ grep '"revision":"pvowmytz' .agent-trace/traces.jsonl | head -1 | jq .
{
"version": "0.1.0",
"id": "b15609b7-9df3-4695-96ca-0ea2062e6f0e",
"timestamp": "2026-06-10T02:02:52Z",
"vcs": { "type": "jj", "revision": "pvowmytzxwmuqqowssmuskwxxpzlwsqk" },
"tool": { "name": "claude-code" },
"files": [{
"path": "src/trace/mod.rs",
"conversations": [{
"contributor": { "type": "ai", "model_id": "anthropic/claude-opus-4-8" },
"related": [{ "type": "session", "url": "claude-code://session/<SESSION_ID>" }]
}]
}]
}
「指定行 → 指定change → AI が claude-opus-4-8 で書いたセッション」を確認できます。
なお、この簡易 hook は ranges(行範囲)を出していないので、record の粒度はファイル単位です。
ranges は仕様上 conversation の必須フィールドなので、この時点の record は
RFC 非準拠の最小デモです。
TUIツールで読む(RFC reader 部分の最小実装)
hook によって trace(.agent-trace/traces.jsonl)は出力されるようになりました。
ですが、書き出せただけで、それを人が確認する手段がありません。
jj file annotate → grep → jq でも確認できますが、量が増えるとすぐに限界が来ます。
そこで、自作の jj 用 TUI ツール(以下tij) に Agent Trace 機能を組み込んでみました。
※RFC の reader 部分のうち、jj ワークフローで使う機能だけを最小実装したものです
tij が提供する Agent Trace 機能は以下です。
| 操作 | 効果 |
|---|---|
| Log View | AI 寄与のある change に [AI] バッジを表示([AI?] は git SHA 経由で change ID に解決できないケース) |
: → show-traces |
選択中の change の trace records を一覧(time / tool / model / files) |
| Enter on record | conversation URL をクリップボードへコピー |
jj config set --repo tij.agent-trace.path <path> |
trace ファイルのパスをカスタマイズ |
これで、以下のような処理が TUI 上で完結します。
- 気になる行を見つける
jj file annotateで change を特定- Log View で
[AI]バッジを確認 :show-tracesから会話 URL をコピー

[AI] バッジと行マーカー(▎)
さきほどのファイル単位 hook(ranges なし)で書いた trace を tij で開くと、
Log View の [AI] バッジは出ますが、Diff View 側の 行マーカー(▎、どの行が AI か)は出ません。
(行マーカーはrange が空だと出ない)
| tij の表示 | 必要なデータ | ranges なし hook |
|---|---|---|
Log View の [AI] バッジ |
change に record があればOK | 出る |
Diff View の ▎ 行マーカー |
ranges[](行範囲)が必須 |
出ない |
行範囲も出してみましょう。
Claude Code の PostToolUse payload には 行情報がそのまま入っているので、これを使います。
# AI が書いた行範囲 (ranges) を算出
# Edit : structuredPatch の追加行(+)の new-file 行番号から hunk ごとに min..max
# Write: structuredPatch が無い/空のときは content 全体を 1..N で代用
# ※ tool_response の形状は tool 依存なので、両者を見て fallback する作りにしている
RANGES=$(printf '%s' "$IN" | jq -c '
def hunk_adds:
. as $h
| [ foreach (($h.lines)//[])[] as $l (
{ ln: (($h.newStart)-1), cur: null };
if ($l|startswith("-")) then { ln: .ln, cur: null }
else { ln: (.ln+1),
cur: (if ($l|startswith("+")) then (.ln+1) else null end) } end;
.cur ) ]
| map(select(.!=null));
if ((.tool_response.structuredPatch)//[] | length) > 0 then
[ .tool_response.structuredPatch[] | hunk_adds | select(length>0)
| {start_line: min, end_line: max} ]
elif (.tool_name=="Write") then
[ { start_line: 1, end_line: ((.tool_input.content)//"" | split("\n") | length) } ]
else [] end
')
あとは record 生成時に conversations[].ranges へ
--argjson ranges "$RANGES" で差し込むだけです。
テストリポジトリで「src/util.rs を作って関数を1つ書く(Write)→ もう1つ追記する(Edit)」
をやらせると、以下のようになりました。
$ jq -r '"\(.files[0].path) range=\(.files[0].conversations[0].ranges)"' \
.agent-trace/traces.jsonl
src/util.rs range=[{"start_line":1,"end_line":5}] # Write: add() を書いた全体
src/util.rs range=[{"start_line":5,"end_line":9}] # Edit : multiply() を追記した分
この状態で tij の Diff View(Single + color-words)を開くと、
src/util.rs の AI 行にマーカーが出ます(gutter の縦線)。
[AI] バッジ(ファイル単位)に加えて、行レベルでどこが AI 記述か見えるようになりました。

ですが、現状、行範囲は目安です。記録後の編集で行はずれていくので、▎ は
「だいたいこの辺が AI」くらいのヒントとして見るのが正解です。
追従させるなら、仕様の content_hash(行内容のハッシュ)を hook で出して行番号ではなく
内容で再アンカーする必要があります(今回の hook も参照実装も未対応)。
現状の tij の reader 機能は以下。
vcs.type: "jj"(change ID 紐付け)とvcs.type: "git"(SHA 紐付け、[AI?]バッジ)の両方を読み込みtool.name/tool.version、contributor.model_id、conversations[].url/related[].urlの表示・コピーranges[]を使った Diff View の行マーカー overlay(上で実際に▎を出したもの)content_hash追跡、署名検証は未対応(upstream の RFC 自体が議論中なので様子見)
end-to-end testing
設計の話だけだと「で、実際に動くの?」が残るので、
テスト用jj リポジトリに Claude Code の hook を仕込み、AI 生成 → 人間修正 → tij
で表示まで確認してみます。
セットアップ
新規 jj リポジトリに、「Try Agent Trace」で作った jj-aware hook を
.claude/hooks/agent-trace-jj.sh として置き、.claude/settings.json の
PostToolUse(Write|Edit) に接続。.gitignore で .agent-trace/ を
除外(除外しないと trace 追記が working copy を dirty にする)。
hook の要点は先述のとおりで、編集のたびに「現在の @ の change ID」を vcs.type:"jj" で記録します。
3 つのシナリオ
次の 4 change を作りました(うち 3 つが検証対象)。
| change | 操作 | trace |
|---|---|---|
initial README |
人間が README 作成 | なし |
feat: add calc (AI) |
AI が calc.py を生成(hook 発火) |
calc.py 1..10 / ai |
docs: usage notes (human) |
人間が README 編集(hook なし) | なし |
feat: greeter (AI) + manual tweak (human) |
AI が greet.py 生成 → 人間が同ファイルに shout() 追記 |
greet.py 1..6 / ai |
最後の change が「同一ファイル・同一コミットで AI と人間が混在」のケースです。
AI が書いたのは 1..6 行、人間が後から足した shout() は 8..11 行目になります。
tij での表示結果
tij を起動して Log View を見ると、期待どおりに出ました。

| change | 表示 | 解釈 |
|---|---|---|
feat: greeter (AI) + manual tweak (human) |
[AI] |
AI 寄与の record が 1 件でもあれば付く(混在でも [AI]) |
docs: usage notes (human) |
バッジなし | trace が無いので何も出ない |
feat: add calc (AI) |
[AI] |
AI record あり |
initial README |
バッジなし | — |
change 数が増えてくると「AI が触った change だけ見たい」が出てくるので、
command palette の filter-ai で Log View を AI 寄与 change のみに絞れます
※もう一度実行で解除
そして混在 change(greet.py)の Diff View では、AI が記録した 1..6 行にだけ
▎ ガターが付き、人間が足した shout()(8..11 行)には付きませんでした。

change 単位のバッジは「AI が混ざっているか」、
行単位のオーバーレイで「どこが AI / どこが人間か」が見えるようになりました。
レビュー時に「AI 行は精読、手書き行は流し読み」を 1 画面で確認できます。
結果
- 末尾追記なら range は正確: 人間が
shout()をファイル末尾に足したので、AI の記録 range 1..6 は正しい AI 行を指す。 - 中間挿入だとドリフト: もし人間が AI 行の間に挿入していたら、記録済み range 1..6 はズレる(writer は再記録しないため)。
content_hash実装するまで残る制約。 - hook はファイル単位の粗い range: デモ hook は「新ファイルの 1..N」を 1 range として記録するだけ。precise な per-edit range は edit の old/new diff が要る(参照実装がやっている)。tij はどちらの粒度の record も読めます。
Summary
Agent Trace は「どの行を・どの AI が・どの会話で書いたか」を記録するためのデータ仕様です。
必要なのは、
agent host の hook・change ID を書く小さな adapter・sidecar JSONL
の3点です。
jjの場合、change ID が rewrite に強いので、AI が書いた版を人間が手直ししても
trace が壊れにくく、change が abandon されたときも孤立として機械的に拾えるので
比較的相性が良さそうです。
※jj split 時の帰属や、編集による行ドリフトは課題ですが
Agent Trace はまだ RFC(v0.1.0)で参照実装も小規模、
jj や git といった VCS 本体は Agent Trace を取り込んでいません。
ですがコーディングエージェントが当たり前になった今、「AI が書いた行をどう識別・追跡するか」は
いずれ誰かが答えるべき問いで、その共通フォーマットとして Agent Trace が固まる可能性はあります。
仕様が落ち着き、Cursor 以外の主要エージェントや VCS 側が hook / reader 仕様に対応すれば、
外側のラッパーで個別に書いている処理は、標準ツールに吸収されていくかもしれません。
Agent Traceの今後の流れに注目しておきましょう。
Appendix
hook 全文
コピペで使える最終版。(jj adapter + 実モデル取得 + ranges 出力)
.claude/hooks/agent-trace-jj.sh として置き、
本文のとおり .claude/settings.json の PostToolUse(Write|Edit) に接続してください。
jq / uuidgen / jj(>= 0.42.0)が PATH にあれば動きます。
#!/bin/bash
# PostToolUse (Write|Edit) で実行。.agent-trace/traces.jsonl に append する
IN=$(cat)
FILE=$(echo "$IN" | jq -r '.tool_input.file_path // empty')
SESSION=$(echo "$IN" | jq -r '.session_id // empty')
[ -z "$FILE" ] && exit 0
# 編集対象が jj 管理下のリポジトリかを判定
ROOT=$(cd "$(dirname "$FILE")" 2>/dev/null && jj root 2>/dev/null) || exit 0
# 現在の working copy change の change ID (rewrite 後も安定)
# --ignore-working-copy で snapshot を回さない (編集途中の取り込みを防ぐ)
REV=$(jj -R "$ROOT" log -r @ --no-graph --ignore-working-copy \
-T 'change_id' 2>/dev/null) || exit 0
# model_id: transcript の最後の assistant ターンから実モデルを拾う
TRANSCRIPT=$(printf '%s' "$IN" | jq -r '.transcript_path // empty')
RAW=$(jq -rs 'map(select(.type=="assistant" and .message.model
and .message.model != "<synthetic>")) | last.message.model // empty' \
"$TRANSCRIPT" 2>/dev/null)
BASE=$(printf '%s' "${RAW:-claude-opus-4-8}" | sed -E 's/-[0-9]{8}$//')
case "$BASE" in claude-*) MODEL="anthropic/$BASE";; *) MODEL="$BASE";; esac
# AI が書いた行範囲 (ranges) を算出
# Edit : structuredPatch の追加行(+)の new-file 行番号から hunk ごとに min..max
# Write: structuredPatch が無い/空のときは content 全体を 1..N で代用 (新規=全行 AI)
# ※ tool_response の形状は tool 依存。両者を見て fallback する
RANGES=$(printf '%s' "$IN" | jq -c '
def hunk_adds:
. as $h
| [ foreach (($h.lines)//[])[] as $l (
{ ln: (($h.newStart)-1), cur: null };
if ($l|startswith("-")) then { ln: .ln, cur: null }
else { ln: (.ln+1), cur: (if ($l|startswith("+")) then (.ln+1) else null end) } end;
.cur ) ]
| map(select(.!=null));
if ((.tool_response.structuredPatch)//[] | length) > 0 then
[ .tool_response.structuredPatch[] | hunk_adds | select(length>0) | {start_line: min, end_line: max} ]
elif (.tool_name=="Write") then
[ { start_line: 1, end_line: ((.tool_input.content)//"" | split("\n") | length) } ]
else [] end
')
# realpath で正規化してから相対パスを得る (macOS の /tmp→/private/tmp 対策)
ABS=$(cd "$(dirname "$FILE")" && pwd -P)/$(basename "$FILE")
REL=${ABS#"$ROOT"/}
mkdir -p "$ROOT/.agent-trace"
jq -cn \
--arg id "$(uuidgen | tr 'A-Z' 'a-z')" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg rev "$REV" --arg file "$REL" --arg session "$SESSION" --arg model "$MODEL" \
--argjson ranges "$RANGES" '
{
version: "0.1.0",
id: $id,
timestamp: $ts,
vcs: { type: "jj", revision: $rev },
tool: { name: "claude-code" },
files: [{
path: $file,
conversations: [{
contributor: { type: "ai", model_id: $model },
ranges: $ranges,
related: [{ type: "session", url: ("claude-code://session/" + $session) }]
}]
}]
}' >> "$ROOT/.agent-trace/traces.jsonl"
exit 0
なお id / timestamp に uuidgen / date を使うため、
厳密な再現生成(同じ入力→同じ出力)はしません。
冪等性が必要なら payload の tool_use_id などを使う手法があります(未検証)。







