## Context

Claude Code persists every session as JSONL files under `~/.claude/projects/<project-slug>/<session-id>.jsonl`. Each line is a JSON object with a `type` discriminator: `user`, `assistant`, `system`, `progress`, `file-history-snapshot`, or `queue-operation`. Messages form a linked list via `parentUuid`/`uuid`.

A critical detail: **a single API response is split across multiple JSONL lines**, each containing one content block (thinking, text, or tool_use), all sharing the same `message.id` and `requestId`. These must be reassembled into a single ATIF step.

Subagent sessions use the same schema, stored in `<session-dir>/subagents/agent-*.jsonl` with `isSidechain: true`.

Harbor's ATIF v1.6 is a flat array of steps with `source` (user/agent/system), `tool_calls`, `observation`, and `metrics`. It supports `subagent_trajectory_ref` for nested agent work.

## Goals / Non-Goals

**Goals:**
- Export any CC session into a valid ATIF v1.6 JSON file viewable with `harbor view`
- Preserve the full richness: thinking/reasoning, tool calls + results, token usage, costs, subagent work, context compaction events
- Support batch export (all sessions, by project, by date range)
- Simple CLI: `cc-logger export ./trajectories` just works
- Handle the streaming split correctly (reassemble content blocks per API response)

**Non-Goals:**
- Real-time streaming / watching active sessions
- Modifying or deleting CC data (strictly read-only)
- Supporting CC versions older than 2.1.x
- Computing costs ourselves — we only pass through `cost_usd` from the SQLite `__store.db` if available, or leave metrics at token counts
- A web UI — we rely on Harbor's existing viewer

## Decisions

### 1. Language: Python with uv

**Choice**: Python CLI managed with `uv`, using `click` for CLI parsing and `pydantic` for typed data models.

**Why**: Python's ecosystem is strong for data transformation. `uv` provides fast dependency management and makes the project easy to install (`uv run cc-logger`). Pydantic gives us runtime validation for both CC input and ATIF output structures.

**Alternatives considered**:
- TypeScript/Node.js: CC users have Node.js but Python is more natural for data processing and the Harbor ecosystem is Python-based.
- Go: Fast single binary but slower iteration for JSON transformation logic.

### 2. Reassembling Streamed Assistant Responses

**Choice**: Group consecutive assistant JSONL lines by `message.id` (the Anthropic API message ID) into a single ATIF agent step.

**How it works**:
1. Read JSONL lines sequentially
2. When we see an assistant line, check `message.id`
3. Accumulate all content blocks (thinking, text, tool_use) from lines sharing that `message.id`
4. Emit one ATIF step with:
   - `message`: concatenated text blocks (plain string if one text block, ContentPart array if multiple)
   - `reasoning_content`: content from `thinking` blocks
   - `tool_calls`: all `tool_use` blocks
   - `metrics`: usage from the **last** line in the group (most complete token counts)
5. `step_id` increments per reassembled group, not per JSONL line

**Why**: ATIF expects one step per model turn. CC's streaming split is an implementation detail that shouldn't leak into the trajectory.

### 3. Mapping CC Message Types to ATIF Steps

| CC Type | CC Subtype | ATIF Source | Notes |
|---------|-----------|-------------|-------|
| `user` (external, string content) | — | `user` | Human messages |
| `user` (tool_result content) | — | *Folded into previous agent step as `observation`* | Not a separate step |
| `assistant` | — | `agent` | Reassembled from multiple lines |
| `system` | `compact_boundary` | `system` | Context compaction event |
| `system` | `microcompact_boundary` | `system` | Selective compaction event |
| `system` | `turn_duration` | *Skipped* | Internal timing, not meaningful in trajectory |
| `system` | `api_error` | `system` | API errors with retry info |
| `system` | `local_command` | `system` | Slash command execution |
| `progress` | all subtypes | *Skipped* | Transient UI state, not part of conversation |
| `file-history-snapshot` | — | *Skipped* | Undo/redo bookkeeping |
| `queue-operation` | — | *Skipped* | Internal queue management |
| `user` (`isCompactSummary: true`) | — | `system` | Context compaction summary, not real user input |

**Why skip progress/file-history/queue**: These are CC internal state, not conversation turns. Including them would bloat trajectories without adding value for visualization. Compaction events are kept because they represent meaningful conversation structure changes.

### 4. Tool Results as Observations

**Choice**: Tool result `user` messages are NOT emitted as separate ATIF steps. Instead, they become the `observation` on the preceding agent step.

