# Refactor survey: providers, runner, agents and the process entry points

The plan is split so that parallel workers never edit the same file. Batch 0 is a short mechanical move that must land first; it makes the provider batch and the driver batch file-disjoint. Batches 1–5 can then run in parallel worktrees. Wave 2 holds the items that need files from two wave-1 batches, so they start after wave 1 merges.

## Files covered

- **providers/:** `__init__.py`, `base.py`, `payload.py`, `claude.py`, `claude_rows.py`, `codex.py`, `codex_rows.py`, `drivers.py`, `turns.py`, `skill_homes.py`.
- **runner/:** `hooks.py`, `engine.py`, `engines.py`, `worker.py`, `speed.py`. The `__init__.py` files are empty.
- **agents/:** `actors.py`, `seat.py`, `control.py`, `terminal.py`, `band.py`.
- **Entry points:** `install.py`, `serve.py`, `supervisor.py`, `worker.py`, `__main__.py`, `journal.py`, `channel.py`, `engine/worker.py`, `engine/keeper.py`.
- **Skimmed for overlap:** `features/session_recording/recorder.py`, `features/sharing/server.py`.
- **Clean, nothing to do:** `__main__.py`, `journal.py`, `worker.py`, `engine/keeper.py`.
- **Not dead:** `engine/worker.py` is the address that supervisors started by older builds keep in their launch spec (commit 258728f9). `install.py:31` STUBS keeps it working. Leave it.

---

## Batch 0 (do first, mechanical): move the terminal drivers out of the provider files

**Files:** `providers/claude.py`, `providers/codex.py`, new `providers/claude_driver.py` and `providers/codex_driver.py`, `providers/__init__.py:1-2`, `features/worktrees/test.py` (lines 82, 126 and 222, including the mock string `"providers.claude.ClaudeDriver.placed"`).

- **Move the two driver classes.** `claude.py:580-718` (`ClaudeDriver`, `claude_state`, the `CONFIRM_*` and `ASKS_BEFORE` constants) and `codex.py:504-592` (`CodexDriver`) are the terminal-typing layer. They sit in the same file as the transcript and hook parsing layer, so every provider change and every driver change collide.
  - Move each class to its own module and import `Claude`, `Row`, `Handed`, `HandedLine` and `CHANNEL_MARK` from `claude.py`.
  - In the same move, put `SERVER = "journal"` (`claude.py:586`) in the top constant block. It is read at line 210, before it is defined.
- **Risk:** low. **Behaviour change:** none.

---

## Batch 1 (most valuable): one funnel for reading provider transcripts, plus provider-class duplication

**Files:** `providers/base.py`, `payload.py`, `claude.py`, `codex.py`, `claude_rows.py`, `codex_rows.py`, `turns.py`, plus new `providers/jsonl.py` and `providers/transcript_cache.py`. Keep the public method names (`transcript`, `tail`, `recent`, `crew`) so callers in other batches are not touched. Their renames are W4.

1. **Seven hand-written JSONL readers.** The same "seek, read, keep complete lines, parse each one" loop is written seven times:
   - `base.py:243-268` (`recent`), `:270-283` (`entries`), `:288-310` (`transcript`), `:312-321` (`tail`), `:436-457` (`folded`)
   - `claude.py:544-574` (`thoughts`)
   - `codex.py:477-485` (`session`)
   - Plus `claude.py:536-542`, `codex.py:324-328`, `:342-344` and `:406-410`, which re-parse `engine.stored.tail` output by hand.

   **Refactor:** create `providers/jsonl.py` with `complete_lines(path, offset) -> (lines, next_offset)`, `last_lines(path, span)` and `rows(lines, row_of)`. The module-level mutable caches (`base.py:17-22`, `:45-48`: `RECENT`, `FOLDS`, `KEPT`, `TRANSCRIPTS`) and the pickle persistence (`base.py:25-44`) move into one owner object, `TranscriptCache`, in `providers/transcript_cache.py`. The `Provider` methods become thin calls into it.
   - **Risk:** medium. This is the hot path for hooks, engine ticks and the viewer. Compare `journal speed` before and after.
   - **Behaviour change:** none.
