I covered all 24 feature folders from q to z. The plan has 10 batches: Batch 0 adds shared helpers and must land first, Batches 1–8 can run in parallel worktrees after it, and Batches 9 and 10 change behaviour and need your decision first.

All paths below are under `/Users/jessegall/projects/agent-journal/src/`. Each file belongs to exactly one batch. When a move forces an import change in another feature's file, that file is assigned to the batch doing the move and nobody else touches it.

## Files looked at
I read the whole of every `.py` file in these folders except the test files. For those I read the imports and test names, and the bodies only where a finding needed them.
- **record_audit**: `audit`, `details`, `feature`, `handlers`, `test`
- **reminders**: `details`, `feature`, `test`
- **revisions**: `commands`, `details`, `feature`, `handlers`, `history`, `test`
- **row_links**: `details`, `feature`, `formatters`, `handlers`, `test`
- **rules**: `details`, `feature`, `handlers`, `test`
- **runtime_cleanup**: `details`, `feature`, `handlers`, `tidy`, `test`
- **sequences**: `controller`, `details`, `drafting`, `exploration`, `feature`, `handlers`, `orchestration`, `resource`, `shipped`, `shipping`, `test`
- **session_briefing**: `block`, `details`, `feature`, `handlers`, `start`, `test`
- **session_recording**: `controller`, `demo`, `details`, `feature`, `recorder`, `resource`, `scrub`, `test`
- **sharing**: `controller`, `details`, `feature`, `guard`, `page`, `preview`, `resource`, `routes`, `server`, `services`, `tunnel`, `visitors`, `watchdog`, `test`
- **skill_loading**: `catalogue`, `details`, `feature`, `handlers`, `interceptors`, `required`, `test`
- **source_links**: `details`, `feature`, `handlers`, `test`
- **starting_agents**: `commands`, `details`, `feature`, `handlers`, `test`
- **status_bar**: `bar`, `commands`, `details`, `dissect`, `feature`, `group`, `handlers`, `queue`, `runs`, `shell`, `spoken`, `usage`, `test`
- **suggestions**: `controller`, `details`, `feature`, `handlers`, `test`
- **templates**: `applying`, `controller`, `details`, `feature`, `instructions`, `resource`, `shipped`, `test`
- **terminal**: `details`, `feature`, `log`, `test`
- **thinking**: `details`, `feature`, `handlers`, `test` — no findings
- **tickets**: `commands`, `controller`, `details`, `feature`, `handlers`, `limits`, `phases`, `resource`, `test`
- **triggers**: `controller`, `details`, `feature`, `handlers`, `resource`, `test`
- **update_reports**: `commands`, `details`, `feature`, `gather`, `handlers`, `test`
- **work_modes**: `details`, `feature`, `interceptors`, `modes`, `test`
- **work_tracking**: `auto`, `commands`, `details`, `feature`, `handlers`, `interceptors`, `next`, `tracker`, `test`
- **worktrees**: `details`, `feature`, `handlers`, `test`

The `__init__.py` files are all empty.

---

## Batch 0 — Shared helpers (lands first; adds code only)
Nothing in this batch changes a call site, so it changes no behaviour. Risk is low.

Files it owns: `engine/journal_calls.py` (new), `engine/probe.py` (new), `engine/keeper.py`, `engine/proc.py`, `engine/events/resources.py`, `features/parts.py`, `features/__init__.py`, `controllers/works.py`, `features/sequences/resource.py`, `features/boards/resource.py`, `features/boards/controller.py` (additions only), `features/plans/resource.py`, `features/collections/resource.py`, `resources/base.py`, `features/triggers/controller.py`.

1. **`engine/journal_calls.py`: one reader for `journal …` calls in a shell line.**
   - It finds the calls, skips the global options (taken from one `GLOBAL_OPTIONS` list) and returns noun, verb and arguments.
   - This replaces eight separate parsers, which Batches 1, 2, 4, 5 and 7a adopt: `runner/hooks.py:43`, `sequences/handlers.py:34-37`, `status_bar/commands.py:44`, `status_bar/shell.py:9,101`, `skill_loading/interceptors.py:11`, `sharing/guard.py:15-31`, `sharing/visitors.py:66`, `work_modes/interceptors.py:10`.
   - The option lists already disagree: `guard.py:28` accepts `--env/--agent/--root`, while `shell.py:9` also accepts `--as/--session`.
