Metadata-Version: 2.5
Name: atif-make
Version: 0.3.0
Summary: Convert agent logs (Claude Code, Codex, Copilot, HAR) into ATIF v1.7 trajectories.
License: MIT
Requires-Python: >=3.12
Provides-Extra: spec
Requires-Dist: harbor>=0.21.0; extra == 'spec'
Description-Content-Type: text/markdown

# atif-make

Make [ATIF v1.7](https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md)
trajectories from agent logs — Claude Code, Codex, Copilot CLI, and HAR captures.

Zero runtime dependencies. Python 3.12+.

## Install

```sh
uv tool install atif-make          # puts `atif-make` on your PATH
```

Or as a library in a project:

```sh
uv add atif-make
```

For a browser view of what you convert, see the companion
[`atif-view`](https://github.com/jammastergirish/atif-view), which depends on this package.

`uv tool install` builds an isolated environment, so nothing lands in your
project or system Python. To follow local edits instead, use
`uv tool install --editable .`; to remove it, `uv tool uninstall atif-make`.

Running from a checkout without installing works too: `uv run atif-make ...`.

```sh
atif-make ~/.claude/projects/my-project/session.jsonl   # convert one log
atif-make index --stats                                 # what do I have, across agents?
```

## Commands

```
atif-make <file>                 convert (shorthand for `atif-make convert`)
atif-make convert <file>         convert one log
atif-make convert <dir|archive>  convert every log inside
atif-make index [roots...]       scan for sessions across agents
atif-make formats                list supported input formats
```

| Flag | Command | Meaning |
|---|---|---|
| `-o, --output` | convert | output path (default `<input>.trajectory.json`) |
| `-f, --format` | convert | force the input format instead of detecting it |
| `--json` | convert | write one self-contained document to stdout |
| `--bundle OUT.zip` | convert | zip the trajectory with its images and subagents |
| `--split-subagents` | convert | write subagents as sibling files, not embedded |
| `--indent N` | convert | JSON indent (default 2) |
| `-q, --quiet` | convert | suppress progress output |
| `--stats` | index | print a per-agent summary |
| `--add` | index | merge into the existing index instead of replacing it |

## Supported inputs

Most agents write *two* unrelated log shapes — what the CLI streams, and what it
persists on disk — and they are not interchangeable. atif-make reads both.

| Format | Source |
|---|---|
| `claude-code-transcript` | `~/.claude/projects/<project>/<session>.jsonl` |
| `claude-code-stream` | `claude -p --output-format stream-json` |
| `codex-rollout` | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` |
| `codex-exec` | `codex exec --json` |
| `copilot-cli` | Copilot CLI session logs |
| `sleight-bench` | SLEIGHT-Bench transcripts — one content block per line |
| `har` | Anthropic Messages, OpenAI Chat Completions, OpenAI Responses |
| `atif` | An ATIF trajectory that is already converted |

`sleight-bench` reads the
[SLEIGHT-Bench](https://huggingface.co/datasets/sleightbench/SLEIGHT-Bench)
benchmark, where each line holds a single Anthropic content block rather than a
whole message, so one assistant turn spans several lines. Its transcripts open
with a canary object asking that the data be kept out of training corpora; that
line is skipped as a message and carried into the trajectory's `extra`, so the
opt-out travels with the converted file instead of being dropped at the door.

The parser was built against all 86 transcripts in the dataset, not the
published spec alone, which is how the three places they disagree came to light:
a tool result can be a list of content blocks rather than a string, `cwd` is
documented as required but is sometimes absent, and a transcript can end on a
tool call that never got a result. All 86 convert with no loss and pass the
reference validator.

Directories, `.zip` and `.tar.gz` (also `.tgz`, `.tar.bz2`, `.tar.xz`) are read
as containers: every log inside is found and converted. That closes the loop on
`--bundle` — the zip atif-make hands you to send someone opens again in atif-make,
images and all.

Archives are extracted to a temporary directory, once per run. Members naming
absolute paths or climbing out with `..` are refused rather than quietly
sanitised, and an archive that expands past 8 GB or 20,000 entries is rejected
outright.

Format is detected from content, never from the extension. `atif` exists so a
trajectory someone sends you opens like anything else — it is loaded, not
reparsed, and unknown fields from a newer ATIF minor version are dropped rather
than rejected.

## What it gets right

These are the things that are easy to get wrong, and that silently corrupt a
trajectory when you do:

**Split messages.** Claude Code writes one API response as several JSONL lines
that share a `message.id` — thinking, text, and each parallel `tool_use` arrive
separately, with the *same* `usage` object repeated on every line. Treating those
as separate turns inflates step counts and multiplies token totals. atif-make
coalesces them and counts usage once.

**Out-of-order tool results.** Parallel calls come back interleaved, and a slow
call can return several turns after it was issued. Pairing results to calls by
*position* drops some and misattributes others. atif-make pairs by `tool_use_id`.

**Byte-capped detection.** A JSONL preamble (hook events, rate-limit notices) can
push the identifying line kilobytes into a file. atif-make scans whole lines.

**Subagent structure.** Claude Code links a delegated agent through a `.meta.json`
sidecar (`toolUseId`) and an `agentId` field on the result line — not through
anything in the result *text*. atif-make links by call id, so refs actually resolve
instead of leaving orphaned subagents.

**Images.** Codex embeds screenshots as base64 data URLs and Claude Code as
base64 content blocks — 24 of the sessions on one test machine carried them, and
a single Codex session held 65 images totalling 14 MB. Dropping them loses the
thing the agent was actually looking at, and inlining them makes an unreadable
document. atif-make writes them to `images/` and references them by relative path,
which is what the spec asks for.

**Malformed timestamps.** ATIF requires ISO 8601. A truncated or hand-edited log
can carry something else, and passing it through would make the whole trajectory
fail validation, so an unparseable timestamp is dropped rather than emitted.

**HAR tool results.** In a HAR capture a tool's output is not in the response that
called it — it appears in the *next* request's message history. atif-make harvests
results across entries and pairs them back by id, while emitting the shared
conversation prefix only once.

## Output — a trajectory is a directory, not a file

ATIF references images and split subagents by path *relative to the trajectory
file*, so anything with attachments is inherently multi-file:

```
session.trajectory.json                 the document
session.trajectory.<agent-id>.json      subagents, with --split-subagents
images/<sha>.png                        images, referenced as "images/<sha>.png"
```

Images are de-duplicated by content hash, so the same screenshot pasted five
times is stored once.

Three ways out, depending on where it's going:

```sh
atif-make session.jsonl -o out/t.json      # directory form: t.json + images/
atif-make session.jsonl --json             # one self-contained doc; images inlined as data: URIs
atif-make session.jsonl --bundle send.zip  # zip of the whole directory — for sending someone
```

`--json` is the exception that proves the rule: stdout has no directory to put
siblings in, so images become data URIs to keep the document standalone.

Output carries `timestamp`, `reasoning_content`, per-step `metrics`, multimodal
`ContentPart` message content, and subagents either embedded
(`subagent_trajectories`) or split into sibling files with resolvable
`trajectory_path` refs.

## Viewing

Conversion is all this package does. To browse trajectories in a browser,
install the companion [`atif-view`](https://github.com/jammastergirish/atif-view), which depends on this
package and reads the index below.

## Identity

Every session gets a **content key** — a hash of the file's first line. Logs are
append-only, so that line never changes while a live session grows; hashing a
fixed byte span re-keys a small file on every write. Documents that are not
line-oriented open with a bare brace and fall back to a bounded head read.

The obvious alternatives do not work. A path changes when a file moves, and a
browser upload has none. `session_id` is run-scoped, not document-scoped — Codex
continuations inherit their parent's, so two distinct rollouts share one — and
some logs carry none at all. Their first lines still differ.

The key is what lets `atif-view` attach a name or a folder to a transcript and
have it survive the file moving, or a full re-index.

## Where sessions come from

`atif-make index` scans your own machine — nothing is uploaded, and no network call
is ever made. By default it looks in:

```
~/.claude/projects/     Claude Code sessions
~/.codex/sessions/      Codex sessions
```

The result is cached at `~/.atif-make/index.json`. Indexing reads only each file's
leading lines, so a 143 MB rollout costs the same as a 4 KB one; full conversion
happens lazily, when you actually open a session.

Scan somewhere else, or add a single file someone sent you:

```sh
atif-make index ~/work/logs                    # replace the index with this scan
atif-make index --add received.trajectory.json # merge one file into the existing index
atif-make index --add received-bundle.zip      # or a whole bundle
```

`--add` merges; without it, `index` replaces — except for files opened by hand,
which live outside the scan roots and are carried across so a rescan cannot
silently drop them. Both raw logs and already-converted ATIF trajectories can be
added.

With a library present, `--stats` also groups by collection, so the command line
and the viewer describe the same corpus.

## Tests

```sh
uv run pytest                      # unit tests, all synthetic fixtures
uv sync --extra spec               # pulls harbor (large)
uv run --extra spec pytest         # + validate against the reference ATIF models
```

The `spec` suite validates every fixture against harbor's own pydantic models —
ground truth for whether the output is really ATIF, rather than what atif-make
believes ATIF to be.
