
Tracing the Lineage of AI Code with Agent Trace × Jujutsu
This page has been translated by machine translation. View original
Introduction
With generative AI now writing code on a daily basis, codebases have come to contain a mix of
"human-written code" and "AI-written code."
It's no longer uncommon for most or all code to be written by AI agents,
and how to handle code history during both review and maintenance has become a challenge.
Current VCS tools (git/jujutsu, etc.) have no features that make this explicit.
The author shown by git blame or jj file annotate is the "human who operated the agent,"
and there is no mechanism to distinguish which lines were AI-written, which model wrote them, or which conversation produced them.
Additionally, agent-side session history varies by product specification and cannot be tracked across tools.
Requirements like "this code was written by AI, so..." during review, or "I want to check the intent of this code from the AI conversation log" during maintenance,
are currently handled manually.
One approach to solving this problem is "Agent Trace".
It has also been featured as Assess on the ThoughtWorks Technology Radar.
This article covers the following:
- Overview of the Agent Trace specification
- A reference implementation for writing traces using Claude Code hooks
- A reference implementation for reading traces with a TUI tool
In this article, we use Claude Code as the AI Agent and jujutsu (hereafter jj) as the VCS.
※For more about jj, see here and similar resources
About Agent Trace
Agent Trace is an open data specification published as RFC v0.1.0 by Cursor in January 2026,
and according to the README, Cognition (Devin) and Google Jules are listed as partners who assisted in drafting it.
Its stance is that of a pure data specification rather than a product,
aiming for a vendor-neutral format to record
"which lines / which AI / which conversation wrote what."
Incidentally, Cursor itself has a dashboard called "AI Share of Committed Code" within its product.
It aggregates, based on per-line diff signatures, the proportion of committed lines written by AI (intended for Team / Enterprise use).
However, this is a feature confined within the Cursor product and is officially separate from Agent Trace.
Agent Trace can be understood as taking just the idea of "recording AI attribution at the line level" from that,
and turning it into a common format that doesn't depend on a specific IDE.
Data Model (TraceRecord)
A TraceRecord represents "1 record = 1 edit event."
Records are appended in formats such as JSONL, and the reference implementation writes them to a sidecar JSONL file (.agent-trace/traces.jsonl).
The schema is defined in 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", // ← link to the conversation
"contributor": { "type": "ai", "model_id": "anthropic/claude-opus-4-8" },
"ranges": [{ "start_line": 29, "end_line": 52 }]
}]
}],
"metadata": { "com.example.custom": "…" } // vendor extension
}
The data model has the following attributes (excerpt):
- contributor*: 4 values —
human/ai/mixed/unknown+ optionalmodel_id - vcs.type:
git/jj/hg/svn - Storage location is undefined in the spec (local file / git notes / DB, etc. — your choice)
※The reference implementation uses a sidecar JSONL - Line numbers represent positions in the revision at the time of recording
※content_hashis also defined for tracking purposes
Architecture
[agent host] [spec] [VCS] [reader]
Claude Code / Cursor → TraceRecord gen →→→ bind to revision →→→→→→→ blame/annotate-style query
(capture edits (JSONL append) (git SHA / jj change ID) (not yet established)
via hook)
Writing the trace is the responsibility of the AI agent side (hook); the VCS only provides identifiers.
On the reading side (query/viewer), there are individual implementations like Git AI, but no standard tooling yet.
How to achieve this with jj
Since jj itself has no agent hook,
the current approach is to have an AI agent running on a jj repository write the traces
and associate them with jj identifiers.
Here is how we implemented it this time.
Using the agent host's hook
Register a PostToolUse or similar hook in .claude/settings.json in the repository where Claude Code is launched,
so that a "trace-writing script" is invoked every time a file is edited.
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "<trace writing script>" }]
}],
"SessionStart": [ /* same */ ],
"SessionEnd": [ /* same */ ]
}
}
What the script does is:
"extract edit information from the hook's payload, generate a TraceRecord, and
append 1 line to .agent-trace/traces.jsonl."
The official Cursor reference implementation is in TypeScript, but
any language or implementation is acceptable.
※This article uses a minimal implementation in Bash
Note that any agent with a hook that fires after file edits (a post-edit hook) can apply the same configuration.
jj VCS adapter
The reference implementation's getVcsInfo() runs git rev-parse HEAD,
which doesn't correctly retrieve the revision binding for a jj repository.
So we run a jj command inside the script to get the change ID
and embed it in the TraceRecord's vcs.revision.
# Get the change ID of the working copy change
REV=$(jj log -r @ --no-graph --ignore-working-copy -T 'change_id')
# → e.g.: "xqnktzmlworukplnyrropmtzylsuxxlv"
The TraceRecord is configured as follows:
"vcs": { "type": "jj", "revision": "xqnktzmlworukplnyrropmtzylsuxxlv" }
For implementation specifics, refer to the Bash hook described later (the REV=$(jj -R "$ROOT" log ...) line).
Key notes:
| Item | Details |
|---|---|
| Use change ID (not commit ID) | The jj commit ID changes with amend/rebase etc., but the change ID is stable. However, if a change is folded into another via squash, traces pointing to the original change become orphaned |
Add --ignore-working-copy |
Prevents the side effect of a snapshot being triggered when jj is called from the hook, which could unintentionally capture files mid-edit into a commit |
| git fallback is not recommended | Even in a colocated repo, git rev-parse HEAD may point to the parent change (@-) rather than jj's @, causing a mismatch with the edited change. It can also break on rewrites, so the change ID should be used |
| Handling the working copy | In jj, edits are immediately reflected in @, so recording the change ID of @ at the time of editing tells you "this was written during work on this change" |
We append to the same path as the reference implementation (<jj root>/.agent-trace/traces.jsonl).
To later trace "was this line written by AI? which conversation?" from the stored traces,
you can use standard jj commands and general-purpose tools as follows:
# 1. Interesting line → change ID
# jj file annotate returns "the change ID that last wrote each line"
jj file annotate src/app/navigation.rs
# 2. change ID → trace record
# annotate shows a short ID; records use the full length, so grep by prefix
grep '"revision":"xqnktzml' .agent-trace/traces.jsonl | jq .
# 3. Open the URL from the record (this demo hook outputs a session URL in related[].url.
# The spec also defines conversations[].url pointing to the conversation itself)
# You can jump directly to the conversation session where that line was generated
This lets you trace the path: "code line → change ID → record → conversation URL."
Use jj
Advantages of jj
One caveat of Agent Trace is that if the referenced revision changes,
line ranges and bindings can become stale, but using jj helps mitigate this.
Since jj change IDs don't change across rebases or amends,
even through an "AI generates → human edits → rebase" workflow,
the revision referenced by the trace is less likely to break, keeping things traceable.
(However, when a change is folded into another via jj squash,
traces pointing to that change become orphaned — these can be caught with the orphan detection described later.)
Also, since jj's working copy is always a commit (@),
edit events are immediately associated with the change ID of @.
The situation that arises in git —
"edits don't bind to a revision until committed (so HEAD at hook time points to the pre-edit parent)" —
does not occur in jj.
Furthermore, operation history can be traced via jj evolog and op log,
making it easier to look back even when anonymous branches and frequent rebase/squash operations occur.
For these reasons, jj seems to pair well with Agent Trace.
Caveats
If a change is split into two with jj split, traces still point to the pre-split change ID,
and the spec doesn't define which one to attribute them to.
Even with a stable change ID, line positions will drift as further edits occur,
so long-term history tracking requires a writer that generates the spec's content_hash,
which the current reference implementation does not output.
When a single change ID has multiple commits, "which variant's lines" becomes ambiguous.
It's also worth keeping in mind that tooling is still incomplete and the spec itself has many unresolved areas.
jj Workflow
When writing traces, having Claude Code implement things in a jj repository
causes the hook to append records to .agent-trace/traces.jsonl
(with the change ID of @ at that moment).
After that, even if you clean up history with jj rebase / describe,
traces remain traceable as long as the change ID is preserved.
If a change is folded with jj squash, traces for that change become orphaned —
these can be caught with the detection described later.
Later, when you wonder "why is this implemented this way?",
you can use jj file annotate to find the line → change ID, then open
the conversation URL from the trace to review the AI session and confirm the original intent.
During code review, knowing "does this change have AI contribution or not" and "which model" for each change
allows humans to make informed decisions accordingly.
Try Agent Trace
Now that we've covered the overview of Agent Trace,
let's create and read a trace in a jj repository.
This time we'll verify things with a minimal implementation at the level of "run it and get a feel for it."
※Full spec compliance requires a complete port of the reference implementation
Outputting traces using Claude Code hooks
Using Claude Code's PostToolUse hook, we append 1 line of JSON to
.agent-trace/traces.jsonl every time Write / Edit runs.
As a jj VCS adapter, we get the change ID with jj log -r @ and write it with vcs.type: "jj".
Create .claude/hooks/agent-trace-jj.sh as follows:
#!/bin/bash
# Runs on PostToolUse (Write|Edit). Appends to .agent-trace/traces.jsonl
IN=$(cat)
FILE=$(echo "$IN" | jq -r '.tool_input.file_path // empty')
SESSION=$(echo "$IN" | jq -r '.session_id // empty')
[ -z "$FILE" ] && exit 0
# Determine whether the edited file is in a jj-managed repository
ROOT=$(cd "$(dirname "$FILE")" 2>/dev/null && jj root 2>/dev/null) || exit 0
# Get the change ID of the current working copy change (stable even after rewrites)
# --ignore-working-copy to avoid running a snapshot (prevents mid-edit files from being captured)
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
Register it in .claude/settings.json as follows:
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/agent-trace-jj.sh" }]
}]
}
}
This hook fires on every Write / Edit tool call, so
multiple records with the same change ID will accumulate when a session edits the same file multiple times.
※To also capture MultiEdit, change the matcher to "Write|Edit|MultiEdit"
Add .agent-trace/ to .gitignore so that
trace appends don't dirty the working copy.
※.gitignore applies as-is in jj as well
Note that the script above hardcodes the model_id. Since
.model is not in the PostToolUse payload,
if you want to record the actual model, you need to retrieve the
.message.model from the last assistant turn in
transcript_path (the session JSONL) from the payload.
Verification
We set up a test jj repository, configured the hook above, and asked Claude Code to
"create src/trace/mod.rs and src/app/navigation.rs." The result is below.
A record was appended to .agent-trace/traces.jsonl for each Write.
# Check timestamp / change ID / file / model
$ 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
Both change IDs are the same: pvowmytz.
Since jj's working copy is always @,
both files written during the same session are bound to the same change.
Next, we can use jj file annotate to get the line → change ID,
then search trace records by that change ID to verify "was this line written by AI?"
# Line → change ID
$ jj file annotate src/trace/mod.rs
pvowmytz nakamura 2026-06-10 1: //! Trace recording module
pvowmytz nakamura 2026-06-10 2:
pvowmytz nakamura 2026-06-10 3: /// A single trace event
...
# change ID → trace record
# annotate shows a short form; records use the full length, so grep by prefix
$ 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>" }]
}]
}]
}
We can confirm "specified line → specified change → session written by AI with claude-opus-4-8."
Note that this simple hook does not output ranges (line ranges), so records have file-level granularity.
Since ranges is a required field for conversations in the spec, these records are
a minimal demo that is not fully RFC-compliant.
Reading with a TUI tool (minimal implementation of the RFC reader portion)
The hook now outputs traces (.agent-trace/traces.jsonl).
However, we've only written them out — there's no means for humans to inspect them.
You can verify things with jj file annotate → grep → jq, but this quickly hits its limits as volume grows.
So I've integrated Agent Trace functionality into my own jj TUI tool (hereafter tij).
※This is a minimal implementation of just the features from the RFC's reader portion that are useful in a jj workflow
The Agent Trace features provided by tij are as follows:
| Action | Effect |
|---|---|
| Log View | Displays an [AI] badge on changes with AI contribution ([AI?] for cases where the change ID cannot be resolved via git SHA) |
: → show-traces |
Lists trace records for the selected change (time / tool / model / files) |
| Enter on record | Copies the conversation URL to the clipboard |
jj config set --repo tij.agent-trace.path <path> |
Customize the trace file path |
This makes the following workflow complete entirely within the TUI:
- Find a line of interest
- Identify the change with
jj file annotate - Confirm the
[AI]badge in Log View - Copy the conversation URL from
:show-traces