2. **`Context.refusal(line, **values)` in `features/parts.py`.** It replaces the same "fill a line, return `title - brief`" code in `sharing/guard.py:50-51`, `skill_loading/interceptors.py:47-48`, `ask_questions/interceptors.py:25` and `plans/interceptors.py:12`.
3. **Typed events in `engine/events/resources.py`:** `RuleCreated`, `CommentCreated`, and `WorkCreated` (moved from `work_tracking/handlers.py:23`).
4. **`Works.unparked()`** beside `Works.active` (`controllers/works.py:9`).
5. **`engine/probe.py`** gets `status_of` / `answers` / `reached`, copied from `sharing/controller.py:84-99`. `engine/keeper.py:88` `answers` uses it (same "under 500 answers" rule).
6. **`engine/proc.commits_since(project, since, short, most)`**, used later by update_reports and work_tracking.
7. **`features.switched_on(name, record, behaviour="")`** replaces the `"x" in FEATURES and FEATURES["x"].on(...)` lookups in `work_tracking/auto.py:16-18`, `work_tracking/next.py:22-23` and `sharing/services.py:25-27`.
8. **`RunKey` frozen dataclass in `sequences/resource.py`** for the `"env|about"` run key: `of`, `text`, `here(env)`, `about_ref` (None when started by hand), `flag`.
9. **Board helpers.**
   - `Board.stage_for(meaning)` (resource).
   - `Boards.paused()`, `Boards.of_message(message)` (the board a message refers to) and `Boards.finish(n)` (sets finished and raises `board.finished`).
10. **`member_refs()` on Plan and Collection**, and a default `start_line()` on `Resource` with a Plan override. These are for the sharing and session_briefing batches.
11. **`Triggers.fired(n, about)`, `Triggers.watch_for(title, words)` and `Triggers.unwatch(title, why)`.**

## Batch 1 — Command runs move to the engine; work tracking (most valuable)
Files it owns: `features/status_bar/*`, `features/work_tracking/*`, `features/terminal/*`, `runner/hooks.py`, `runner/worker.py`, `features/pull_requests/handlers.py`, `features/long_commands/handlers.py`, `features/plugins/parts.py`, `features/file_feed/test.py`, `engine/shell.py`, `engine/command_runs.py` (new), `engine/command_effects.py` (new).

- **The run-recording code is in the wrong layer.**
  - `status_bar/runs.py` (whole) and `status_bar/commands.py` (whole) are not the status bar. `runner/hooks.py:18,272-274` calls `commands.shell` / `commands.writes` on every hook, even when the feature is off. So the runner depends on a feature.
  - `CommandRun` is also used by work_tracking, pull_requests, long_commands and plugins.
  - Fix: move `runs.py` → `engine/command_runs.py`, and `commands.py` → `engine/command_effects.py`. `work_tracking/tracker.py:53-63` `count()` moves with them; it is run bookkeeping, not work tracking.
  - Risk: medium (many imports). Behaviour: none.