2. **Codex crew skips the cache.** `codex.py:421-472` `Codex.crew` re-reads the whole transcript through `entries()` on every call. It is the only crew or fold that bypasses `folded()`.
   - **Refactor:** rewrite it as a fold, `crew_rows(state: CodexCrew, row)`, matching `claude.py:458-477`.
   - **Risk:** medium, because the `compacting` flag is carried across rows. **Behaviour change:** none, and it is faster.
3. **The same skill list built several ways.**
   - `claude.py:446` `loaded()` and `codex.py:424` are the same expression, `sorted({u.skill … if u.name == "Skill"} - {""})`.
   - `claude.py:449-456` `skills`/`skill_names` repeats the fold in `base.py:406-434`.
   - `payload.py:125-127` `ToolCall.skill_loaded` and `:199-206` `ToolUse.loaded_skill` are two names for the same fact.

   **Refactor:** one `Provider.skills_in(uses)` and one property name, `loaded_skill`, on both tool models.
   - **Risk:** low. **Behaviour change:** none.
4. **Crew rows built as hand-made dicts.**
   - Subagent, shell and monitor rows are built by hand at `claude.py:502-521`.
   - Codex builds the subagent row twice (`codex.py:416-418` and `:457-458`), plus shell rows at `:462` and `:467`.
   - `agents/seat.py:24-32` reads them back with aliases that cover the shape differences.

   **Refactor:** add frozen `SubagentRow`, `ShellRow` and `MonitorRow` dataclasses in `providers/base.py`, each with `to_json()` that emits exactly today's keys. Add one Codex `subagent_row()` helper.
   - **Risk:** low to medium. The viewer keys must stay byte-identical. **Behaviour change:** none.
5. **Two copies of the "control choice" lookup.** `base.py:157-168` `control_choice` is duplicated in `codex.py:172-184`, which also calls `catalog()` twice and imports `Refused` inline at line 180.
   - **Refactor:** the base class does the lookup and raises the refusal. It calls one override point, `commands_for(selected, action, value, model)`, which only Codex implements.
   - **Risk:** low. **Behaviour change:** none.
6. **The same write-if-changed loop twice.** `claude.py:251-261` and `codex.py:265-277` (`agent_types`) have identical loops.
   - **Refactor:** the loop lives in `Provider.agent_types`. Each provider supplies only `agent_folder(project)` and `agent_text(kind, model)`.
   - **Risk:** low. **Behaviour change:** none.
7. **The hook command is built as a string and parsed back three times.**
   - `install.py:208` builds the command string. `base.py:503` re-splits it, as do `claude.py:209-210` (`words[3]`) and `claude.py:222` (`split()[1]`).
   - Two separate "is this hook block ours" tests exist: `base.py:55-56` `journal_hook` and `:506`. `json.dumps(b)` runs four times per block.

   **Refactor:** add a `HookCommand` value object (script, provider, root) with `.text`, `.parse(text)` and `.owns(block)`. `wire` parses once. Keep `journal_hook` as the single predicate, because `features/clean_slate/slate.py` imports it.
   - **Risk:** low to medium. **Behaviour change:** none.
8. **Hook event names are bare strings.** `payload.py:10-15`, `base.py:183-184`, `:366` (`"idle"`), `:372`, `:379`, `:388` and `:390` repeat names like `"PreToolUse"`.
   - **Refactor:** add a `HookEvent` StrEnum in `payload.py` and use it here. The runner side is W1.
   - **Risk:** low. **Behaviour change:** none.
9. **Provider folder names repeated as literals.** `".claude"` appears at `claude.py:185, 252, 264, 267, 270, 273` and `".codex"` at `codex.py:209, 215, 266, 280, 283, 286`.
   - **Refactor:** add one class fact, `home = ".claude"` / `".codex"`. W8 needs it as well.
   - **Risk:** none. **Behaviour change:** none.