[AI] badge and line markers (▎)
When you open a trace written with the file-level hook above (no ranges) in tij,
the [AI] badge appears in Log View, but the line marker (▎, indicating which lines are AI-written) does not appear in Diff View.
(Line markers don't appear when ranges are empty)
| tij display | Required data | Hook without ranges |
|---|---|---|
[AI] badge in Log View |
Just needs a record on the change | Appears |
▎ line marker in Diff View |
ranges[] (line ranges) required |
Does not appear |
Let's also output line ranges.
The Claude Code PostToolUse payload contains line information directly, so we'll use that.
# Calculate the line range (ranges) written by AI
# Edit : min..max per hunk from the new-file line numbers of added (+) lines in structuredPatch
# Write: if structuredPatch is absent/empty, substitute 1..N covering the entire content
# ※ Since the shape of tool_response depends on the tool, we look at both and fall back accordingly
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
')
Then it's just a matter of injecting it into conversations[].ranges at record generation time
using --argjson ranges "$RANGES".
When we had it "create src/util.rs and write one function (Write) → append another (Edit)"
in the test repository, the result was as follows:
$ 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: full file with add()
src/util.rs range=[{"start_line":5,"end_line":9}] # Edit : appended multiply()
When you open the Diff View (Single + color-words) in tij with this data,
a marker appears on the AI lines in src/util.rs (a vertical line in the gutter).
In addition to the [AI] badge (file-level), you can now see at the line level which parts are AI-written.

However, line ranges are currently approximate. Since lines drift with edits after recording, treat ▎ as
a hint meaning "roughly this area is AI" rather than a precise indicator.
To keep them accurate, you would need the hook to output the spec's content_hash (a hash of line content) and re-anchor by content rather than line number (not yet supported by this hook or the reference implementation).
The current reader features of tij are as follows:
- Reads both
vcs.type: "jj"(change ID binding) andvcs.type: "git"(SHA binding,[AI?]badge) - Displays and copies
tool.name/tool.version,contributor.model_id,conversations[].url/related[].url - Line marker overlay in Diff View using
ranges[](the▎we actually displayed above) content_hashtracking and signature verification are not supported (monitoring the upstream RFC discussion)
end-to-end testing
Discussing the design alone leaves "but does it actually work?" unanswered,
so let's set up Claude Code hooks in a test jj repository, and verify the full flow
from AI generation → human modification → display in tij.
Setup
In a new jj repository, place the jj-aware hook from "Try Agent Trace" as
.claude/hooks/agent-trace-jj.sh and wire it to
PostToolUse(Write|Edit) in .claude/settings.json. Add .agent-trace/ to .gitignore
(without this, trace appends will dirty the working copy).
The key points of the hook are as described earlier: on every edit, record "the change ID of the current @" with vcs.type:"jj".
3 Scenarios
We created the following 4 changes (3 of which are the verification targets):
| change | operation | trace |
|---|---|---|
initial README |
human creates README | none |
feat: add calc (AI) |
AI generates calc.py (hook fires) |
calc.py 1..10 / ai |
docs: usage notes (human) |
human edits README (no hook) | none |
feat: greeter (AI) + manual tweak (human) |
AI generates greet.py → human appends shout() to same file |
greet.py 1..6 / ai |
The last change is the case where "AI and human are mixed in the same file, same commit."
The AI wrote lines 1..6, and the human's shout() added afterward is on lines 8..11.
Display results in tij
Launching tij and looking at Log View, it appeared as expected.

| change | display | interpretation |
|---|---|---|
feat: greeter (AI) + manual tweak (human) |
[AI] |
Appears if there is even 1 AI contribution record (shown as [AI] even for mixed changes) |
docs: usage notes (human) |
no badge | Nothing appears since there is no trace |
feat: add calc (AI) |
[AI] |
AI record present |
initial README |
no badge | — |
As the number of changes grows, you'll want to "see only changes that AI touched,"
so you can use filter-ai from the command palette to narrow Log View to only AI-contributed changes.
※Run again to clear the filter
And in the Diff View for the mixed change (greet.py), the ▎ gutter appeared only on lines 1..6 recorded by the AI,
while the human-added shout() (lines 8..11) had no marker.

Change-level badges show "whether AI was involved,"
and line-level overlays show "which parts are AI / which parts are human."
During review, you can check "read AI lines carefully, skim human-written lines" all on one screen.
Results
- Range is accurate for appended content: The human added
shout()at the end of the file, so the recorded range 1..6 for AI correctly points to the AI lines. - Drift occurs with mid-file insertions: If the human had inserted content between AI lines, the recorded range 1..6 would be off (the writer doesn't re-record). This limitation persists until
content_hashis implemented. - The hook records coarse file-level ranges: The demo hook records just "1..N for a new file" as a single range. Precise per-edit ranges require the old/new diff of the edit (which the reference implementation does). tij can read records at either level of granularity.
Summary
Agent Trace is a data specification for recording "which lines / which AI / which conversation wrote what."
All you need is:
an agent host hook, a small adapter that writes the change ID, and a sidecar JSONL.
With jj, since change IDs are resilient to rewrites, even when a human edits an AI-generated version
traces are less likely to break, and when a change is abandoned it can be mechanically caught as an orphan —
making it a relatively good fit.
※Issues like attribution on jj split and line drift from edits remain
Agent Trace is still an RFC (v0.1.0) with a small reference implementation,
and VCS tools like jj and git have not incorporated Agent Trace natively.
But now that coding agents are commonplace, "how to identify and track AI-written lines" is
a question someone will eventually need to answer, and there's a possibility that Agent Trace will solidify as the common format for it.
Once the spec stabilizes and major agents beyond Cursor and VCS tools start supporting hook/reader specs,
the processing we're currently wrapping individually on the outside may get absorbed into standard tooling.
Let's keep an eye on how Agent Trace develops going forward.
Appendix
Complete Hook
Ready-to-paste final version. (jj adapter + real model retrieval + ranges output)
Place it as .claude/hooks/agent-trace-jj.sh,
and connect it to PostToolUse(Write|Edit) in .claude/settings.json as described in the main text.
It works if jq / uuidgen / jj (>= 0.42.0) are in PATH.
#!/bin/bash
# Runs on PostToolUse (Write|Edit). Appends to .agent-trace/traces.jsonl
IN=$(cat)
FILE=$(echo "$IN" | jq -r '.tool_input.file_path // empty')
SESSION=$(echo "$IN" | jq -r '.session_id // empty')
[ -z "$FILE" ] && exit 0
# Determine if the edit target is in a jj-managed repository
ROOT=$(cd "$(dirname "$FILE")" 2>/dev/null && jj root 2>/dev/null) || exit 0
# Change ID of the current working copy change (stable even after rewrite)
# --ignore-working-copy prevents snapshot capture (avoids picking up mid-edit state)
REV=$(jj -R "$ROOT" log -r @ --no-graph --ignore-working-copy \
-T 'change_id' 2>/dev/null) || exit 0
# model_id: retrieve the actual model from the last assistant turn in the transcript
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
# Calculate the line ranges (ranges) written by the AI
# Edit : from the added lines (+) in structuredPatch, compute min..max per hunk by new-file line numbers
# Write: when structuredPatch is absent/empty, substitute 1..N for the entire content (new file = all lines are AI)
# * The shape of tool_response varies by tool. Check both and fall back accordingly
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
')
# Normalize with realpath then get relative path (workaround for 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
Note that uuidgen / date are used for id / timestamp,
so strict reproducible generation (same input → same output) is not supported.
If idempotency is required, there is an approach using tool_use_id from the payload (untested).
References
- Agent Trace (GitHub, official repository)
- Agent Trace schema definition (schemas.ts)
- Agent Trace reference implementation (reference/)
- ThoughtWorks Technology Radar — Agent Trace
- Cursor Analytics (AI Share of Committed Code)
- Jujutsu (jj) (GitHub)
- Claude Code hooks documentation
- tij (jj TUI used in this article)
- Introduction to Jujutsu (jj) (DevelopersIO)