- **`status_bar/group.py:5-7` `ran()` re-parses `row.commands`** the same way as `runs.command_runs` (`runs.py:85`). Fix: `ran` = dissect of `command_runs(row)`. Separately, `status_bar/handlers.py:32` repeats `current_run`. Risk: low. Behaviour: none.
- **`status_bar/shell.py:25-109` is a general shell splitter** (`split`, `pieces`, `words`, `verb_of`, `unwrapped`, `expanded`) sitting beside `engine/shell.py`. Fix: move the general parts to `engine/shell.py`; `journal_words` and `VALUED` go to `engine/journal_calls.py`. Risk: low. Behaviour: none.
- **`terminal/log.py:6,29`** imports the status bar's `JOURNAL_CALL`. Fix: use `journal_calls`. Also, `terminal/feature.py:9-11` defines a handler inside `feature.py`; move it to `handlers.py`. Risk: low. Behaviour: none.
- **`work_tracking/tracker.py:23-28`: `begin()` and `end()` have the same body.** Fix: one `forget_files(record, n)`. Risk: low. Behaviour: none.
- **"Unparked work" is filtered four ways:** `work_tracking/handlers.py:48-49` `working()`, `auto.py:33`, and part of `next.py:26`. Fix: `Works.unparked()`. Risk: low. Behaviour: none.
- **`work_tracking/handlers.py:169` reads raw `row.data["commands"]` dicts** and hard-codes `"Bash"`. Fix: `command_runs(row)` plus a `CommandRun` shell check (`runs.py:65-70` already holds the tool names). Risk: low. Behaviour: none.
- **`work_tracking/tracker.py:18-20` `committed()` duplicates `update_reports/gather.py:68-75`.** Fix: `engine.proc.commits_since`. Risk: low. Behaviour: none.
- **`work_tracking/auto.py` does two jobs:** the auto-mode state (lines 11-22) and the runner's `CheckIn` watcher (lines 25-50, imported by `runner/worker.py:22`, with the hard-coded `STILL_THERE` text). Fix: move `CheckIn` to `work_tracking/check_in.py` and use `switched_on` in `auto.py:16-18` and `next.py:21-23`. Risk: low. Behaviour: none.
- **`work_tracking/handlers.py:65-91`: `OpenWork` and `CloseWork` each do two to three jobs** (reset tracked files, link the to-do, name parked work). Fix: one handler per job, in the same registration order. Risk: low. Behaviour: none.
- **Class names:** `StatusLineDetails` / `StatusLine` → `StatusBar…`, `WorkFeature` → `WorkTracking`. Keep `WorkDetails`, because sequences imports it. Risk: low.

## Batch 2 — Sequences
Files it owns: `features/sequences/*` (after Batch 0), `features/boards/controller.py`, `features/boards/feature.py`.

- **The run key `"env|about"` is split by hand in 14 places:**
  - `sequences/controller.py:179,184,231,233-235`
  - `sequences/handlers.py:51-53,57,147,174,243,275-277,292`
  - `boards/controller.py:134,290`

  Fix: use `RunKey`. Tickets adopts it in Batch 3. Risk: medium. Behaviour: none.
- **`sequences/controller.py`: the "not running" refusal is written four times** (119-120, 141-142, 151-152, 194-195). "Remove this run from the dict" is written four times (130, 163, 205, and `handlers.py:127`). Fix: `_run_of(r, about)` and `_without(r, key)`. Risk: low. Behaviour: none.
- **`summaries()` filtered by "not completed and not deleted"** at `controller.py:178,214,230` and `handlers.py:104,123,288`. Fix: `_standing()`, or a `running()` query. Risk: low. Behaviour: none.
- **`controller.py:94` reaches a controller by string key** (`CONTROLLERS[TRIGGER]`), which CLAUDE.md forbids. Fix: the `Triggers` class. Risk: low. Behaviour: none.
- **`handlers.py:189-192` and `247-249` build the same step values.** Fix: `step_values(context, sequence, key, run)`. Risk: low. Behaviour: none.
- **`handlers.py:266-267` hard-codes a refusal that repeats the `STEP_HELD` line** (`details.py:113-116`). Fix: `context.refusal(STEP_HELD, …)`. Risk: low. Behaviour: the wording changes slightly.
- **`handlers.py:214-227`: `unfinished_pace` and `pace` are the same function** with different sources. Fix: one `pace(context, own_minutes)`. Risk: low. Behaviour: none. (Merging the two knobs themselves is a decision for you; see Batch 9.)
- **Line-name constants live in `handlers.py:27-33`, and `details.py:4` imports them from there.** That is backwards: in row_links, handlers import names from details. Fix: move them to `details.py`. Risk: low. Behaviour: none.
- **`handlers.py:67-81,146-165` mixes two jobs into the step handlers.**
  - The `DISPATCH_MODELS` registry, its `unchosen` fallback, `dispatched_by_line`, `board_of` and `request_of` are the board-filler dispatch job. Fix: move them to `sequences/dispatch.py`, and update the import in `boards/feature.py:9`.
  - `board_of` duplicates `tickets/limits.py:34-35`. Fix: `Boards.of_message`.
  - Risk: low. Behaviour: none.