10. **The shell tool names are listed three times** (`codex.py:20`, `:159`, `:438`).
    - **Refactor:** one `SHELL_TOOLS` tuple in `codex.py`; `TOOLS` and `tool_kinds` are derived from it.
    - **Risk:** low. **Behaviour change:** none.
11. **One question-tool list, written twice.** `claude.py:23` `ASKS` duplicates `Claude.question_tools` at line 139.
    - **Refactor:** use `self.question_tools`.
    - **Risk:** none. **Behaviour change:** none.
12. **Dead code to delete.**
    - `base.py:341-342` `Provider.tools` is never called.
    - `payload.py:455` `Hook.agent_type` is never read. Grep for it once more before deleting.
    - **Risk:** none. **Behaviour change:** none.
13. **One name for two things.** `base.py:62` module function `recent(rows)` has the same name as the method `Provider.recent(path)` at line 243.
    - **Refactor:** rename the module function to `running_and_latest`.
    - **Risk:** none. **Behaviour change:** none.
14. **Dishonest return type.** `codex.py:139` `initial_effort` returns the int `0` where a string is expected.
    - **Refactor:** return `""`. Both values fall outside `standard`, so nothing changes.
    - **Risk:** none. **Behaviour change:** none.
15. **Duplicate lookup in `providers/turns.py`.** Lines 19-40 repeat the provider lookup and settle step in both `turns` and `last_turn`.
    - **Refactor:** one private helper, `_settled(agent)`. Dropping the unused `record` parameter touches callers, so that is W4.
    - **Risk:** low. **Behaviour change:** none.

---

## Batch 2: driving the terminal (drivers, worker, supervisor protocol, terminal.py internals)

**Files:** `providers/drivers.py`, `providers/claude_driver.py`, `providers/codex_driver.py`, `runner/worker.py`, `agents/terminal.py`, `supervisor.py`.

1. **ANSI stripping written five times.**
   - `b"".join(ANSI.sub(b"", x).split())` appears at `drivers.py:331`, `claude.py:636`, `:654`, `codex.py:530` and `:535`.
   - Codex's `_printed_tail` (`codex.py:559-564`) re-implements `Driver.printed_tail` (`drivers.py:418-422`). Its `SCREEN_TAIL = 8192` (`codex.py:521`) shadows the module constant `SCREEN_TAIL = 16384` (`drivers.py:19`).

   **Refactor:** add `squeezed(raw)` and `plain(raw)` in `drivers.py`. Codex calls `self.printed_tail(self.PROMPT_TAIL)`.
   - **Risk:** low. **Behaviour change:** none.
2. **ClaudeDriver rebuilds Driver's screen helpers.** `claude.py:640-657` (`run_command`, `_confirm_after`) re-creates `Driver._screen_file`, `_shown_size` and `_shown_since` (`drivers.py:320-333`).
   - **Refactor:** call the Driver methods.
   - **Risk:** low. **Behaviour change:** none.
3. **Small ClaudeDriver duplicates.**
   - `claude.py:659-660`: `_post` only forwards to `_handed`. Rename `_handed` to `_post`.
   - The pid lookup is written three times (`claude.py:663`, `:668`, `drivers.py:407-410`). Add `Driver.pid()`.
   - The handed file is read twice (`claude.py:676-677`, `:704-705`). Add `_held()`.
   - **Risk:** low. **Behaviour change:** none.
4. **Redundant imports.** `drivers.py:400-401` and `:407` import inside functions what is already imported at the top (`Agents` and `SYSTEM` at lines 8 and 11). `claude.py` already imports `Sessions` at the top, so there is no import cycle.
   - **Refactor:** delete the inline imports.
   - **Risk:** none. **Behaviour change:** none.