**How**:
1. After emitting an agent step with `tool_calls`, look ahead for the corresponding `user` messages with `tool_result` content
2. Match via `tool_use_id` in the tool_result to `id` in the tool_call
3. Attach as `observation.results[]` with `source_call_id` linking them

**Why**: ATIF models tool results as observations on the agent step, not as separate user turns. This matches how Harbor's viewer renders tool interactions.

### 5. Subagent Trajectory Handling

**Choice**: Export each subagent as a separate ATIF file. Reference it from the parent via `subagent_trajectory_ref`.

**How**:
1. When we encounter a `Task` tool_call in the main session, note the tool_call_id
2. Scan `<session-dir>/subagents/` for the corresponding agent file (match via agentId from the tool result)
3. Export the subagent JSONL as its own ATIF file: `<output-dir>/<session-id>/subagents/<agent-id>.json`
4. In the parent's observation for that tool call, include `subagent_trajectory_ref` pointing to the subagent file

**Why**: Subagent sessions can be large. Inlining them would make parent trajectories unwieldy. Separate files with references lets Harbor lazy-load them.

### 6. Session Discovery and Listing

**Choice**: Read `sessions-index.json` for fast listing, fall back to scanning JSONL files.

**How**:
1. `cc-logger list` reads all `sessions-index.json` files under `~/.claude/projects/`
2. Displays: session ID, project path, date, first prompt, message count
3. Supports `--project <path>` to filter to a specific project
4. `cc-logger export` can accept `--session <id>`, `--project <path>`, or `--since <date>` filters
5. Default (no filters): exports all sessions

**Why**: The index files are small and already maintained by CC. Parsing 993+ JSONL files just for listing would be slow.

### 7. Conversation Tree → Linear Step Sequence

**Choice**: Follow the `parentUuid` chain to produce a linear sequence, ignoring sidechain branches in the main trajectory.

**How**:
1. Build a map of `uuid → message` from all JSONL lines
2. Find the root (parentUuid = null)
3. Walk the chain: for each message, find the child whose `parentUuid` matches and `isSidechain` is false
4. This produces the main conversation thread as a linear sequence
5. Subagent (sidechain) messages are exported separately per Decision 5

**Why**: ATIF steps are a flat array. CC's tree structure with sidechains needs to be linearized. The main thread (isSidechain=false) is the primary conversation.

### 8. Output Structure

```
<output-dir>/
  <session-id>.json                    # Main trajectory ATIF file
  <session-id>/
    subagents/
      <agent-id>.json                  # Subagent trajectory ATIF files
```

For batch exports with `--project`:
```
<output-dir>/
  <project-slug>/
    <session-id>.json
    <session-id>/
      subagents/
        <agent-id>.json
```

### 9. Agent Configuration in ATIF

```json
{
  "name": "claude-code",
  "version": "2.1.49",
  "model_name": "claude-opus-4-6"
}
```

- `version`: taken from the first message's `version` field
- `model_name`: taken from the first assistant message's `message.model` field (skip `<synthetic>` models)

### 10. Metrics Mapping

| CC Field | ATIF Field |
|----------|-----------|
| `usage.input_tokens` | `prompt_tokens` |
| `usage.output_tokens` | `completion_tokens` |
| `usage.cache_read_input_tokens` | `cached_tokens` |
| `cost_usd` from `__store.db` | `cost_usd` (if available) |

`final_metrics` sums all per-step metrics across the trajectory.

## Risks / Trade-offs

**[Streaming reassembly edge cases]** → Some assistant responses may have unusual ordering or missing `message.id` on synthetic messages. Mitigation: fall back to treating each line as its own step if `message.id` is missing or `model` is `<synthetic>`.

**[Large sessions]** → Some sessions are 12K+ lines. Mitigation: stream JSONL line-by-line instead of loading entire files into memory. Build the UUID map incrementally.

**[CC format changes]** → CC's JSONL schema is undocumented and may change between versions. Mitigation: be lenient in parsing — skip unknown types, use optional fields, log warnings for unexpected structures. The `version` field on messages lets us detect format changes.

**[Cost data availability]** → `__store.db` only covers a subset of sessions (16 of 993). Most sessions won't have `cost_usd`. Mitigation: omit `cost_usd` when unavailable; token counts are always present in the JSONL.

**[Sidechain complexity]** → Some sessions may have complex branching where the "main thread" isn't obvious. Mitigation: use the simplest heuristic (follow isSidechain=false chain) and document that branched conversations export the primary thread only.