- **Other modules call private controller methods:** `handlers` uses `_in_hand`, `_steps`, `_left`; `boards/controller.py:134,241-248` uses `_finish`, `_jump`. Fix: make them public `@internal` methods (`in_hand`, `steps_of`, `left`, `finish`, `jump`), like `update_run` and `give_up` already are. Risk: low. Behaviour: none.
- **`shipped.py:280` and `313` build the same title→n map**, once per shipped sequence (quadratic). `shipped.py:316-321` lists the same nine fields twice. Fix: build the map once; one field tuple. Risk: low. Behaviour: none.
- **`shipped.py:291-305` (`watched` / `unwatched`) manages Trigger rows.** Fix: `Triggers.watch_for` / `unwatch`. Risk: low. Behaviour: none.
- **`shipping.py:9-27`: `starts_on` / `started_by` are typed `str | None`**, then two properties turn None into `""`. Fix: default them to `""` and delete `start_moment` / `starter`. Risk: low. Behaviour: none.
- **`handlers.py:283` uses `journal.of("question")`.** Fix: the `Questions` class. Risk: low.
- **`boards/feature.py`:** its `START_PARTS[ORCHESTRATION]` line also switches to `register_global` (see Batch 6). This one is behaviour-changing.

## Batch 3 — Tickets
Files it owns: `features/tickets/*`, `features/hosting/feature.py`, `features/hosting/apps.py`.

- **Dead code and a shadowed import in `tickets/controller.py`.**
  - `_plan_waits` is defined twice (254 and 293); the first (ready only) is dead.
  - Lines 21 and 29 both import `DONE`. The plans one shadows the boards one, so `meaning == DONE` at 576 compares a board meaning with the plan status constant. Both are `"done"` today, so it works by accident.
  - Fix: delete 254-255; import the plans one as `PLAN_DONE`; use it at 483 instead of the literal `"done"`.
  - Risk: low. Behaviour: none.
- **Dead code that also costs work.**
  - `controller.py:357-361` `_review` and `REVIEWED_BY_SUBAGENT` (53) are never used.
  - Four Lines in `details.py` are never said: `plan_checkpoint` (47), `ticket_attention` (55), `ticket_plan_done` (91), `plan_waits` (113). The handlers raise moments for sequences instead and only reuse the names as once/every keys.
  - `controller.py:476` computes `_ahead()`, which runs `git rev-list` in every repository, for values that `handlers.py:42-43` throws away.
  - Fix: delete all of these, and keep the key names as constants.
  - Risk: low. Behaviour: the Settings page lists four fewer lines, and fewer git calls run.
- **The ticket's board is looked up by hand about 10 times** (`Boards(...).load(ticket.board) if ticket.board else None` at 221, 247, 334, 359, 364, 576, 620, 627, 650, 838). "Find the stage with this meaning" is written twice (248, 576). Fix: `_board(ticket)` and `Board.stage_for`. Risk: low. Behaviour: none.
- **`tell` (224-233) and `screen` (235-243) repeat** "find the session, refuse if none, build the driver". Fix: `_driver(ticket, doing)`. `ask_session(root, terminal_of(root, session))` is repeated at 509, 562, 637; fix: `_ask_to_stop(session)`. Risk: low. Behaviour: none.
- **`controller.py` is an 847-line god class.** Split it:
  - `tickets/cards.py`: `board()`, `_card`, `_runtime`, `_agent_state`, `_live_state`, `_waited_run`, `_reporting`, `_slots`, `_roles`, `_role_work`, `_repository_states`, `_actions`, `CardState`, `Slots`, `ordinal`, `ago`, `CARD_EXTRAS` (hosting import).
  - `tickets/landing.py`: `Landing`, `_repositories`, `_merged`, `_ahead`, `_clean`, `_off_branch`, `_started_at`, `_based`, `_branch`, `_into`, `keep_branches`.
  - `tickets/orchestration.py`: `_orchestrating`, `_awaiting_orchestrator`, `_orchestrator_may`, `_awaiting_decisions`, `_why_not`, `_as_orchestrator`, `_reviewing`.
  - `tickets/calls.py`: `_calls`, `_waits_on_people`, `_people`. Deciding what the orchestrator is told is feature behaviour, not a controller operation.
  - The controller keeps the CLI verbs. Risk: medium. Behaviour: none.