5. **Instance method called as if unbound.** `Driver.command` (`drivers.py:81-82`) is an abstract instance method, but `agents/terminal.py:85` calls it as `driver.command(driver, …)`, passing the class as `self`.
   - **Refactor:** make it a `classmethod` and fix the call.
   - **Risk:** low. **Behaviour change:** none.
6. **runner/worker.py duplicates the driver.**
   - `ENTER_AFTER` (line 33) repeats `drivers.py:16`.
   - `press()` (lines 71-77) repeats `Driver.press` / `_entered`.
   - `Confirm` (lines 80-109) builds a second driver for the session, re-reads the printed tail by hand (lines 93-95) and looks up `DRIVERS[self.agent]` four times while it already holds `self.driver`.

   **Refactor:** `Confirm(driver)` uses `driver.printed_tail`, `consent`, `opening` and `CONFIRM_AFTER`. Add a new `Driver.press_raw(keys)` that replaces `worker.press`. Share the driver that `checks()` creates, and build a separate one only if `checks()` failed.
   - **Risk:** medium. **Behaviour change:** none.
7. **The exit-code protocol is defined twice.** `supervisor.py:20` and `agents/terminal.py:16-19` both define 75–78.
   - **Refactor:** `agents/terminal.py` imports `RELOAD`, `STOP`, `RELAUNCH` and `HEAL` from `supervisor`. The supervisor stays stdlib-only and import-free, as the 2.118.0 changelog requires; the dependency only points toward it.
   - **Risk:** low. **Behaviour change:** none.
8. **Session file names spelled in several places.** `"printed"`, `"screen"`, `"typed"`, `"screen.json"` and `"launched.json"` appear in `supervisor.py:121-124` and `:172`, `drivers.py:78-79` and `:321`, and `agents/terminal.py:24` and `:198-199`.
   - **Refactor:** export them as constants from `supervisor.py` and import them in the driver and terminal modules. `engine/typist` and `engine/runtime` are outside this survey and stay as they are; note the socket-path duplicate at `supervisor.py:144-145` versus `engine/typist.py:12-16`.
   - **Risk:** low. **Behaviour change:** none.
9. **The same wait loop twice in supervisor.py.** Lines 42-48 and 197-203 are the same `waitpid` polling loop.
   - **Refactor:** one `waited(pid, seconds, drain)` function.
   - **Risk:** low. **Behaviour change:** none.
10. **Three different classes named `Seat`.** `agents/terminal.py:94`, `agents/seat.py:45` and `engine/seats.py:25` all define one.
    - **Refactor:** rename the one in `terminal.py` to `TerminalSession`. Its only importer is `runner/worker.py:15`, which is in this batch.
    - **Risk:** none. **Behaviour change:** none.
11. **More redundant imports.** `agents/terminal.py:104` and `:116` import `Sessions` inline, but line 7 already imports it.
    - **Refactor:** delete them.
    - **Risk:** none. **Behaviour change:** none.

**Leave as is:** `supervisor.py:29` `ESCAPES` and `drivers.py:25` `ANSI` are two different escape regexes. Merging them would change what the driver matches on screen, which is a behaviour change.

---

## Batch 3: the hook path and the engine (runner and agents)

**Files:** `runner/hooks.py`, `runner/engine.py`, `runner/engines.py`, `agents/seat.py`, `agents/actors.py`, `agents/control.py`. Edits to importers outside these files: `commands/http.py:39`, `serve.py:24`, and the `from runner.hooks import displayed` lines in the messages, update_reports, triggers and command_tags tests.

1. **The hook handler also runs the chat mirror.** `runner/hooks.py:64-218` keeps the "displayed.json" deduplication ledger (`replay`, `displayed`, `display_chunk`, `stopped`, `next_turn`, `unfinished`, `send_to_chat`) inside the hook handler.
   - The ledger path is built five times (lines 108, 142, 159, 171, 207).
   - The locked read-modify-write is written six times.
   - `shown()` at line 132 uses a word banned by rule 45.

   **Refactor:** move this into `runner/chat_mirror.py`. A `DisplayedLedger(root, session)` class owns the path and the lock. Rename `shown` to `joined_parts`.
   - **Risk:** medium. The deduplication rules must be kept exactly. **Behaviour change:** none.
