Metadata-Version: 2.5
Name: atif-make
Version: 0.2.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 |
| `har` | Anthropic Messages, OpenAI Chat Completions, OpenAI Responses |
| `atif` | An ATIF trajectory that is already converted |

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.