- **`controller.py:340-350` and `372-376` parse sequence run keys** (`rsplit(":")`, `endswith("|ref")`). Fix: `RunKey`. Risk: low. Behaviour: none.
- **`handlers.py:24-53`: `LookAfterTicketBranches` does seven jobs in one tick.** Fix: one handler per job, registered in the same order. Risk: medium (the order within a tick must be kept). Behaviour: none.
- **`handlers.py` raises other types' events directly.**
  - The paused-board set is built twice (33, 96).
  - Raw `record.emit("ticket"/"board", …)` at 37, 43, 53, 91 bypasses the controllers.
  - `FinishTheBoardWithItsLastTicket` (80-91) updates the board itself.
  - Fix: `Boards.paused()`, a `Tickets.raise_moment(n, m)` method, and `Boards.finish(n)`.
  - Risk: low. Behaviour: none.
- **`handlers.py:68` uses `journal.of("question")`.** Fix: `Questions`.
- **`handlers.py:20` `all_parked`.** Fix: `Works.unparked()`.
- **`phases.py:7` `start_phase_tickets` has the same name as `plans/worker.py:39`**, the registry runner that calls it. Fix: rename it `start_tickets_of_phase`. Risk: low.
- **`commands.py:14,16`: `and not t.completed` / `not todo.completed` are dead conditions**, because `_standing()` already drops completed rows. `controller.py:164-167` filters summaries by hand. Risk: low. Behaviour: none.

## Batch 4 — Sharing
Files it owns: `features/sharing/*`, `features/critique/controller.py`.

- **`controller.py` (440 lines) does five jobs:** links, visitor comments and answers, tunler admin, shared-page data, and passwords. Split:
  - Visitor comments and answers (147-249) → `visiting.py`.
  - Scope and page data (351-428) → `page_data.py`.
  - `hashed` / `matches` / `_unlocked` → `passwords.py`.
  - Tunler verbs become one-line delegates to `tunnel.py`.
  - The CLI method signatures stay on the controller. Risk: medium. Behaviour: none.
- **`controller.py:84-99` HTTP probes duplicate `engine/keeper.py:88`.** Fix: use `engine/probe.py`; switch `critique/controller.py:12` to it; `watchdog.py:69` `serving()` uses `keeper.answers`. Risk: low. Behaviour: none.
- **The agent's list of comments it has not agreed to (`UNAGREED`) is edited in three places:** `guard.py:40` adds, `controller.py:186` removes (by session), `controller.py:228` removes (primary agent). Fix: `hold(agent, ns)` / `unhold(agent, n)` in `visitors.py`. Risk: low. Behaviour: none.
- **`controller.py:173-177` and `209-213` build the same "Let the agent act on it" message.** Fix: one helper. Risk: low. Behaviour: none.
- **`services.py:17-21` `open_shares` re-implements `Share.ended`** (`resource.py:34-36`) on summary dicts. Fix: one open-share check that takes the summary. Risk: low. Behaviour: none.
- **`controller.py:299` `_host` repeats the setting default `"tunler.jessegall.nl"` from `details.py:66`.** Fix: `SharingDetails.values(record).host`. Risk: low.
- **`controller.py:428` uses `CONTROLLERS["plan"]`** (string key). Fix: `Plans`. **`controller.py:45-48` `member_refs` switches on `row.type`.** Fix: the resource's `member_refs()`. Risk: low. Behaviour: none.
- **`server.py`: `do_GET` (101-115) and `do_POST` (145-155) repeat** path parsing, share-by-token lookup and the unlock check. Fix: `_opened(parts)`, keeping the 404/410/401 codes exactly. Also, `server.py:265` re-imports `LAYOUT_FILE` inside `main()` and never uses it (dead import). Risk: low. Behaviour: none.
- **`sharing.json` is read twice with different error handling:** `tunnel.py:25-38` with `json.loads`, `controller.py:315` with `read_json`. Fix: one `kept_address(root)`. **`tunnel.py:102-112` `ran()` duplicates `engine/proc.ran`,** and `asked_status` (92) bypasses even that. Risk: low. Behaviour: none.
- **`watchdog.py:21-64` `KeepTunnelAnswering` does three jobs** (address refused, settings unreadable, misses and restart), and writes the "flag once, then message" idiom three times (29-33, 39-42, 55-59). Fix: three handlers, or one `_alert_once` helper. Risk: low. Behaviour: none.
- **`guard.py:59-61` filters `ResourceCreated` for comments** and uses `journal.of("comment")`. Fix: `CommentCreated` and the `Comments` class. **`guard.py:15-31`, `visitors.py:66`:** adopt `journal_calls`. **`guard.py:50-51`:** use `refusal`. Risk: low. Behaviour: `--as` / `--session` start being accepted before `share agree`.