2. **Small fixes in `hooks.py`.**
   - Lines 234 and 257 import `Hook` inline, though line 17 already imports from `providers.payload`. Move it to the top.
   - In `answer` (lines 240-249), `agent_pid(pid)` runs four times and `owned_environments(root)` twice; each call loads every environment. Compute each once.
   - **Risk:** low. **Behaviour change:** none.
3. **Dead statement.** `runner/engine.py:119` is a stray f-string expression that does nothing. Delete it.
   - **Risk:** none. **Behaviour change:** none.
4. **`Engine` inherits a mixin whose state it initialises.** `runner/engine.py:66` makes `Engine` inherit the `agents/seat.py` `Seat` mixin, and lines 74-79 initialise the mixin's fields.
   - **Refactor:** composition. `agents/seat.py` gets a `SeatReport(record, agent)` class that owns its own fields. `Engine` holds one and calls `.write(why)`.
   - **Risk:** low. **Behaviour change:** none.
5. **The same provider lookup six times.** `runner/engine.py:132-134`, `185-191`, `281`, `289`, `317` and `370` each look up `PROVIDERS.get(row.provider)` and check `row.transcript`. The "has the transcript grown" check is also repeated: `peer_size` (lines 81, 137), `failure_size` (82, 192) and `crewed_size` in seat.
   - **Refactor:** `Engine.reading() -> (provider, Path) | None`, plus a small `Grown(path)` helper.
   - **Risk:** low. **Behaviour change:** none.
6. **Two copies of the defaults.** `agents/control.py:18-20` `configured` returns a class nobody uses and repeats the default text of `base.py:151`. Lines 48-52 repeat the refusal message of `base.control_choice`.
   - **Refactor:** `cls = PROVIDERS.get(provider, Provider)`, then call the classmethods directly. The abstract class works for this because they are classmethods.
   - **Risk:** low. **Behaviour change:** the refusal for an unknown provider name would say "this agent" rather than the name. Keep the name if that wording matters.
7. **Small fixes in `control.py`.**
   - `permit` (lines 106-110) is `pressed` (lines 65-68) plus a value. Add `value=""` to `pressed` and use it.
   - The local `relaunch` shadows the imported `relaunch as restart` (lines 10 and 113). Rename one of them.
   - **Risk:** low. **Behaviour change:** none.
8. **Rule-45 name.** `agents/actors.py:25` `spoken_data` uses a word from the same family rule 45 bans. Rename it to `event_data` and update its callers in `runner/engine.py`.
   - **Risk:** none. **Behaviour change:** none.
9. **Literals and magic numbers.**
   - `agents/seat.py:59` uses `"compacting"` where `AgentRow.compacting` exists.
   - The 10-second interval is written twice (lines 181, 201).
   - `runner/engines.py:57` and `:140` build `root/"runtime"` by hand instead of using `runtime.folder(root)`.
   - `runner/engines.py:128` uses `"agent"` where the `AGENT` constant exists.
   - **Risk:** none. **Behaviour change:** none.
10. **The same loop twice in `engines.py`.** Lines 56-65 and 159-165 are the same "tick, report any exception, wait" loop. Replace them with one `keep_ticking(...)` helper.
    - **Risk:** low. **Behaviour change:** none.

---

## Batch 4: entry points and packaging

**Files:** `install.py`, `serve.py`, `runner/speed.py` moved to `commands/speed.py`, and the import line at `commands/queries.py:121`.

1. **Bug in the "no agent found" check.** `install.py:211-212` tests `if not done`, but `done` already holds the old git-hook line, so the check can be skipped. The message also hard-codes "Claude nor Codex".
   - **Refactor:** test `if not present:` and build the names from `PROVIDERS`.
   - **Risk:** low. **Behaviour change:** yes, this is a bug fix.