## Batch 5 — Skill loading
Files it owns: `features/skill_loading/*`, `skills.py`.

- **`catalogue.py:119-120` `defaults()` is an alias of `primary()`.** Fix: delete it. Risk: low. Behaviour: none.
- **`catalogue.py:87-91` `marked()` is byte-for-byte `controllers/stored.py:33` `mtime`.** Fix: use `mtime`. Risk: low.
- **`catalogue.py:42-44` `frontmatter()` duplicates `skills.py:126`.** Fix: one function in `skills.py`. Risk: low. Behaviour: none.
- **`catalogue.py:131-142`: `loaded_at` and `loaded_before_compaction` share the same provider guard.** Fix: `_provider_of(agent)`. Risk: low.
- **`interceptors.py:31-35` `require_named` is not an interceptor** (it is used by `handlers.py:84`). Fix: move it to `required.py`. **`interceptors.py:11`:** adopt `journal_calls`. **`interceptors.py:47-48`:** use `refusal`. Risk: low.
- **`feature.py:14` `START_PARTS[SKILLS] = …`** switches to `register_global` (Batch 9 decision). Class names `SkillsDetails` / `Skills` → `SkillLoading…`.

## Batch 6 — Session briefing
Files it owns: `features/session_briefing/*`, `features/journal_laws/feature.py`, `features/form_of_address/feature.py`.

- **`start.py:14-15` `open_work` uses `CONTROLLERS["work"]`** (string key, against CLAUDE.md). `start.py:6-7` `standing()` repeats `Feature.standing` (`base.py:250`). Fix: `Works(...)._standing()`. Risk: low. Behaviour: none.
- **`start.py:33-38` `describe()` branches on `r.type == "plan"`.** Fix: `r.start_line()` (Batch 0). Risk: low. Behaviour: none.
- **`test.py:2-12` imports the same names twice.** `test.py:100-240`: six of its ten tests cover the channel, typing and drivers, not the briefing. Fix: move them to the feature that owns that code. Tests only.
- **Class names:** `StartDetails` / `Start` → `SessionBriefing…`.
- **The other `START_PARTS[...] =` lines** (`journal_laws/feature.py:20`, `form_of_address/feature.py`) are part of the Batch 9 decision.

## Batch 7a — Small features I
Files it owns: `rules`, `triggers`, `templates`, `suggestions`, `update_reports`, `work_modes`, `starting_agents`, `source_links` (all under `features/`), plus `controllers/environments.py`.

- **`rules/handlers.py:21-27` subscribes to every created row and filters for rules.** Fix: `RuleCreated`. The same applies to `templates/instructions.py:39-43` (use `WorkCreated`). Risk: low. Behaviour: none.
- **`triggers/handlers.py:27` raises `trigger.fired` with a raw `emit`.** Fix: `Triggers.fired`. The text a trigger says is written `row.text or row.brief` at 22, 26, 38, 49, but `row.brief or row.text` at 24. Fix: a `Trigger.said` property, keeping the message's order. Risk: low. Behaviour: none.
- **`templates`: two template lookups.** `applying.py:11-17` `chosen()` raises; `instructions.py:9-17` `template_of()` returns None; both load the same template. Fix: one `Templates` lookup.
  - `applying.py:38-41` branches on `event.type == "plan"`. Fix: the controller adds the part, and Plans overrides it to make a phase.
  - `instructions.py:12` scans every plan to find the one holding a to-do. Fix: a Plans query.
  - `shipped.py:5-46` describes shipped templates as string-keyed dicts. Fix: a `ShippedTemplate` dataclass.
  - Risk: low to medium (touches the plans controller). Behaviour: none.
- **`suggestions/controller.py:30-34` `complete()` writes twice** (update, then complete), so two events. Fix: pass `decision` through `super().complete(**data)`. Feature class `SuggestionsDecided` → `SuggestionsFeature`. Risk: low. Behaviour: one fewer "updated" event.
- **`update_reports/commands.py:47-55` and `69-77` repeat "find the row or refuse".** Fix: `_item_of`.
  - `rows_of` (18) clashes with `plans/resource.rows_of`; rename it `items_of`.
  - The command class `Item` clashes with the `gather.Item` TypedDict; rename it `AddItem` and keep `name="item"`.
  - `gather.py:68-75`: use `commits_since`.
  - `handlers.py:30`: the literal `"update"` should be `UPDATE`.
  - Risk: low. Behaviour: none.
- **`work_modes/modes.py:14-16,24`** reads and writes the mode setting by hand, although `details.py:27` declares the Setting. Fix: `WorkModesDetails.values()`. `interceptors.py:10`: adopt `journal_calls`. Risk: low. Behaviour: none.
- **`starting_agents/commands.py:17-20` and `handlers.py:34-35` repeat** `detached(...)` plus `update(launched…)`. Fix: `Environments.launch(n, provider, conversation="")`. Risk: low. Behaviour: none.
- **`source_links`:** class names `TrackingDetails` / `Tracking` → `SourceLinks…`.

## Batch 7b — Small features II
Files it owns: `record_audit`, `row_links`, `revisions`, `runtime_cleanup`, `session_recording`, `worktrees`, `reminders` (all under `features/`), plus `controllers/docs.py`, `commands/parser.py`, `features/phone/controller.py`, `engine/project_files.py`.

- **`record_audit/audit.py:48-71` `evidence()` does two jobs:** stale claims, and to-dos waiting on old questions.
  - It returns dicts whose `"title"` key nobody reads.
  - It builds the retire command two ways: `named("complete")` at 59, a hard-coded `done` at 70.
  - It uses `_every()` plus a completed check where `_standing()` already does that.
  - Fix: `claim_evidence` / `waiting_evidence` returning an `Evidence(ref, evidence, retire)` dataclass. Todos' complete name is `done`, so behaviour stays the same.
  - Class names `CleanupDetails` / `Cleanup` → `RecordAudit…`.
  - Risk: low. Behaviour: none.
- **"Is this path in the project" is checked twice:** `record_audit/audit.py:31-34` `present()` and `row_links/formatters.py:110-120` `resolved` / `existing`. Fix: one `exists_in_project` in `engine/project_files.py`. Risk: low.
- **`row_links/formatters.py` does two jobs:** row chips (lines 12-89) and file/commit/URL chips (92-171). Fix: move the path chips to `row_links/paths.py`. The row chips stay in `formatters.py`, so `sharing/page.py`'s import does not change.
  - `found()` (139-143) and `coded()` (156-159) repeat the "path in a code span" check. Fix: one `code_path(span, project)`.
  - Risk: low. Behaviour: none.