2. **Hard-coded briefing file names.** `install.py:217` writes "AGENTS.md and CLAUDE.md". Build it from each provider's `briefing_file`.
   - **Risk:** none. **Behaviour change:** none.
3. **Repeated reads and literals in `install.py`.**
   - The VERSION file is read four times (lines 278, 339, 359, 406). Use one `release_version(folder, default)`.
   - `"AGENT_JOURNAL_BOOTSTRAPPED"` is spelled three times (lines 319, 354, 367) and `"AGENT_JOURNAL_REPO"` twice. Make them constants.
   - `PACKED_DIRS` (line 34) re-lists `PACKAGE_DIRS` (line 19). Derive one from the other.
   - Lines 297-299 rebuild the upgrade marker that `engine.runtime.UPGRADE_MARK` already defines. Use it.
   - **Risk:** low. **Behaviour change:** none.
4. **Hidden names.** `install.py:470-510` pushes 13 names into the module with `globals().update(package())`, so readers and linters cannot see them.
   - **Refactor:** keep one frozen `Package` instance and reference its attributes.
   - **Risk:** low to medium, because the heal path must still work. **Behaviour change:** none.
5. **Misleading names in `install.py`.**
   - `counted` (line 247) has a different meaning from `engine.wording.counted`. Rename it to `version_key`.
   - Rename `plain` (line 243) to `redacted` and `reachable` (line 239) to `with_token`.
   - **Risk:** none. **Behaviour change:** none.
6. **Port 8430 written three times.** `serve.py:110`, `191` and `231`. Make it one `DEFAULT_PORT` constant.
   - **Risk:** none. **Behaviour change:** none.
7. **`runner/speed.py` is a benchmark living in the agent runtime layer.** It also imports the `serve` entry module. Its only caller is `commands/queries.py:121`.
   - **Refactor:** move it to `commands/speed.py`.
   - **Likely latent crash:** `Quiet.send(self, text)` (lines 73-74) does not match `Driver.send`. `Agent.flush` passes `groups=` and `yielding=`, so an engine tick with pending events would raise a TypeError. Fix the signature.
   - Lines 50 and 56 use `"AGENT_JOURNAL_ACTIVE"`; use `ACTIVE_ENV`.
   - **Risk:** low. **Behaviour change:** yes, it fixes that crash.

---

## Batch 5: dead code

**Files:** `agents/band.py`, `features/status_bar/test.py`.

- **`agents/band.py` (287 lines) has no runtime caller.** It is imported only by three tests at `status_bar/test.py:90-116`. `SHOWN = False` and the test "an exit clears none of its rows" both confirm the header band is gone. Delete the file and those three tests.
- **Risk:** none. **Behaviour change:** none.

---

## Wave 2 (after wave 1 merges; each item spans files from more than one batch)

- **W1: providers report facts, the runner decides (rule 19).** `base.py:368-392` `facts()` merges into the stored row: the fallbacks (`or row.model`), the `uses` counter, `started`, the `loops` bookkeeping and the `prompted` carry-over.
  - **Refactor:** the provider returns a `HookFacts` dataclass, and the merge into the row moves to `runner/hooks.py:272-274`.
  - In the same change, replace the event literals in `hooks.py` (lines 268-291) and `engine.py:171` with `HookEvent`, and `hooks.py:273` `paths[0]` with a `ToolUse.path` property.
  - **Risk:** medium. **Behaviour change:** none.
- **W2: one `Asking` type.** `base.py:109-117` and `payload.py:50-58` define the same three fields. Keep the one in `payload.py`. Callers: `drivers.py:9`, `codex_driver`, `runner/hooks.py:16`, `runner/engine.py:11`.
  - **Risk:** low. **Behaviour change:** none.