- **`revisions/commands.py:35-43` `Cut` removes a doc section; that is not revision history.** Fix: `Docs.cut(n, title)`. Risk: low. Behaviour: `journal doc cut` keeps working when revisions is switched off.
- **`runtime_cleanup/tidy.py`:**
  - 96-97 `summary()` only passes through `Tidied.summary`. Fix: `commands/parser.py:15` uses `.summary` directly.
  - 59-63 `stamp()` duplicates `mtime` (and 52 already uses `mtime`).
  - 48-52 hard-code runtime paths that `engine.runtime.folder` / `sessions` already provide.
  - `details.py:22` help text says `housekeeping.days`; the setting is `runtime_cleanup.days`.
  - Class names `Housekeeping…` → `RuntimeCleanup…`.
  - Risk: low. Behaviour: none.
- **`session_recording/recorder.py:44-66` re-implements incremental event-log reading.** Fix: `Record(root, env).events(since=newest)` (`engine/record.py:129-157`). Risk: medium. Behaviour: none.
- **`session_recording/demo.py:85-183` `Throwaway` does three jobs:** the sandbox (86-130), the API answers (132-153) and the phone reads (156-183). Its phone part also builds a `Phone` from internal keys (165-166). Fix: split the class; add `Phones.connected()` / a stand-in on the phone controller. Risk: low. Behaviour: none.
- **Misplaced tests:** `worktrees/test.py` (9 of its 10 tests cover sessions, seats and `journal claude -w`) and `reminders/test.py:9` (a local `nudges` beside `tests.kit.nudges`). Tests only.

## Batch 8 — Misplaced tests elsewhere (optional)
`status_bar/test.py:90-185` (terminal band, typing, pause), `tickets/test.py:69`, `work_tracking/test.py:155,314` and `revisions/test.py:37` test other owners' code. Fix: move them after Batches 1, 3 and 7b land. No behaviour change.

## Batch 9 — Decisions for you (behaviour changes)
- **`START_PARTS`** is filled by direct assignment in five features. Those parts stay in the start block when the feature is switched off. Switching them to `register_global` would drop them. This affects Batches 2, 5, 6 and 7a.
- **Tickets globals:** `tickets/feature.py:38-41` writes `PHASE_ROWS` / `CARD_ROWS` / `PHASE_STARTS` directly, so they stay active when tickets is off and are never cleared. Fix: `register_always` / `register_global`.
- **Two reminders for a standing sequence step.** Sequences has `NudgeWaitingStep` (WAITING) and the UNFINISHED nudge, plus two settings for one interval (`details.py:53-68`). Merge them into one reminder and one setting? That needs a settings migration.
- **Hard-coded nudges.** `sharing/controller.py:169,229`, `sharing/watchdog.py:57,64` and `work_modes/modes.py:25` speak through `Nudges._to_primary` with hard-coded text instead of the feature's Lines. Moving them to Lines changes wording and while-waiting handling.
- **`tickets/controller.py:816-821` `_live()` writes while answering a query**, and it is called when rendering the board. Move that write to the clock tick?
- **Moving the board-filler content to the boards feature.** Candidates:
  - `sequences/exploration.py`, `drafting.py`, `orchestration.py`, and the board sequences in `shipped.py`.
  - The `PANEL` / `FILLER` / `LOG` constants.
  - `tickets/limits.py:29-52` (`PanelRepliesStayShort`, `FillerKeepsToTheBoard`).

  This touches migrations m0022/m0023, `install.py:492` and `journal_laws/handlers.py`. It is large; do it only after Batches 2 and 3.
- **`status_bar/handlers.py:29-44` `MarkTestRuns`** posts test cards to the chat feed, which is not a status-bar job. Where should it live?

## Batch 10 — Optional, after Batches 2, 5 and 6
`catalogue.py:182-188 always()`, `commands/http.py:462` and `boards/controller.py:153` each rebuild the session briefing block directly. Instead, `session_briefing/handlers.py` would rebuild it on the setting change. Behaviour: the rebuild happens a moment later (deferred) instead of at once.

### Critical Files for Implementation
- /Users/jessegall/projects/agent-journal/src/features/status_bar/commands.py
- /Users/jessegall/projects/agent-journal/src/features/sequences/handlers.py
- /Users/jessegall/projects/agent-journal/src/features/tickets/controller.py
- /Users/jessegall/projects/agent-journal/src/features/sharing/controller.py
- /Users/jessegall/projects/agent-journal/src/features/parts.py