- **W3: one "shell command of a tool call".** `base.py:198-199` `Provider.shell_command` (no provider overrides it) and `payload.py:474-475` `Hook.shell` are the same logic.
  - **Refactor:** a single `ToolUse.shell_command` property. Callers: `engine/ran.py:16`, `features/journal_laws/interceptors.py:62`, `features/work_modes/interceptors.py:26`, `features/plugins/payload.py:47` (which also gets `.path`).
  - **Risk:** low. **Behaviour change:** none.
- **W4: public renames.**
  - `Provider.transcript` → `turns`, `tail` → `last_turns`, `recent` → `recent_rows`. Callers: `runner/hooks.py:170`, `runner/engine.py:143`, `features/agent_sessions/history.py:13`, `features/family_tree/tree.py:107`, `commands/http.py:716`, `commands/queries.py:30` and `:61`, `features/messages/test.py:241`, and the driver modules.
  - Drop the unused `record` parameter from `providers/turns.py`. Callers: `features/ask_questions/handlers.py`, `features/messages/handlers.py`, `runner/engine.py:162`.
  - **Risk:** low. **Behaviour change:** none.
- **W5: `channel.py` is a Claude-only MCP server.** Lines 107 and 123 send `claude/channel` messages from top-level `src`, which breaks rule 18.
  - **Refactor:** move the body to `providers/claude_channel.py`. Keep `channel.py` as the stub entry, because users' `.mcp.json` files and running channels exec `-m channel`. Update `install.py` STUBS.
  - **Risk:** medium to high, because running channels re-exec themselves.
- **W6: one code-change fingerprint.** `serve.py:134-146` `code_snapshot` and `agents/terminal.py:40-42` `watched` compute it with different rules. The build-file path is also written in `runner/engines.py:76`, `channel.py:73-74` and `serve.py:202`.
  - **Refactor:** `engine/package.py` gains `build_file(root)` and `code_stamp()`.
  - **Behaviour change:** possibly, because which edits trigger a reload could change. Choose the semantics deliberately.
- **W7: split `agents/terminal.py`.** It mixes launching (lines 27-91 and 115-177), seating (93-112) and screen I/O (180-243).
  - **Refactor:** split it into `agents/launch.py` and `agents/screen.py`, and drop the one-line `type_keys` wrapper. This touches imports in `commands/http.py`, `commands/queries.py` and six feature files.
  - Also share one `seat_session(root, env, session)` helper with `runner/hooks.py:243-249`.
  - **Risk:** low. **Behaviour change:** none.
- **W8: provider folder names in a feature.** `features/session_recording/recorder.py:10` lists `".claude"` and `".codex"`. Build the list from `PROVIDERS` using the `home` fact added in Batch 1.
  - **Risk:** none. **Behaviour change:** none.
- **W9: peer turns encoded as strings.** `claude.py:387-391` builds `who` strings like `"peer:name:address"`, and `runner/engine.py:143-155` parses them back.
  - **Refactor:** give `Turn` peer fields (in `engine/transcript.py`).
  - **Risk:** medium. **Behaviour change:** none.
- **Optional, low value:**
  - `providers/skill_homes.py` decides between copying and symlinking, which is behaviour; it could move next to `skills.py`.
  - The `runner/engines.py:125-137` "message never reached the agent" alarm is feature behaviour and could become a feature.
  - `runner/hooks.py:258` `handle` accepts either a dict or a `Hook`; making it take only `Hook` means editing the tests that pass dicts.

**Verify each batch** with `pytest`, including `tests/test_the_gate.py` and the `agent_sessions`, `long_commands`, `messages` and `worktrees` feature tests. Also run `journal check sweep`, which includes the duplicate-body check, and compare `journal speed` before and after Batch 1.

### Critical Files for Implementation
- /Users/jessegall/projects/agent-journal/src/providers/base.py
- /Users/jessegall/projects/agent-journal/src/providers/claude.py
- /Users/jessegall/projects/agent-journal/src/providers/codex.py
- /Users/jessegall/projects/agent-journal/src/providers/drivers.py
- /Users/jessegall/projects/agent-journal/src/runner/hooks.py