# leeroy-wip manual review notes (full coverage, WIP)

Generated: 2026-01-24
Source: `upstream/main..leeroy-wip` (indexed `001..249`, oldest → newest)

## Mandatory Workflow (strict, per-commit)

This document is only trustworthy if every commit section is derived from the actual patch content.

### Pass 1: Initial manual review (already done)

- For every commit `NNN`:
  - Retrieve the full patch and base notes on it:
    - `happys wt git happy slopus/tmp/leeroy-wip -- show --no-color --format=fuller <sha>`
  - Write `### Manual Review Notes`, `### Verdict`, and a suggested rewrite message.

### Pass 2: Full-diff verification (MANDATORY; must cover 249/249)

For every commit `NNN`, even if the diff is very large:

1. Generate the full patch to disk (this is the non-negotiable “full diff” evidence):
   - `happys wt git happy slopus/tmp/leeroy-wip -- show --no-color --format=fuller <sha> > /tmp/leeroy-wip-pass2/<NNN>.patch`
2. Record verifiable evidence for the patch:
   - `patchSha256` (sha256 of the patch file)
   - `patchBytes` and `patchLines`
3. Re-check the existing `### Manual Review Notes` against that patch:
   - If the notes are accurate: do not rewrite them (avoid churn); just mark verification.
   - If anything is missing/incorrect (mixed concerns, wrong ordering deps, wrong file list/behavior): refine the notes.
4. Add/update the per-commit marker line immediately under the `## NNN ...` heading:
   - `Pass2FullDiff: CAPTURED at <ISO8601> sha=<12> patchFile=/tmp/leeroy-wip-pass2/<NNN>.patch patchSha256=<64> patchBytes=<n> patchLines=<n> notesReviewed=<yes|no> reviewedAt=<ISO8601|-> notesUpdated=<yes|no>`

Important: for “diff summary skipped” commits, Pass 2 must replace “skipped” with an actual diff-based summary.

### Pass 3: PR assignment (MANDATORY; must cover 249/249)

Goal: assign every commit to exactly one “final feature / final fix” PR, without skipping any commit section.

Rules:

1. For each commit `NNN`, read the **entire** section in this document (not just the subject line).
2. Decide which PR this commit belongs to (or mark it “needs new PR”).
3. Record the decision in the working TSV:
   - `docs/commit-audits/happy/leeroy-wip.pr-assignment.working.tsv`
4. Add a per-commit marker line immediately under the `Pass2FullDiff:` line:
   - `Pass3PrPlan: finalPr=<PRxx|TBD> plannedAt=<ISO8601|-> manualReviewedForPrPlan=<yes|no> notes=<free text>`

Notes:

- Do **not** create a separate PR for translations/a11y/tests that belong to an existing final feature. Instead, fold them into that feature’s PR.
- If the commit verdict is `split`, you may still assign it to one PR (but note the split plan in `notes`).

---

## 001 2026-01-17 58528a7c37d3 `crypto`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=58528a7c37d3 patchFile=/tmp/leeroy-wip-pass2/001.patch patchSha256=48c936d17414ce5361a3086fd65da99e39d72f6c998f554d2b1ace589969063e patchBytes=1529 patchLines=34 notesReviewed=yes reviewedAt=2026-01-24T18:00:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Foundations/DevX (build/patch infra); consider squashing with other patch-package plumbing.

- Subject: chore(crypto): patch react-native-libsodium
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Adds an iOS pods patch for `@more-tech/react-native-libsodium` (`react-native-libsodium.podspec`).
- Change: when `ENV['RCT_USE_RN_DEP'] != '1'`, the podspec no longer blindly uses `folly_version` (which can be undefined during evaluation in some Expo / RN >= 0.81 setups). It now:
  - uses `folly_version` when defined
  - otherwise falls back to an unpinned `s.dependency 'RCT-Folly'`
- Purpose: prevent pod install / podspec evaluation failures caused by an undefined `folly_version`.

### Verdict

- Keep/squash/split/drop/reorder: keep, but likely squash with other libsodium/postinstall patching commits in the same topic (to keep “patch-package maintenance” cohesive).
- Depends on: expo iOS build pipeline uses `expo-app/patches/*` (patch-package or equivalent); ensure the patch is applied in postinstall.

### Rewrite Commit Message

- Keep as-is: `chore(crypto): patch react-native-libsodium`

---

## 002 2026-01-17 552b8da0f621 `deps`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=552b8da0f621 patchFile=/tmp/leeroy-wip-pass2/002.patch patchSha256=70706e72b85504f86151e911c2a8868a33ae7ba3cfda2ac29ce89acee1ae847f patchBytes=1439 patchLines=38 notesReviewed=yes reviewedAt=2026-01-24T18:00:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Foundations/DevX (test types dep).

- Subject: test(deps): add @types/react-test-renderer
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Adds `@types/react-test-renderer` to `expo-app/package.json` dev dependencies.
- Updates `expo-app/yarn.lock` with the new `@types/react-test-renderer@^19.1.0` entry.
- Coherence: fully matches the subject; no unrelated files.

### Verdict

- Keep/squash/split/drop/reorder: keep; can be squashed with adjacent “test deps” commits if we’re minimizing granularity.
- Depends on: none (safe standalone).

### Rewrite Commit Message

- Suggested tweak (optional): `test(expo-app): add @types/react-test-renderer`

---

## 003 2026-01-17 017b15c07ad7 `config`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=017b15c07ad7 patchFile=/tmp/leeroy-wip-pass2/003.patch patchSha256=3100e5166a4c76905c7effa2bb865b80821cdda72665ed6d5ee9ee7626a7471c patchBytes=2186 patchLines=62 notesReviewed=yes reviewedAt=2026-01-24T18:00:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Foundations/DevX (app config reliability).

- Subject: chore(config): harden app variant defaults
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Hardens `expo-app/app.config.js` config derivation:
  - Adds opt-in local overrides: `EXPO_APP_NAME`, `EXPO_APP_BUNDLE_ID`, `EXPO_APP_SCHEME`.
  - Replaces direct `namesByVariant[variant]` / `bundleIdsByVariant[variant]` indexing with safe fallbacks to development values when `APP_ENV` is unknown.
  - Keeps default URL `scheme` stable for upstream (“happy”) but allows overrides to avoid collisions in multi-install setups.
- Coherence: entirely config-scoped; message matches diff.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is reliability hardening for local dev variants and prevents “undefined name/bundle id” configs.
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `chore(config): harden app variant defaults`

---

## 004 2026-01-17 4890a471df31 `auth`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4890a471df31 patchFile=/tmp/leeroy-wip-pass2/004.patch patchSha256=181784f9f7f930afca8c24951e7b885dbc21f8390185cb75a2bba25293ea31b3 patchBytes=7931 patchLines=214 notesReviewed=yes reviewedAt=2026-01-24T18:00:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR04 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Auth + storage scoping (web persistence hardening + scoped native keys).

- Subject: fix(auth): harden tokenStorage web persistence
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Adds web-focused hardening for token persistence and aligns native token storage with storage scoping:
  - Web `getCredentials()` now reads `localStorage` once and guards JSON parsing in a `try/catch`, returning `null` on invalid JSON instead of throwing.
  - Web `setCredentials()` / `removeCredentials()` now guard `localStorage.setItem/removeItem` errors (quota/security errors) and return `false` on failure.
  - Native path now uses a scoped key via `scopedStorageId(AUTH_KEY, scope)` (scope read from env) and caches credentials **per key** (`credentialsCacheKey`) to avoid returning stale data if the scope changes.
  - Cache JSON parsing is guarded; cache parse errors fall through to SecureStore read.
- Adds focused vitest coverage for the web behavior:
  - invalid JSON → `null`
  - set/remove exceptions → `false`
  - single `getItem` call per `getCredentials()`

### Verdict

- Keep/squash/split/drop/reorder: keep; belongs in an “auth storage / storage scoping” topic (likely near other storage-scope commits).
- Depends on: `readStorageScopeFromEnv` + `scopedStorageId` semantics; verify ordering so the utility exists before this commit in the rewritten history.

### Rewrite Commit Message

- Keep as-is: `fix(auth): harden tokenStorage web persistence`

---

## 005 2026-01-17 4adc41d92af0 `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4adc41d92af0 patchFile=/tmp/leeroy-wip-pass2/005.patch patchSha256=a413acfa408057ab2dd0bc65925982631375704ab9af67ee7e0ec3864cd80802 patchBytes=9077 patchLines=250 notesReviewed=yes reviewedAt=2026-01-24T18:00:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Permission framework baseline (types/mapping/normalization); model-mode allowlist may later be separated if needed.

- Subject: feat(sync): add permission mode types and mapping
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Introduces shared permission-mode primitives and cross-agent mapping:
  - Adds `PermissionMode` union containing Claude modes (`default`, `acceptEdits`, `plan`, `bypassPermissions`) and Codex/Gemini modes (`default`, `read-only`, `safe-yolo`, `yolo`).
  - Adds validators/normalizers:
    - `isPermissionMode`
    - `normalizePermissionModeForAgentFlavor` (clamps incompatible modes to `default`)
    - `normalizeProfileDefaultPermissionMode` (profile defaultPermissionMode is restricted to Claude modes; codex-like values clamp to `default`)
  - Adds `mapPermissionModeAcrossAgents(mode, from, to)` to map between Claude ↔ Codex/Gemini permission sets (and preserve Codex↔Gemini modes).
  - Adds `MODEL_MODES` and `isModelMode` with tests (note: this couples model-mode validation to this commit; consider whether it belongs in the same topic or a separate “model modes” commit).
- Adds tests that cover:
  - the mapping table between agents
  - normalization/clamping behavior
  - basic `isPermissionMode` / `isModelMode` validation
- Coherence: all changes are in `expo-app/sources/sync/*` and are tightly related to permissions/modes, but the model-mode validation is adjacent rather than strictly permission-specific.

### Verdict

- Keep/squash/split/drop/reorder: keep; foundational for later permission-mode persistence and UI toggles. Consider splitting out `MODEL_MODES/isModelMode` into its own commit if it ends up being used broadly outside permission-mode work.
- Depends on: `expo-app/sources/sync/modelOptions` `AgentType` definition; any later commits that persist/restore permission modes should come after this.

### Rewrite Commit Message

- Suggested tweak (optional): `feat(permission): add permission mode types and cross-agent mapping` (current `feat(sync): ...` is acceptable since files live under `sync/`).

---

## 006 2026-01-17 a5fbc3e8ba00 `persistence`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a5fbc3e8ba00 patchFile=/tmp/leeroy-wip-pass2/006.patch patchSha256=71fe190c0a569839e8a14c097f8700658da1a7a41054a4d76045a90c4dd0d6b3 patchBytes=13668 patchLines=348 notesReviewed=yes reviewedAt=2026-01-24T18:03:45+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR04 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Primarily storage scoping + persistence hardening; during rewrite likely split into (a) storage scoping core and (b) model-mode persistence helpers.

- Subject: feat(persistence): scope storage and validate draft modes
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Introduces “storage scoping” to prevent cross-stack/worktree state bleed (primarily for native runtimes):
  - Adds `expo-app/sources/utils/storageScope.ts` + tests:
    - reads `EXPO_PUBLIC_HAPPY_STORAGE_SCOPE` and normalizes it into a safe id fragment (conservative charset + clamp length).
    - `scopedStorageId(baseId, scope)` → stable `${baseId}__${scope}` when scope present.
  - Updates `expo-app/sources/sync/persistence.ts` to initialize MMKV with a scoped id when scope is present (and avoids scoping in web runtime).
- Hardens persisted draft parsing (and expands what is persisted):
  - `loadNewSessionDraft()` now clamps `permissionMode` via `isPermissionMode` and `modelMode` via `isModelMode` instead of trusting arbitrary strings.
  - Adds `selectedProfileId` to the persisted draft shape and returns it when present.
- Adds persistence for per-session model modes:
  - Defines a `SESSION_MODEL_MODES` allowlist and filters persisted `session-model-modes` entries to that allowlist.
  - Adds `loadSessionModelModes()` / `saveSessionModelModes()` (with tests).
- Minor robustness tweak:
  - `loadSettings()` now treats `version` as nullable unless it’s actually a number (avoids propagating invalid versions from corrupted persisted state).
- Coherence: all changes are about persistence correctness and isolation; message matches diff.

### Verdict

- Keep/squash/split/drop/reorder: keep; foundational for multi-stack reliability and later model-mode persistence.
- Depends on:
  - `isPermissionMode` / `isModelMode` exports from `sync/permissionTypes` (introduced earlier in this history; keep ordering intact).

### Rewrite Commit Message

- Keep as-is: `feat(persistence): scope storage and validate draft modes`

---

## 007 2026-01-17 18e77c46f38a `session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=18e77c46f38a patchFile=/tmp/leeroy-wip-pass2/007.patch patchSha256=a9e88970d34226b5cb63b54ef3e10143e45db6bc52b39456270e46a52cf25ede patchBytes=2980 patchLines=59 notesReviewed=yes reviewedAt=2026-01-24T18:03:45+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR28 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Model modes final feature (UI clamping correctness).

- Subject: fix(session): clamp configurable model modes
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Prevents invalid model-mode writes from the Session UI:
  - `expo-app/sources/-session/SessionView.tsx` changes `updateModelMode` to accept `ModelMode` but only persists modes that are in a local allowlist of configurable Gemini modes.
  - Removes previous `as any` casts when passing `modelMode`/handler down to the input component.
- Coherence: good; subject matches diff.

### Notes / Risks / Follow-ups (for history rewrite planning)

- There is duplication of the “configurable Gemini model modes” allowlist across commits (also appears in persistence/model-mode work). Consider centralizing this list in a single shared module during the history rewrite (or later refactor).

### Verdict

- Keep/squash/split/drop/reorder: keep; correctness hardening, small + focused.
- Depends on: `ModelMode` type from `sync/permissionTypes`.

### Rewrite Commit Message

- Keep as-is: `fix(session): clamp configurable model modes`

---

## 008 2026-01-17 f7bb8a28fff7 `tools`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=f7bb8a28fff7 patchFile=/tmp/leeroy-wip-pass2/008.patch patchSha256=0f1443318c9c721269b45a532998bedab5cc0fc91c350dc4d0510c4896b52035 patchBytes=18534 patchLines=404 notesReviewed=yes reviewedAt=2026-01-24T18:03:45+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR39 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Tool UX (normalization + rendering): ACP tool parsing hardening, safe tool_result normalization, and tool-title generation (includes required i18n keys).

- Subject: fix(tools): harden ACP tool parsing and titles
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Hardens UI tool-title generation for Gemini/ACP shapes:
  - `knownTools.tsx` now checks that nested `locations[0].path` is a non-empty string before using it.
  - Adds stronger type guards for nested `toolCall`/`input` shapes when extracting titles.
  - Changes `change_title` tool title from a raw literal to `t('tools.names.changeTitle')`.
- Improves Gemini Execute tool view UX:
  - `GeminiExecuteView.tsx` uses `t('tools.geminiExecute.cwd', { cwd })` instead of embedding an icon/prefix literal.
- Makes raw-message normalization more resilient and safer around secrets:
  - `typesRaw.ts` changes `tool_result.content` validation from “string or array of text blocks” to `z.any()` and normalizes later.
  - Adds `toolResultContentToText(...)` that converts:
    - `null/undefined` → `''`
    - string → string
    - Claude-style `[{type:'text', text:'...'}]` → concatenated text
    - objects/other arrays → `JSON.stringify(...)` best-effort
  - Reduces validation failure logging to avoid dumping full raw messages; prints detailed issues only in `__DEV__`.
  - Adds tests for ACP tool result normalization (tool-result + tool-call-result; string/object/null cases).
- Coherence: consistent with “ACP parsing hardening”; message matches changes.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is correctness + safety hardening for tool rendering and raw message normalization.
- Depends on:
  - i18n keys for `tools.names.changeTitle` and `tools.geminiExecute.cwd` existing (otherwise fallback rendering may show raw keys until later i18n commits land).

### Rewrite Commit Message

- Keep as-is: `fix(tools): harden ACP tool parsing and titles`

---

## 009 2026-01-17 292424245277 `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=292424245277 patchFile=/tmp/leeroy-wip-pass2/009.patch patchSha256=61a8397b7db9e5dbdc71a14b647b19e149fc65154fe3d952cbcf2c1b72140946 patchBytes=12798 patchLines=324 notesReviewed=yes reviewedAt=2026-01-24T18:03:45+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR27 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Sync robustness; outgoing message metadata builder is a foundational correctness improvement.

- Subject: refactor(sync): centralize outgoing message metadata
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Introduces a dedicated builder for outgoing user-message metadata:
  - Adds `sync/messageMeta.ts` with `buildOutgoingMessageMeta(...)` and focused unit tests ensuring:
    - optional fields are only included when explicitly provided
    - `displayText` is included even when it’s an empty string (important: empty is meaningful, not “absent”).
- Updates `sync.ts` to use `buildOutgoingMessageMeta(...)` when constructing outgoing raw records:
  - Passes `model` only for Gemini and only when non-default; stops sending `fallbackModel: null`.
- Also trims noisy debug logs in a few unrelated sync paths (machines fetch, purchases, update handler), with lifecycle-event debug logs gated by `__DEV__`.

### Verdict

- Keep/squash/split/drop/reorder: keep; this centralization reduces “optional field gotcha” drift and is a good precursor for later metadata evolution.
- Coherence: good overall; the log cleanup is tangential but still within `sync.ts`.

### Rewrite Commit Message

- Keep as-is: `refactor(sync): centralize outgoing message metadata`

---

## 010 2026-01-17 39cc25154cdd `storage`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=39cc25154cdd patchFile=/tmp/leeroy-wip-pass2/010.patch patchSha256=faab1b0943ff87e82028b877ffe2c0a3d77782d7f90e9e56ab6429a5af839c09 patchBytes=10425 patchLines=186 notesReviewed=yes reviewedAt=2026-01-24T18:03:45+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR28 plannedAt=2026-01-25T12:16:53+0100 manualReviewedForPrPlan=yes notes=Mixed: (a) server-config scoping + (b) per-session model-mode persistence + (c) replaceSettings for convergence; split during rewrite.

- Subject: feat(storage): persist session model modes
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Extends storage scoping to server-config persistence:
  - `sync/serverConfig.ts` now scopes MMKV id `server-config` via `scopedStorageId(...)` when `EXPO_PUBLIC_HAPPY_STORAGE_SCOPE` is set (non-web runtime).
- Adds persistence for per-session `modelMode`:
  - `sync/storage.ts` loads `sessionModelModes` from persistence on startup and merges them into sessions on initial load.
  - When updating a session model mode, collects and saves only non-`default` values (space-efficient).
  - On session delete, removes the session’s persisted model mode entry.
- Adds `replaceSettings(settings, version)`:
  - Unlike `applySettings(...)` (“only apply when version increases”), `replaceSettings` always writes, enabling convergence when switching servers/accounts or after server-side resets (this is used by 011).

### Verdict

- Keep/squash/split/drop/reorder: keep; this is key for “Gemini model mode per session” and for later reliable settings sync.
- Depends on:
  - 006 for `loadSessionModelModes` / `saveSessionModelModes`.
  - `storageScope` helpers from 006.

### Rewrite Commit Message

- Keep as-is: `feat(storage): persist session model modes`

---

## 011 2026-01-17 c2d3507357dc `settings`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c2d3507357dc patchFile=/tmp/leeroy-wip-pass2/011.patch patchSha256=cb3d81d637c10a4b25e538663898c9d1a8dc3427f3d3d01c5a5dca3a4163b6b4 patchBytes=31188 patchLines=675 notesReviewed=yes reviewedAt=2026-01-24T18:06:57+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T12:24:17+0100 manualReviewedForPrPlan=yes notes=Settings core reliability: tolerant parsing + legacy profile normalization + settings sync convergence (relies on split of 010 for replaceSettings).

- Subject: fix(settings): make parsing tolerant for profiles
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Makes settings parsing robust to partial corruption (critical reliability improvement):
  - Replaces whole-object `SettingsSchemaPartial.safeParse(settings)` with per-field parsing so one invalid field doesn’t reset everything to defaults.
  - Special-cases `profiles`: validates each profile independently and keeps only valid ones.
  - Adds a migration helper `normalizeLegacyProfileConfig(...)` to convert legacy provider config objects into `environmentVariables` entries:
    - Migrates `anthropicConfig/openaiConfig/azureOpenAIConfig/togetherAIConfig` fields into `ANTHROPIC_*`, `OPENAI_*`, `AZURE_OPENAI_*`, `TOGETHER_*` env vars.
    - Removes the provider config objects from the stored profile shape (values are preserved via env vars).
  - Also removes profile fields that should not be persisted in the GUI profile shape:
    - Drops `startupBashScript` from the profile schema.
    - Drops `tmuxConfig.updateEnvironment` and stops mapping it into `TMUX_UPDATE_ENVIRONMENT`.
- Updates settings schema/defaults to support new UX features:
  - Adds `useProfiles` and picker-search toggles (`useMachinePickerSearch`, `usePathPickerSearch`) while preserving the legacy `usePickerSearch` knob for backward compatibility migration.
  - Adds `favoriteProfiles` and changes defaults so favorites are empty by default (no more hard-coded `~/src` etc).
- Fixes settings sync convergence under version-mismatch:
  - `sync.ts`: tracks version mismatch info for better errors and uses `storage.getState().replaceSettings(...)` when server version is lower than local (prevents infinite retry loops).
  - Adds an error message hint containing expected/current/pendingKeys when retries are exhausted.
- Coherence: the commit is cohesive around “settings robustness + profile shape migration + sync convergence”.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a high-value reliability fix.
- Depends on:
  - 010 (introduces `replaceSettings` used here).

### Rewrite Commit Message

- Keep as-is: `fix(settings): make parsing tolerant for profiles`

---

## 012 2026-01-17 207ef1b2f572 `dev`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=207ef1b2f572 patchFile=/tmp/leeroy-wip-pass2/012.patch patchSha256=a9b3dfe31a3465be8415ccab235bb3bef26cb918530e86d596b6075891c94423 patchBytes=11240 patchLines=240 notesReviewed=yes reviewedAt=2026-01-24T18:10:19+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:24:17+0100 manualReviewedForPrPlan=yes notes=Dev-only debug-log gating; optionally squash into the feature that introduces the logs (capabilities/voice) during rewrite.

- Subject: fix(dev): gate CLI detection logging
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Introduces a small `debugLog(...)` helper gated by `__DEV__` and uses it to remove noisy console output in production builds:
  - `useCLIDetection.ts`: gates “starting detection / result / parsed status / errors” logs.
  - `RealtimeVoiceSession.tsx` and `.web.tsx`: reduces logs that may contain message data; keeps errors as errors but makes non-essential logs dev-only.
- Coherence: purely logging behavior; message matches.

### Verdict

- Keep/squash/split/drop/reorder: keep; improves production hygiene and reduces potential leakage/noise.
- Depends on: none.

### Rewrite Commit Message

- Suggested scope tweak (optional): `chore(dev): gate CLI detection and realtime debug logs`
- Current is acceptable: `fix(dev): gate CLI detection logging`

---

## 013 2026-01-17 58a892d413d1 `command-palette`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=58a892d413d1 patchFile=/tmp/leeroy-wip-pass2/013.patch patchSha256=1a11a0e1395cd3aa82663418fb8d16080ee1348ac4e44b1c39132dd733b6ee00 patchBytes=1434 patchLines=36 notesReviewed=yes reviewedAt=2026-01-24T18:10:19+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:26:06+0100 manualReviewedForPrPlan=yes notes=UI primitives: command-palette stale-closure fix (memo deps).

- Subject: fix(command-palette): include navigate dependency
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Fixes a stale-closure risk in `CommandPaletteProvider`:
  - Adds `navigateToSession` to the `useMemo` dependency list that builds the commands array.
  - Removes an unnecessary `as any` cast when opening the palette (TypeScript type alignment).
- Coherence: tight; message matches.

### Verdict

- Keep/squash/split/drop/reorder: keep; small but correct and prevents subtle bugs when `navigateToSession` changes.
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `fix(command-palette): include navigate dependency`

---

## 014 2026-01-17 41479b35415f `env`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=41479b35415f patchFile=/tmp/leeroy-wip-pass2/014.patch patchSha256=0a14bef9963c50010d2c6baa8bdc255195f7ac07b50ecece4fcaf9c0fcd9a657 patchBytes=5851 patchLines=141 notesReviewed=yes reviewedAt=2026-01-24T18:10:19+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR07 plannedAt=2026-01-25T12:26:06+0100 manualReviewedForPrPlan=yes notes=Env var templates: parser/formatter + tests (shared primitive).

- Subject: feat(env): add env var template parsing
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Adds explicit parse/format support for env var template strings:
  - New `utils/envVarTemplate.ts` + tests:
    - parses `${VAR}`, `${VAR:-fallback}`, `${VAR:=fallback}`
    - can round-trip format and preserves operator choice.
- Adjusts env substitution semantics in `hooks/envVarUtils.ts`:
  - Updates the matching regex to capture `:-` and `:=` via a unified pattern.
  - Treats empty string as “missing” when a fallback operator is present (bash-like semantics), but preserves empty string for plain `${VAR}` (explicit value).
- Coherence: good and clearly supports later “env preview UI” work.

### Verdict

- Keep/squash/split/drop/reorder: keep; this becomes a shared primitive used by later env preview commits.
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `feat(env): add env var template parsing`

---

## 015 2026-01-17 695bcd09eb9e `env`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=695bcd09eb9e patchFile=/tmp/leeroy-wip-pass2/015.patch patchSha256=2ce8f5113f7e010edb0f7ddce7fa3f1ad2ef7e7f57b8074c9ad0d46707d5d2b5 patchBytes=24232 patchLines=540 notesReviewed=yes reviewedAt=2026-01-24T18:10:19+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR07 plannedAt=2026-01-25T12:26:06+0100 manualReviewedForPrPlan=yes notes=Env preview correctness/safety + preview-env integration (fallback avoids secrets); may fold preview-env backend into PR07 for a true end-to-end feature.

- Subject: fix(env): improve remote env resolution and previews
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Major improvement to environment-variable preview correctness and safety:
  - `useEnvironmentVariables(...)` now prefers a daemon RPC `preview-env` (when supported) via new `machinePreviewEnv(...)` helper:
    - daemon computes “effective spawn env” including optional `extraEnv` expansions (so UI preview matches what sessions will receive)
    - supports secret policy (`EnvPreviewSecretsPolicy: none | redacted | full`) and per-key sensitivity metadata (`PreviewEnvValue`).
  - When daemon doesn’t support `preview-env`, falls back to bash probing **only for non-sensitive keys**:
    - filters out keys by heuristic (`TOKEN|KEY|SECRET|AUTH|PASS|PASSWORD|COOKIE`) and optional `sensitiveKeys`.
    - uses a JSON protocol (`node -e ...`) when `node` is available to preserve newlines and distinguish unset vs empty/null.
    - has a robust fallback line parser with a sentinel `__HAPPY_UNSET__`.
    - intentionally avoids fetching secret-like values into UI memory (marks as hidden).
  - Adds/extends tests around env substitution semantics (empty-string + `:-` fallback).
- Adds `profileId` to the session spawn RPC payload in `sync/ops.ts` (spawn-in-directory now sends `profileId`).
- Coherence: mostly “env preview correctness and safety”; the `profileId` wiring is adjacent/related to env/profile selection.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a reliability + security improvement for env previews and should land before the UI-heavy env preview work (016).
- Depends on:
  - 014 (env template parsing/formatting is used by the preview UI patterns).
  - daemon/CLI supporting `preview-env` for full functionality (commit gracefully degrades when unsupported).

### Rewrite Commit Message

- Keep as-is: `fix(env): improve remote env resolution and previews`

---

## 016 2026-01-17 6cdb90a49aad `env`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=6cdb90a49aad patchFile=/tmp/leeroy-wip-pass2/016.patch patchSha256=c1429ac752ff6a5828610cd00f89c3490559f5ebdde07fa158afececee5239fb patchBytes=90044 patchLines=2144 notesReviewed=yes reviewedAt=2026-01-24T18:10:19+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR07 plannedAt=2026-01-25T12:26:06+0100 manualReviewedForPrPlan=yes notes=Env vars UX overhaul (card/list/preview modal) + tests; optional split for reviewability.

- Subject: feat(env): update env var list, cards, and preview modal
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Large env-variable UX rework with meaningful test coverage:
  - Adds/updates env components:
    - `EnvironmentVariableCard` now:
      - uses `envVarTemplate` parsing/formatting instead of ad-hoc regexes
      - supports per-variable “secret” handling (auto vs user override) and respects daemon-enforced sensitivity
      - supports accurate previews via `machineEnv` metadata + `machineEnvPolicy` (hidden/redacted/unset/full)
      - moves to index-based callbacks (`onUpdate(index, ...)`, etc.) and includes duplication/deletion actions.
    - `EnvironmentVariablesList` refactors list behavior to pass down machine env meta/policy and handle ordering/duplication more deterministically.
    - Adds `EnvironmentVariablesPreviewModal` to preview a computed effective environment (including referenced vars) and surface “machine vs fallback vs missing” detail states.
    - Adds `ProfileCompatibilityIcon` for quick UI indication of which agents a profile supports.
  - Adds extensive tests:
    - `EnvironmentVariableCard.test.ts`
    - `EnvironmentVariablesList.test.ts`
- Coherence: this is a feature-sized UI refactor but stays within the “env vars UX + previews” scope.

### Verdict

- Keep/squash/split/drop/reorder: keep; likely worth splitting during rewrite only if we want more granular reviewability (e.g. “preview modal” separate from “card/list refactor”).
- Depends on:
  - 014 + 015 (template parsing + env preview plumbing).
  - i18n keys under `profiles.environmentVariables.*` existing (later i18n commits should ensure coverage).

### Rewrite Commit Message

- Keep as-is: `feat(env): update env var list, cards, and preview modal`

---

## 017 2026-01-17 65bae55a0b61 `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=65bae55a0b61 patchFile=/tmp/leeroy-wip-pass2/017.patch patchSha256=9734e2764a6330f2d829d9fd3a1e995d14127da3449088ffb2335f5a732ba331 patchBytes=181014 patchLines=3802 notesReviewed=yes reviewedAt=2026-01-24T18:10:19+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR03 plannedAt=2026-01-25T12:26:06+0100 manualReviewedForPrPlan=yes notes=i18n foundation: structural refactor (types from en.ts) + key expansion; split arch vs per-feature keys if feasible, otherwise land early to reduce conflicts.

- Subject: refactor(i18n): separate translation types and content
- Reasons: large change (+1756/-1006)

### Manual Review Notes

- Refactors the i18n “source of truth” and type derivation:
  - Deletes `expo-app/sources/text/_default.ts` (previously contained the canonical English object and the type-level machinery).
  - Adds `expo-app/sources/text/_types.ts` which re-exports `TranslationStructure` from `translations/en.ts` and defines `Translations` from that structure.
  - Updates `expo-app/sources/text/index.ts` to import:
    - `en` from `translations/en.ts`
    - `Translations` / `TranslationStructure` from `_types.ts`
- Removes debug `console.log` statements from language selection in `expo-app/sources/text/index.ts` (preferred language, device locales, Chinese script handling, final language).
- Updates all locale files to import `TranslationStructure` from `../_types` instead of `../_default`:
  - `translations/{ca,en,es,it,ja,pl,pt,ru,zh-Hans}.ts`
- Translation content changes (not purely structural):
  - Adds multiple new keys across locales (notably under `common.*`, plus settings/experiments/profile-related keys). Examples seen in the diff include:
    - `common.add`, `common.actions`
    - `common.discardChanges`, `common.unsavedChangesWarning`, `common.keepEditing`
    - `common.noMatches`, `common.all`, `common.machine`, `common.clearSearch`
    - updated web “enter to send” wording
    - profile / picker-search related strings
    - env var preview modal strings (suggesting this commit is partially supporting later env/profile UX)
- Small style tweak in `expo-app/sources/theme.css` (14-line change in diffstat; review indicated it’s a minor adjustment).

### Verdict

- Keep/squash/split/drop/reorder: keep, but consider splitting by intent during history rewrite:
  - “i18n architecture refactor” (move canonical `en` + derive types) vs
  - “translation content updates” (new keys / wording changes) which may be better grouped with the feature commits that introduce the corresponding UI.
  If splitting is too risky (because every locale must stay in lockstep), keep as one “i18n baseline + key expansion” topic and ensure downstream commits rely on the expanded key set.
- Depends on: must land before any commits that import `TranslationStructure` from `expo-app/sources/text/_types.ts` (instead of `_default.ts`), and before any UI that expects the new translation keys to exist.

### Rewrite Commit Message

- Keep subject: `refactor(i18n): separate translation types and content`
- Suggested body (when squashing): explain that `TranslationStructure` is now derived from `translations/en.ts` and that `_default.ts` was removed; mention that new translation keys were added across all locales.

---

## 018 2026-01-17 2993b5c860ff `new-session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2993b5c860ff patchFile=/tmp/leeroy-wip-pass2/018.patch patchSha256=a3ea133f850c41db35b08b19f9b5fb881ee0ed1fc9af696ad78446643cfcd30d patchBytes=299000 patchLines=5612 notesReviewed=yes reviewedAt=2026-01-24T18:15:13+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T12:26:06+0100 manualReviewedForPrPlan=yes notes=New-session wizard/pickers refactor; must split (components extraction, picker wiring, wizard behavior) and reorder prerequisites.

- Subject: fix(new-session): restore standard modal flow
- Reasons: large change (+3142/-1629)

### Manual Review Notes

- Major refactor of the new-session UX across the wizard + picker screens (scope is broader than the subject suggests):
  - Reworks `expo-app/sources/app/(app)/new/index.tsx` (“NewSessionWizard”) heavily:
    - Removes global callback plumbing for returning selections from picker screens.
    - Persists wizard state before navigating (uses `saveNewSessionDraft(...)`) so going to pick/edit screens doesn’t reset the wizard.
    - Adds route-param based profile selection handling via `consumeProfileIdParam(...)` and clears `profileId` param after consuming it (via `navigation.setParams` / dispatch fallback).
    - Keeps `agentType` compatible with the selected profile (clamps to supported agents; filters Gemini when disabled).
    - Maps permission modes across backends when agent type changes (uses `mapPermissionModeAcrossAgents` instead of bluntly resetting to `default`).
    - Refactors wizard scrolling to section offsets (for AgentInput “jump to section” behavior).
    - Adds env-var preview affordance for the selected profile (computes resolved env vars and opens an “EnvironmentVariablesPreviewModal”).
    - Changes session-creation gating so “profile” is no longer a hard requirement:
      - `canCreate` is based on machine + path only; profile selection becomes optional (default environment).
      - When profiles are enabled, spawn payload includes `profileId` (empty string means “no profile”) and passes profile env vars only when a profile is selected.
    - Improves modal close reliability on web: `handleCloseModal` falls back to `/` when `router.back()` can’t pop history.
  - Introduces reusable picker components:
    - `expo-app/sources/components/newSession/MachineSelector.tsx` (wrapper around `SearchableListSelector`, adds status + favorites + search placement + i18n’d strings).
    - `expo-app/sources/components/newSession/PathSelector.tsx` (large component: recent paths, favorites, optional in-component search header, relative-path formatting, submit behavior, and wizard vs picker UX variants).
  - Updates picker screens to use the new components:
    - `expo-app/sources/app/(app)/new/pick/machine.tsx` now uses `MachineSelector`, supports favorites, and uses `SET_PARAMS` to pass `machineId` back.
    - `expo-app/sources/app/(app)/new/pick/path.tsx` now uses `PathSelector`, adds optional `SearchHeader`, supports favorite directories, and uses “confirm on submit” behavior.
    - Adds a dedicated profile picker screen:
      - `expo-app/sources/app/(app)/new/pick/profile.tsx` sets `profileId` on the previous route via `SET_PARAMS`, supports favorites, and adds per-row actions (edit/duplicate/delete/favorite).
    - Reworks the profile edit screen (`expo-app/sources/app/(app)/new/pick/profile-edit.tsx`) to be picker-friendly and safer:
      - Accepts `profileId` / `cloneFromProfileId` params (plus legacy `profileData`) instead of relying on wizard callbacks.
      - Adds unsaved-changes protection (`beforeRemove` guard + “discard/save/keep editing” prompt; disables iOS modal gesture while dirty).
      - Validates name + duplicate-name conflicts, converts built-ins to custom on save, and passes selection back via `SET_PARAMS` (with `/new` fallback to avoid stacking wizard routes).
    - Adds feature toggles in `expo-app/sources/app/(app)/settings/features.tsx` for `useProfiles`, `useMachinePickerSearch`, `usePathPickerSearch`.
  - Adds utilities for “recent machines/paths”:
    - `expo-app/sources/utils/recentMachines.ts`
    - `expo-app/sources/utils/recentPaths.ts`
  - Adds/updates tests to validate wiring (notably a path picker test ensuring `PathSelector` receives favorite directories + change handler).
- Coherence note: this commit pulls in (and therefore depends on) other abstractions that are introduced in later commits (e.g. profile grouping/actions helpers and row-action primitives). In a cleaned history, those dependencies should be moved earlier or this commit should be split.

### Verdict

- Keep/squash/split/drop/reorder: keep, but strongly recommend splitting during rewrite (this commit is too large + multi-intent):
  - Extract “new picker components” (`MachineSelector`, `PathSelector`, recent utils) as their own commits.
  - Then “wire pick screens” (`/new/pick/machine`, `/new/pick/path`) as a commit.
  - Then “wizard behavior changes” (draft persistence, route-param consumption, permission mapping) as one or more commits.
- Depends on: should be ordered after prerequisite helpers/components it imports:
  - `consumeProfileIdParam` (added in a later commit today)
  - profile grouping/actions helpers (`buildProfileGroups`, `buildProfileActions`, etc.)
  - row-action click-guard helper (`ignoreNextRowPress`) if used in the wizard/profile list
  In the rewritten history, these must precede this commit (or this commit must be split so earlier pieces don’t require them).

### Rewrite Commit Message

- Suggested replacement subject (if kept as one): `refactor(new-session): rework wizard + pickers and persist draft state`
- If split, keep this commit message reserved for only the wizard-level behavioral changes, and give the component extraction commits their own focused subjects.

---

## 019 2026-01-17 9e08047d2478 `profiles`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9e08047d2478 patchFile=/tmp/leeroy-wip-pass2/019.patch patchSha256=fe3ad91efa4fad573b6f1ec9f647ded3034bf8e6b2675b336c8381ad9615120c patchBytes=124997 patchLines=2606 notesReviewed=yes reviewedAt=2026-01-24T18:17:05+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR08 plannedAt=2026-01-25T12:26:06+0100 manualReviewedForPrPlan=yes notes=Profiles core helpers (route params/grouping/actions/mutations/utils) + profiles settings UI wiring; consider split core helpers vs UI.

- Subject: fix(profiles): harden routing, grouping, and editing
- Reasons: large change (+1319/-857)

### Manual Review Notes

- Adds the “profiles plumbing” needed for consistent navigation, grouping, and actions:
  - Adds `expo-app/sources/profileRouteParams.ts` + tests:
    - `normalizeOptionalParam` for expo-router param shape (`string | string[]`)
    - `consumeProfileIdParam` to consume a `profileId` param once and signal when it should be cleared
  - Adds `expo-app/sources/sync/profileGrouping.ts` + tests:
    - `toggleFavoriteProfileId` dedupes favorites and toggles membership, preserving order (new favorites go to the front)
    - `buildProfileGroups` builds:
      - `favoriteProfiles` (custom or built-in)
      - `customProfiles` (non-favorites)
      - `builtInProfiles` (non-favorites)
      - `favoriteIds`/`builtInIds` sets
    - Preserves a special “default environment” favorite marker (`''`) even though it isn’t a real profile object.
  - Adds `expo-app/sources/components/profileActions.ts`:
    - `buildProfileActions(...)` creates an `ItemAction[]` for a profile row (view env vars, favorite toggle, edit, duplicate, delete), keeping “favorite” as the last/inline action.
  - Adds `expo-app/sources/sync/profileMutations.ts`:
    - `createEmptyCustomProfile`, `duplicateProfileForEdit`, `convertBuiltInProfileToCustom`
  - Expands `expo-app/sources/sync/profileUtils.ts` + tests:
    - `getProfilePrimaryCli` with defensive filtering (ignores unknown compatibility keys)
    - `getBuiltInProfileNameKey` mapping built-in ids → translation keys (or null for unknown)
  - UI wiring changes:
    - `expo-app/sources/app/(app)/session/[id]/info.tsx` can display the selected profile (built-in or custom) when `useProfiles` is enabled and the session metadata includes `profileId`.
    - Updates settings navigation paths in `expo-app/sources/components/SettingsView.tsx` to use `/(app)/...` routes.
    - Shows the profiles settings entry when the profiles feature is enabled (`useProfiles`).
    - Large edits to `expo-app/sources/app/(app)/settings/profiles.tsx` and `expo-app/sources/components/ProfileEditForm.tsx` to use the new grouping/actions/mutations patterns.
    - Adds `profileId` to session metadata schema (`expo-app/sources/sync/storageTypes.ts`) and tightens local `permissionMode` / `modelMode` types (removes `null` variants).
- This commit is a functional prerequisite for the new-session wizard work that consumes profile route params and uses profile grouping/actions.

### Verdict

- Keep/squash/split/drop/reorder: keep, but consider splitting into two commits in rewritten history:
  - “profile core helpers” (`profileRouteParams`, `profileGrouping`, `profileMutations`, `profileUtils` improvements + tests)
  - “UI wiring” (settings/profiles screen + ProfileEditForm + SettingsView route corrections)
- Depends on: should land before any code that consumes `consumeProfileIdParam`, `buildProfileGroups`, or `buildProfileActions`.

### Rewrite Commit Message

- Suggested subject: `feat(profiles): add grouping, actions, and route-param helpers`

---

## 020 2026-01-17 8d9f56e85e5a `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8d9f56e85e5a patchFile=/tmp/leeroy-wip-pass2/020.patch patchSha256=eea9bca8aa5358bdcc3b50105b6d6bcebf9819f5c7f4b9c92e66d1dab20ac145 patchBytes=276617 patchLines=6141 notesReviewed=yes reviewedAt=2026-01-24T18:31:18+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:26:06+0100 manualReviewedForPrPlan=yes notes=UI primitives mega-commit: modal/list primitives + AgentInput + row actions + unsaved-changes prompt + cleanup; must split into focused commits within PR02 (some cleanup may move to PR26/PR08).

- Subject: refactor(ui): unify list selectors and modal primitives
- Reasons: too many files (28); safety/security-sensitive area

### Manual Review Notes

- This is a “mega-commit” (UI primitives + large feature cleanup) — the subject only partially describes the scope; plan to split in rewritten history.
- Large refactor of UI primitives (multiple independent concerns bundled):
  - Shared search header:
    - Adds `expo-app/sources/components/SearchHeader.tsx` (themed header search input; clear button; extra web-specific “remove outline/box shadow” styling).
    - Converts `expo-app/sources/app/(app)/settings/voice/language.tsx` to use `SearchHeader` and wraps the screen in `React.memo` (removes inline header styling and `useUnistyles` usage there).
  - List/selectors:
    - Refactors `expo-app/sources/components/SearchableListSelector.tsx`:
      - replaces `MultiTextInput`-based search UI with `SearchHeader`
      - decouples `inputText` (search query) from selection so pickers don’t start pre-filtered when a selection exists
      - adds `searchPlacement` (header / recent / favorites / all) with a “never disappear” fallback when a chosen placement has no visible group
      - adds support for an “All” section title + optional “showAll” section
      - removes controlled/uncontrolled “collapsed sections” logic and the previous “add favorite from typed input” flow
      - changes favorites UX to an inline star toggle (and disallows removing some favorites via `canRemoveFavorite`)
      - filters favorites out of “recent” and “all” buckets to avoid duplicates; adds explicit empty-row rendering and “no matches” behavior
  - Row actions + action menus:
    - Adds `expo-app/sources/components/ItemRowActions.tsx`:
      - renders inline icon actions, and in compact widths moves non-compact actions into an overflow menu (`ItemActionsMenuModal` via `Modal.show`)
      - uses `stopPropagation()` on presses to avoid triggering the row’s onPress
    - Refactors `ItemActionsMenuModal.tsx` and action types to support the new row-action UX.
    - Adds `expo-app/sources/utils/ignoreNextRowPress.ts` (+ test) to guard against click-through / accidental row presses when interacting with row actions.
  - Agent input action chips (large UI refactor; bundled here even though the subject doesn’t mention it):
    - Refactors `expo-app/sources/components/AgentInput.tsx`:
      - adds new props (`onPermissionClick`, `envVarsCount`, `onEnvVarsClick`, `contentPaddingHorizontal`, `panelStyle`) and reorganizes the action bar layout into “chip” pressables
      - normalizes permission mode via `normalizePermissionModeForAgentFlavor(...)` and centralizes display label selection (Codex/Gemini/Claude)
      - uses `getModelOptionsForAgentType(...)` to drive the model overlay options (instead of inline Gemini-only options)
      - changes how profile selection is presented (stable “profile” icon + label, and now shows “No profile” / “Unknown (shortId)” cases)
      - adds machine chip with ellipsis truncation for display-name; removes the previous machine/path “context box” UI
    - Deletes the legacy `expo-app/sources/components/PermissionModeSelector.tsx` (AgentInput now relies on the `@/sync/permissionTypes` types/normalization rather than that UI helper).
  - ItemGroup / list styling:
    - Extracts divider/selection behavior into helpers with tests (`ItemGroup.dividers*`, `ItemGroup.selectableCount*`) and wires `ItemGroup.tsx` through `ItemGroupSelectionContext`.
    - `Item.tsx` now only applies selected-background styling when the group has >1 selectable items (prevents “selected-looking” single-choice rows).
    - Tweaks theme values (e.g. `surfaceSelected` iOS color in `expo-app/sources/theme.ts`).
    - Adds `expo-app/sources/components/Switch.web.tsx` (web implementation of RN `Switch`).
  - Modal primitives:
    - Reworks `expo-app/sources/modal/ModalManager.ts` and modal components (`BaseModal`, `CustomModal`, `WebAlertModal`) + `expo-app/sources/modal/types.ts`.
    - Introduces/adjusts alert semantics to support the new action/menu patterns and web behavior.
      - `BaseModal` stops event propagation on web and sets pointerEvents/layout tweaks so custom modals don’t “click-through” to expo-router overlays.
      - `ModalManager.show` becomes generic-typed over injected `onClose`, and `CustomModal` hardens CommandPalette prop handling.
  - Unsaved changes UX:
    - Adds `expo-app/sources/utils/promptUnsavedChangesAlert.ts` (+ test) which returns a promise resolving to `discard|save|keepEditing` based on which alert button is pressed.
  - Cleanup:
    - Deletes the old monolithic `expo-app/sources/components/NewSessionWizard.tsx` (the wizard now lives under route screens / new composition).
    - Deletes the old `expo-app/sources/sync/profileSync.ts` service (profile sync responsibilities are handled elsewhere after the new routing/profiles refactor).
    - Removes debug `console.log` noise from `expo-app/sources/sync/reducer/phase0-skipping.spec.ts`.
    - Minor docs cleanup: `expo-app/CONTRIBUTING.md` switches examples from `npm run ...` to `yarn ...`.
- Because this touches user interaction primitives (modals, row actions, list selection), it’s appropriately flagged as safety/reliability sensitive; it should be stabilized before stacking feature commits on top.

### Verdict

- Keep/squash/split/drop/reorder: keep, but split in rewritten history (too many unrelated refactors in one commit):
  - “SearchableListSelector + SearchHeader” refactor
  - “AgentInput action chips + permission/model overlay wiring” refactor
  - “Modal primitives / WebAlertModal” refactor
  - “ItemGroup divider + selection helpers” refactor
  - “Row actions + overflow menu + ignoreNextRowPress” addition
  - “promptUnsavedChangesAlert” utility
  - (optional) “delete legacy NewSessionWizard + profileSync + PermissionModeSelector” cleanup
  - (optional) “Switch.web + minor theme/test/docs tweaks” cleanup
- Depends on: should land before any commits that adopt `ItemRowActions`, `SearchHeader`, new modal types, or the updated `SearchableListSelector` API (several later new-session/profile commits appear to depend on these).

### Rewrite Commit Message

- Suggested umbrella subject (if you keep it as one): `refactor(ui): overhaul modal + list primitives`
- Prefer splitting as above so each PR/commit can be reviewed and validated independently.

---

## 021 2026-01-17 2ab560bb0535 `cli-detection`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2ab560bb0535 patchFile=/tmp/leeroy-wip-pass2/021.patch patchSha256=4884006ad132219673c21166156bf5fb14eb96c313c0e1bc39a56c2d564145bf patchBytes=5695 patchLines=144 notesReviewed=yes reviewedAt=2026-01-24T18:32:25+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=Capabilities/CLI detection: add detect-cli RPC path + safe fallback/timeout behavior (may be superseded by capabilities protocol later, but keep as the first reliable detection primitive).

- Subject: feat(cli-detection): add daemon detect-cli RPC support
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Adds a dedicated daemon RPC for CLI availability checks (preferred over shell probing):
  - `expo-app/sources/sync/ops.ts` adds:
    - `machineDetectCli(machineId)` which calls `apiSocket.machineRPC('detect-cli', {})`
    - shape validation for `path` and `clis.{claude,codex,gemini}.{available,resolvedPath?}`
    - graceful downgrade to `{ supported: false }` when daemon responds with `"Method not found"` (older daemons).
- Updates `useCLIDetection` to prefer the RPC and fallback safely:
  - Calls `machineDetectCli` first.
  - Uses a `Promise.race` with a 2s timeout returning `{ supported: false }` to avoid hanging UIs if a daemon never responds to unknown RPCs.
  - If supported, updates availability immediately; otherwise falls back to the existing `machineBash` probing path.
- Coherence: strong; subject matches diff and improves reliability (no shell, better PATH resolution).

### Verdict

- Keep/squash/split/drop/reorder: keep; foundational for later UI that displays detected CLI status per machine.
- Depends on:
  - Daemon implementing `detect-cli` for the preferred path; falls back cleanly for older daemons.

### Rewrite Commit Message

- Keep as-is: `feat(cli-detection): add daemon detect-cli RPC support`

---

## 022 2026-01-17 2d4675a5d051 `agent-input`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2d4675a5d051 patchFile=/tmp/leeroy-wip-pass2/022.patch patchSha256=895c72207bf08128dd67577e593aa5da3a056db3eb634b4754f1183ae41a7dd6 patchBytes=4414 patchLines=82 notesReviewed=yes reviewedAt=2026-01-24T18:32:51+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=AgentInput/UI: compact permission badges; fold required i18n badge keys into PR02 and ensure accessibility labels remain meaningful.

- Subject: fix(agent-input): use compact permission badges
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Changes how the “permission mode” chip renders in `expo-app/sources/components/AgentInput.tsx`:
  - For default permission mode, it now returns an empty label (`''`) and renders icon-only (hides the text label).
  - For non-default modes, it switches to shorter “badge” translation keys:
    - Codex/Gemini: `badgeReadOnly`, `badgeSafeYolo`, `badgeYolo`
    - Claude: `badgeAccept`, `badgePlan`, `badgeYolo` (for bypassPermissions)
  - The chip text element is now conditional: it only renders `<Text>` when the computed label is non-empty.
- Risk/UX note: default mode becoming icon-only can reduce clarity; ensure the Pressable still has a meaningful `accessibilityLabel` / hint and that the icon choice is unambiguous in-context.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be grouped with the i18n commit(s) that add the corresponding `agentInput.*.badge*` translation keys.
- Depends on: translation keys for the new badge labels must exist (and be localized) before this lands in a cleaned history.

### Rewrite Commit Message

- Keep as-is: `fix(agent-input): use compact permission badges`

---

## 023 2026-01-18 d97924f3ef39 `test`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d97924f3ef39 patchFile=/tmp/leeroy-wip-pass2/023.patch patchSha256=264361ca410fc48939c3fe2923b780e887b0def5045593ef123742bb9f34a9d7 patchBytes=829 patchLines=29 notesReviewed=yes reviewedAt=2026-01-24T18:33:10+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=Test/DevX: define __DEV__ in vitest for deterministic dev-guarded code paths.

- Subject: chore(test): define __DEV__ for vitest
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- `expo-app/vitest.config.ts` defines `__DEV__ = false` for the test environment.
- Purpose: code paths gated by `__DEV__` (debug logging, dev-only diagnostics) can run under tests without relying on bundler globals.
- Coherence: fully matches subject; no unrelated changes.

### Verdict

- Keep/squash/split/drop/reorder: keep; small and improves test determinism. Safe to squash into nearby test infra changes if desired.
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `chore(test): define __DEV__ for vitest`

---

## 024 2026-01-18 9c19baa833b9 `settings`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9c19baa833b9 patchFile=/tmp/leeroy-wip-pass2/024.patch patchSha256=bf6dd370043ba1753d4659c1d1c9bf5606f9f5ce5cc35b6a35f26eb660e1beee patchBytes=27820 patchLines=615 notesReviewed=yes reviewedAt=2026-01-24T18:34:24+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=Settings feature: add experiments + saved API keys schema + new-session apiKey route param consumption/persistence (foundation for API keys UI).

- Subject: feat(settings): add api keys and experiment toggles
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Extends settings/profile schema with new capabilities and migrations:
  - Adds per-experiment toggles (`expGemini`, `expUsageReporting`, `expFileViewer`, `expShowThinkingMessages`, `expSessionType`, `expZen`, `expVoiceAuthFlow`) and a migration:
    - if older settings only had `experiments` and no per-experiment keys, defaults all per-experiment toggles to match the master switch (preserves existing behavior).
  - Adds agent-input customization settings:
    - `agentInputActionBarLayout: auto|wrap|scroll|collapsed`
    - `agentInputChipDensity: auto|labels|icons`
  - Introduces saved API keys in encrypted settings:
    - `SavedApiKeySchema` (`id`, `name`, `value` secret, `createdAt/updatedAt`)
    - `Settings.apiKeys: SavedApiKey[]`
    - `Settings.defaultApiKeyByProfileId: Record<profileId, apiKeyId>`
- Tightens profile validation constraints (reliability/safety around auth requirements):
  - Adds Zod issues enforcing v1 constraints like “at most one required secret env var” and restricting required secret vars under `authMode=machineLogin`.
  - Ensures `requiresMachineLogin` only applies when `authMode=machineLogin`.
- Adds route-param + persistence wiring for new-session API key selection:
  - `consumeApiKeyIdParam(...)` alongside the existing profile param consumption helper.
  - Adds `selectedApiKeyId` to `NewSessionDraft` persistence.
- Adds extensive tests in `settings.spec.ts` covering:
  - per-experiment migration behavior
  - apiKeys parsing (keep valid, drop invalid)
  - defaults including new settings fields.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is the foundational data-model work for the subsequent API keys UI and per-experiment UI.
- Coherence: good; settings/schema, persistence, and param helpers all support the subject.
- Depends on:
  - Later UI commits (025/030/031) should come after this to avoid referencing missing settings keys.

### Rewrite Commit Message

- Keep as-is: `feat(settings): add api keys and experiment toggles`

---

## 025 2026-01-18 21cc6df79f50 `api-keys`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=21cc6df79f50 patchFile=/tmp/leeroy-wip-pass2/025.patch patchSha256=e9f31e31a441468fdbad1a80e23b0e87c43b1d6398f73c6bdacceebfe0820a1f patchBytes=30187 patchLines=686 notesReviewed=yes reviewedAt=2026-01-24T18:35:32+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=API keys UI + pick flow; split out unrelated SettingsView/machine CLI glyph wiring into PR14 (capabilities/machines) during rewrite.

- Subject: feat(api-keys): add saved API keys UI
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Adds user-facing screens + components to manage saved API keys:
  - New screens:
    - `/(app)/settings/api-keys.tsx` (manage keys from settings)
    - `/(app)/new/pick/api-key.tsx` (picker flow for selecting a key id via route params)
  - New components:
    - `ApiKeyAddModal`: secure entry for `{ name, value }` with `secureTextEntry` (never re-displays entered value after save).
    - `apiKeys/ApiKeysList`: renders “none” row (optional), empty state, list rows, add flow, and row actions:
      - rename key
      - replace value (secure prompt)
      - delete key (confirmation)
      - optional “default” action via `defaultId` + `onSetDefaultId` props (list-level, not necessarily per-profile yet).
  - Wires `SettingsView` to link to the new API keys settings screen and adds a subtitle.
- Also includes additional `SettingsView` improvements that are adjacent but not strictly “API keys UI”:
  - Adds a “refresh machines” action (button + spinner) and auto-refreshes machine data on focus (`useFocusEffect` + `sync.refreshMachinesThrottled(...)`).
  - Enhances machine rows to show detected CLI status glyphs (`MachineCliGlyphs`) alongside online/offline info.
  - Gates the usage screen behind `experiments && expUsageReporting` and updates the “Claude Code” account title to a translated string.
- Coherence: UI work matches “saved API keys UI”; it builds on the `Settings.apiKeys` model introduced in 024.

### Verdict

- Keep/squash/split/drop/reorder: keep; can be split during rewrite into “UI primitives (ApiKeysList/Modal)” vs “screen wiring” if desired.
- Depends on:
  - 024 for the `apiKeys` settings field and schema.
  - i18n keys under `apiKeys.*` and `settings.apiKeys*` existing (added elsewhere in the history).

### Rewrite Commit Message

- Keep as-is: `feat(api-keys): add saved API keys UI`

---

## 026 2026-01-18 ce9cd74d408a `machine`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ce9cd74d408a patchFile=/tmp/leeroy-wip-pass2/026.patch patchSha256=9eb86cc637175c0ef4d31c6afa1cf75fd4466f1102c09b699aa4575dc94609a8 patchBytes=56472 patchLines=1341 notesReviewed=yes reviewedAt=2026-01-24T18:37:32+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=Machines/capabilities UX: detected CLI status surfaces + cache/timeout + modal/list UI; fold i18n for these strings into PR14.

- Subject: feat(machine): surface detected CLI status
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Surfaces daemon “detected CLI” status in the UI and makes it reliable at scale:
  - Adds a cache layer around `detect-cli`:
    - `hooks/useMachineDetectCliCache.ts` implements a per-machine TTL cache with:
      - stale-while-revalidate loading state (keeps last response while refreshing)
      - in-flight de-duping
      - a defensive timeout to avoid hanging on older daemons that never respond to unknown RPCs
      - prefetch helpers (`prefetchMachineDetectCli*`) for wizard flows.
  - Extends `machineDetectCli` RPC plumbing in `sync/ops.ts`:
    - supports optional `includeLoginStatus`
    - enriches `DetectCliEntry` with `version` and `isLoggedIn`
    - returns `{ supported: false, reason: 'not-supported'|'error' }` so UI can differentiate “update daemon” vs transient failure.
  - UI components:
    - Adds `components/machine/DetectedClisList` and `DetectedClisModal` for inline/stacked rendering + manual refresh.
    - Adds glyph helpers (`MachineCliGlyphs`) and wires machine pickers/lists to show icons/summary.
    - Updates machine detail screen to show a “Detected CLIs” section with refresh affordance; refresh path also calls `sync.refreshMachines()` first to avoid misclassifying transient socket/encryption readiness issues.
  - Also localizes the Stop Daemon confirm flow in machine detail screen (moves literals to `t(...)` keys).
- Coherence: mostly consistent “machine detected CLI status”; there is some adjacent UI cleanup/localization but still within machine UX.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a feature-sized change but brings strong reliability patterns (cache + timeout + explicit refresh).
- Depends on:
  - 021 (initial `machineDetectCli` support) and daemon support for richer fields (`version`, `isLoggedIn`) for full UX.
  - i18n keys for the newly localized machine strings.

### Rewrite Commit Message

- Keep as-is: `feat(machine): surface detected CLI status`

---

## 027 2026-01-18 7899bbd8433e `profiles`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7899bbd8433e patchFile=/tmp/leeroy-wip-pass2/027.patch patchSha256=1d66ea8afae399b1edfb23f0f1eba7203a6aed9c625784e90764da0c29ddaeab patchBytes=95110 patchLines=1988 notesReviewed=yes reviewedAt=2026-01-24T18:40:29+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR08 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=Profiles requirements: preflight required env vars (preview-env) + resolver modal + badges; split (metadata/schema vs hook vs modal/list wiring) and fold missing i18n.

- Subject: feat(profiles): add API key requirements flow
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Adds a “requirements” system for profiles that need API keys / machine auth, with careful secret-handling:
  - Extends built-in profile definitions in `expo-app/sources/sync/profileUtils.ts` to declare auth requirements:
    - Adds `authMode` + `requiredEnvVars` for API-key-based providers (e.g. `OPENAI_API_KEY`, `DEEPSEEK_AUTH_TOKEN`, `Z_AI_AUTH_TOKEN`, Azure key + endpoint).
    - Adds `requiresMachineLogin` for the Anthropic/Claude Code login flow.
    - Updates built-in profile documentation to explain the login flow and optional API-key env override.
  - Adds `expo-app/sources/hooks/useProfileEnvRequirements.ts`:
    - Preflights required env vars on a machine using the daemon `preview-env` flow (via `useEnvironmentVariables`).
    - Passes `extraEnv = getProfileEnvironmentVariables(profile)` so the preview matches spawn-time expansion.
    - Marks secret keys as `sensitiveKeys` so the UI never falls back to probing and accidentally loading secret values into memory.
    - Returns only presence/display metadata (`isSet`, `display`) and an `isReady` boolean; does not expose raw secret values.
  - Adds `expo-app/sources/sync/profileSecrets.ts` helpers to locate “the required secret env var name” for a profile (used by UI prompts).
  - Adds `expo-app/sources/components/ApiKeyRequirementModal.tsx`:
    - Presents options to satisfy a profile’s required secret:
      - use machine environment (if `preview-env` indicates required keys are set)
      - select a saved API key
      - enter a session-only secret once (secure text entry)
    - Remembers last selection per `profileId:machineId` in an in-memory map.
    - Returns a structured result (`useMachine` / `selectSaved` / `enterOnce`) to the caller; the caller decides how to apply it.
  - Wires badges + UX around requirements:
    - Adds `ProfileRequirementsBadge.tsx` and `useProfileEnvRequirements` integration in profile list UIs.
    - Updates `ProfilesList` / `profileListModel` to show requirement state and open the modal.
    - Updates profiles settings screen to use `ProfilesList` and pass `onApiKeyBadgePress`.
  - Adds profile-side authoring UI for requirements:
    - Introduces a reusable tile selector (`OptionTiles.tsx`) and uses it in `ProfileEditForm.tsx` to edit `authMode` (`none|apiKeyEnv|machineLogin`).
    - For `authMode=apiKeyEnv`, allows editing the required secret env-var name (uppercased, validated against `^[A-Z_][A-Z0-9_]*$`).
    - For `authMode=machineLogin`, integrates `useCLIDetection(..., { includeLoginStatus: true })` so the UI can display machine login state per backend when a preview machine is selected.
      - Note: the current “Logged in / Not logged in” text is hardcoded (not `t(...)`) and should be localized in a clean history rewrite.
- Security note: the design intentionally avoids fetching or displaying secret values when checking machine readiness; the only time a raw value enters UI memory is the “enter once” path, which uses `secureTextEntry` and returns the value to the caller.

### Verdict

- Keep/squash/split/drop/reorder: keep; recommend splitting in rewritten history into:
  - “profile metadata schema + built-in requirements” (profileUtils changes)
  - “env requirement preflight hook” (`useProfileEnvRequirements` + `profileSecrets`)
  - “API key requirement modal + badges + list wiring”
- Depends on: requires daemon support for `preview-env` (and the UI’s `useEnvironmentVariables` hook implementation) to be present in the rewritten history before the requirement UI is meaningful.

### Rewrite Commit Message

- Suggested subject: `feat(profiles): add api key requirement preflight + resolver modal`

---

## 028 2026-01-18 0023ba1ea5aa `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=0023ba1ea5aa patchFile=/tmp/leeroy-wip-pass2/028.patch patchSha256=81df78affd393e71a4019a9053bb8432ff7bc0689c208e6e45b9d291f6fbdcc3 patchBytes=245409 patchLines=4739 notesReviewed=yes reviewedAt=2026-01-24T18:44:46+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR03 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=i18n batch: huge locale-key expansion + tooling (compare script + literal scanner). During rewrite, split tooling to PR01 and distribute keys into the owning feature PRs where feasible.

- Subject: refactor(i18n): update translations and tooling
- Reasons: large change (+2913/-143)

### Manual Review Notes

- Expands translation coverage and improves i18n tooling:
  - Updates `expo-app/sources/text/translations/*` across many locales with a large batch of keys (net +2913/-143).
  - Updates `expo-app/sources/scripts/compareTranslations.ts`:
    - includes `it` + `ja` in the translation set and language name map
    - fixes missing newline at EOF
  - Adds `expo-app/sources/scripts/findUntranslatedLiterals.ts` (AST-based, `tsx` shebang):
    - Walks `expo-app/sources/**` and parses files via TypeScript compiler API.
    - Reports likely user-facing string literals in:
      - JSX text nodes (`<Text>Some string</Text>`)
      - selected JSX attributes (e.g. `title`, `description`, `placeholder`, etc.)
      - string args to likely UI calls (heuristic: `*.alert|confirm|prompt`, `Toast*`, `*Modal*`), excluding `t(...)` calls
    - Uses ignore heuristics to suppress likely non-user-facing strings (URLs, env keys, ids/paths, hex colors, numeric strings, punctuation-only).
    - Ignores translation files and scripts as scan targets to keep noise down; groups results by kind + literal and prints occurrences with context.
- Translation content notes (from full diff):
  - Adds UI copy for a lot of new surface area: machine refresh/rename/stop-daemon flows, session-type + profile selection flows, CLI-not-detected banners, action menu labels, voice assistant states, Zen screens, artifacts errors, API keys UI, and profile requirements/machine-login help text.
  - Many locales still leave some newly introduced experimental feature subtitles in English (e.g. `expGeminiSubtitle`, `expUsageReporting*`, etc.). This commit improves coverage overall, but does not fully localize those strings.
- This commit is large but remains “content + dev tooling”; runtime behavior changes are limited to updated strings and additional translation keys.

### Verdict

- Keep/squash/split/drop/reorder: keep; consider splitting into:
  - `chore(i18n): add untranslated literal scanner + expand compare script`
  - `chore(i18n): update locale dictionaries for new UI keys`
- Depends on: should be ordered near the feature commits that introduce the corresponding keys (to avoid intermediate commits failing type/coverage expectations when keys are missing in non-English locales).

### Rewrite Commit Message

- Suggested subject: `chore(i18n): update translations and add literal scanner`

---

## 029 2026-01-18 12e75a3b36fa `autocomplete`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=12e75a3b36fa patchFile=/tmp/leeroy-wip-pass2/029.patch patchSha256=57c5e6fd7a385743a42b43fa45b1e5f0c44b6838324c98a2ce1bdba1b065b90b patchBytes=1469 patchLines=29 notesReviewed=yes reviewedAt=2026-01-24T18:45:28+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=UI polish: remove debug suggestion logs (autocomplete).

- Subject: fix(autocomplete): remove debug suggestion logs
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Removes verbose debug `console.log(...)` calls from `components/autocomplete/useActiveSuggestions.ts`:
  - previously logged every query and serialized suggestions (including function placeholders)
  - now no longer logs in production/dev by default.
- Coherence: good; subject matches diff and reduces noise (and potential leakage of suggestion content).

### Verdict

- Keep/squash/split/drop/reorder: keep; small cleanup.
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `fix(autocomplete): remove debug suggestion logs`

---

## 030 2026-01-18 679877c26ea9 `settings`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=679877c26ea9 patchFile=/tmp/leeroy-wip-pass2/030.patch patchSha256=66c3233690e649ede180f88795c4c1bf4f37c94fc7002111fe2d562f01e2cfd1 patchBytes=7337 patchLines=136 notesReviewed=yes reviewedAt=2026-01-24T18:46:01+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T12:28:26+0100 manualReviewedForPrPlan=yes notes=Settings feature: expose per-experiment toggles (likely squash with 024 in rewritten history).

- Subject: feat(settings): expose per-experiment toggles
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Wires the per-experiment toggles introduced in 024 into the settings UI:
  - `settings/features.tsx` now reads/writes:
    - `expGemini`, `expUsageReporting`, `expFileViewer`, `expShowThinkingMessages`, `expSessionType`, `expZen`, `expVoiceAuthFlow`.
  - Adds `setAllExperimentToggles(enabled)` and updates the master `experiments` toggle to also set all per-experiment toggles by default (keeps the “single switch” UX while enabling fine-grained control).
  - Shows the per-experiment toggle group only when `experiments === true`.
- Coherence: matches subject; this is the UI exposure of the schema introduced in 024.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be adjacent to 024 in rewritten history (schema + UI).
- Depends on:
  - 024 (adds the per-experiment keys).
  - i18n keys `settingsFeatures.exp*` existing.

### Rewrite Commit Message

- Keep as-is: `feat(settings): expose per-experiment toggles`

---

## 031 2026-01-18 4dc9c7f8759c `agent-input`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4dc9c7f8759c patchFile=/tmp/leeroy-wip-pass2/031.patch patchSha256=c738387880e6ad1b5393546c91d12cfe32ba1a05595053edbe8949de232e8e11 patchBytes=51158 patchLines=831 notesReviewed=yes reviewedAt=2026-01-24T18:47:29+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=AgentInput UI feature: configurable action bar layout + chip density; keep adjacent to other AgentInput UX refactors (depends on settings keys from PR06).

- Subject: feat(agent-input): add configurable action bar layout
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Adds user-configurable “action bar layout” and “chip density” controls and wires them into the AgentInput layout:
  - `settings/appearance.tsx`:
    - wraps screen in `React.memo(...)` (adds missing `React` import) and fixes missing newline at EOF.
    - adds UI rows that cycle through `agentInputActionBarLayout` (`auto|wrap|scroll|collapsed`) and `agentInputChipDensity` (`auto|labels|icons`).
  - `components/AgentInput.tsx`:
    - reads the new settings and derives effective layout based on screen width when `auto`.
    - adds support for “collapsed” layout mode (moves shortcuts into a compact row).
    - for “scroll” layout:
      - always renders a horizontal `ScrollView` (even if not scrollable) so it can measure viewport/content widths (avoid “measurement deadlock”)
      - enables/disables scrolling based on whether the chips overflow
      - adds left/right gradient fades based on scroll position using `expo-linear-gradient` and `color` (computes a transparent version of the input background color)
    - adjusts rendering based on chip density to reduce width pressure.
    - collapses action shortcuts into the settings overlay when `actionBarLayout=collapsed`, including:
      - profile/env/agent/machine/path shortcuts
      - file viewer (`agentInput.actionMenu.files`) and abort (`agentInput.actionMenu.stop`)
    - removes the `connectionStatus.cliStatus` UI “CLI: claude✓ …” line (and removes `cliStatus` from the `connectionStatus` prop type).
- Coherence: fits the subject (action bar layout + chip density).

### Verdict

- Keep/squash/split/drop/reorder: keep; should follow 024 (settings keys) and be placed near other AgentInput UX commits.
- Depends on:
  - 024 (`agentInputActionBarLayout` / `agentInputChipDensity` settings added).
  - i18n keys under `settingsAppearance.agentInput*` and their option labels.

### Rewrite Commit Message

- Keep as-is: `feat(agent-input): add configurable action bar layout`

---

## 032 2026-01-18 e8e42a6087ad `modal`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e8e42a6087ad patchFile=/tmp/leeroy-wip-pass2/032.patch patchSha256=5e703a80cc8f7735a4b497cdcf15c3bcfb494780b2384d8980eb5c6133b8f00b patchBytes=10216 patchLines=231 notesReviewed=yes reviewedAt=2026-01-24T18:48:18+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=Modal/overlay reliability: iOS stacked-modal touch-blocking fix; keep early in PR02 before other modal/picker flows.

- Subject: fix(modal): prevent stacked modal touch-blocking on iOS
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Fixes a real iOS interaction bug caused by stacking “navigation modals” (expo-router) with RN `<Modal>` overlays:
  - `app/(app)/_layout.tsx`:
    - uses `presentation: 'containedModal'` on iOS for the New Session screen to avoid RN Modal rendering behind the navigation modal while still capturing touches.
    - adds an explicit close button for iOS (since swipe-to-dismiss is not consistently available with `containedModal`).
  - `modal/components/BaseModal.tsx`:
    - on native (non-web), avoids RN `<Modal>` entirely and renders a “portal-style” absolute overlay with a high zIndex/elevation (prevents the “invisible touch shield” effect).
    - keeps RN `<Modal>` on web where escaping expo-router body pointer-events behavior is needed.
  - Adds `closeOnBackdrop` plumbing:
    - `ModalManager.show(...)` accepts `closeOnBackdrop`
    - `CustomModalConfig` documents it
    - `CustomModal` passes it to `BaseModal` and invokes an optional `onRequestClose` callback for cleanup before closing (safe dynamic check).
- Minor cleanup note (for rewritten history): `BaseModal` uses `behavior={Platform.OS === 'web' ? undefined : ((Platform as any).OS === 'ios' ? 'padding' : 'height')}` which could likely be simplified to avoid the `(Platform as any)` escape hatch.
- Coherence: strong; the subject matches and the changes are directly tied to stacked modal reliability.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a high-value UX/reliability fix and should land before any later UI that relies on stacked modals.
- Depends on: none (internal UI primitives).

### Rewrite Commit Message

- Keep as-is: `fix(modal): prevent stacked modal touch-blocking on iOS`

---

## 033 2026-01-18 daa0b4527d7f `new-session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=daa0b4527d7f patchFile=/tmp/leeroy-wip-pass2/033.patch patchSha256=df9457db150e945395c07246fa6b0e64771d9fbdd764092e42dbaaeefb97fb36 patchBytes=187624 patchLines=3256 notesReviewed=yes reviewedAt=2026-01-24T18:52:09+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=New-session wizard: extracted NewSessionWizard + API key selection/preflight/route plumbing; keep after profiles requirements + preview-env semantics (PR08/PR07) and settings keys (PR06).

- Subject: feat(new-session): add api key selection and wizard extraction
- Reasons: large change (+1719/-1116)

### Manual Review Notes

- Introduces API-key selection into the new-session flow and extracts the wizard UI:
  - Adds `expo-app/sources/app/(app)/new/NewSessionWizard.tsx`:
    - Large extracted component with explicit prop groups (`layout`, `profiles`, `agent`, `machine`, `footer`) to separate rendering from orchestration/state in `new/index.tsx`.
    - Adds props for API key resolution state (`sessionOnlyApiKeyValue`, `selectedSavedApiKeyValue`, `apiKeyPreflightIsReady`) and a callback to open the requirement modal.
    - Adds “missing CLI” banners (claude/codex/gemini), model selection, machine/path/permission selection, and optional session-type section; keeps `AgentInput` sticky with a top fade divider gradient.
  - Updates `expo-app/sources/app/(app)/new/index.tsx` to:
    - Own API key selection state (session-only vs saved key) and drive the wizard via props.
    - Provide `openApiKeyRequirementModal(...)` and manage “auto-prompt suppression” to avoid repeatedly nagging.
    - Adds persisted `selectedApiKeyId` to the draft and introduces `apiKeys` + `defaultApiKeyByProfileId` settings usage; uses `selectedApiKeyId === ''` as an explicit “do not auto-apply default key” sentinel when the user chooses machine env / session-only.
    - Adds spawn-time secret overlay injection: for `authMode=apiKeyEnv` profiles, if a saved key or session-only key is selected, injects it into `environmentVariables[requiredSecretEnvVarName]` before creating the session; errors if neither injected key nor machine env satisfies requirements.
    - Improves iOS responsiveness:
      - defers synchronous MMKV draft writes via `InteractionManager.runAfterInteractions` (and longer debounce delay on native)
      - refreshes machines on focus (throttled) after interactions
      - switches `useCLIDetection(selectedMachineId, { autoDetect: false })` and uses cache-prefetch helpers to avoid network work on tap handlers
    - Removes `connectionStatus.cliStatus` usage (aligns with AgentInput removing that UI in commit 031).
  - Upgrades the profile picker `expo-app/sources/app/(app)/new/pick/profile.tsx` to support API-key gating:
    - Adds route-param plumbing to return `{ profileId, apiKeyId?, apiKeySessionOnlyId? }` to the previous screen.
    - For `authMode === 'apiKeyEnv'` profiles, it preflights the required secret on the selected machine using `machinePreviewEnv(...)` with `extraEnv` and `sensitiveKeys`.
    - If the machine lacks the key and no default saved key exists, it opens `ApiKeyRequirementModal`.
    - If a default saved key exists, it auto-applies it (still overrideable later).
    - For session-only keys, it stores the entered secret in temp storage and passes back a temp id.
    - Refactors profile picker UI to reuse `ProfilesList` (instead of re-implementing favorites/custom/built-in grouping and row actions inline).
  - Adjusts `expo-app/sources/components/newSession/ProfileCompatibilityIcon.tsx` so Gemini glyphs depend on `experiments && expGemini` (not just the master experiments toggle).
- i18n/reliability note (for rewritten history): some user-facing strings in the extracted wizard remain hardcoded English (e.g. `MachineSelector` search placeholder `"Search machines..."`, permission-mode descriptions like `"Read-only mode"`, and the missing API key alert text). These should be moved to `t(...)` keys in the final cleaned history.
- Coherence note: this commit logically stacks on top of the “profile requirements” work (required env var metadata, preview-env support, and ApiKeyRequirementModal).

### Verdict

- Keep/squash/split/drop/reorder: keep; in rewritten history, keep it adjacent to (and after) the commits that introduce:
  - `requiredEnvVars` / `authMode` metadata on profiles
  - `ApiKeyRequirementModal`
  - daemon `preview-env` / `machinePreviewEnv` support
- Depends on: requires `machinePreviewEnv` and secret-safe preview semantics; also requires `storeTempData` behavior to ensure session-only secrets don’t persist beyond their intended lifetime.

### Rewrite Commit Message

- Keep subject: `feat(new-session): add api key selection and wizard extraction`

---

## 034 2026-01-18 94814465bf81 `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=94814465bf81 patchFile=/tmp/leeroy-wip-pass2/034.patch patchSha256=09e85cfc752e6ba9790b2a35f926bc0242b79eeba4b258d26079e985caece0a7 patchBytes=5326 patchLines=136 notesReviewed=yes reviewedAt=2026-01-24T18:53:41+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR27 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=Sync robustness/perf: debounce pending settings writes + safe flush-on-background + refreshMachinesThrottled de-dupe.

- Subject: perf(sync): debounce pending settings writes
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Improves UI responsiveness and crash-safety around settings updates:
  - Debounces synchronous persistence of pending settings (MMKV/JSON) and the subsequent server sync trigger:
    - introduces `pendingSettingsFlushTimer` + `pendingSettingsDirty`
    - schedules flush after ~900ms
    - on native, runs flush via `InteractionManager.runAfterInteractions` to avoid stalling taps/animations on iOS.
  - Ensures pending settings are persisted immediately on app backgrounding (reduces “lost last-second toggle” risk).
- Adds a throttled machines refresh helper:
  - `refreshMachinesThrottled({ staleMs, force })`:
    - de-dupes concurrent refresh calls via `machinesRefreshInFlight`
    - avoids repeated network churn when multiple screens request refresh in quick succession.
- Coherence: slightly broader than “pending settings writes” (also adds machine refresh throttling), but both are “sync performance/reliability” concerns.

### Verdict

- Keep/squash/split/drop/reorder: keep; performance + reliability win, likely safe to keep as one “sync perf” commit.
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `perf(sync): debounce pending settings writes`

---

## 035 2026-01-18 7ffd0c3428cb `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7ffd0c3428cb patchFile=/tmp/leeroy-wip-pass2/035.patch patchSha256=80e3faea4b996f40de251ffcb9a01d834711f4f7cd547a039bfff2017d11f59c patchBytes=1771 patchLines=39 notesReviewed=yes reviewedAt=2026-01-24T18:54:29+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=UI/i18n hygiene for alerts: localize default error alert title/button/unknown fallback; fold required keys into PR02 (don’t keep in a separate i18n PR).

- Subject: fix(ui): localize error alerts
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- `useHappyAction` now localizes the default alert title/button and uses a translated unknown-error fallback:
  - `'Error'` → `t('common.error')`
  - `'OK'` → `t('common.ok')`
  - `'Unknown error'` → `t('errors.unknownError')`
- Coherence: matches subject; this is a small i18n hardening.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be adjacent to i18n commits ensuring `errors.unknownError` exists across locales.
- Depends on: translation keys `common.error`, `common.ok`, `errors.unknownError` existing.

### Rewrite Commit Message

- Keep as-is: `fix(ui): localize error alerts`

---

## 036 2026-01-18 97ca69e0a735 `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=97ca69e0a735 patchFile=/tmp/leeroy-wip-pass2/036.patch patchSha256=d4e60307db035e91b99d59b7b132811586a8129b313005bf226fb69356c692b8 patchBytes=26606 patchLines=559 notesReviewed=yes reviewedAt=2026-01-24T18:55:47+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR03 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=Mixed i18n sweep + behavior: most changes are translation-key wiring; BUT includes expShowThinkingMessages gating in MessageView (move that behavior into PR06). Split during rewrite.

- Subject: refactor(i18n): replace remaining UI literals
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Replaces remaining hard-coded UI strings with translation keys across multiple screens/components (19 files; small diff):
  - General UI empty states / connect flows / command palette / message view / voice status bar / markdown renderers / usage chart.
  - Notable safety-adjacent change: `expo-app/sources/utils/microphonePermissions.ts` now uses `t(...)` for permission-denied alerts (titles, body, and button labels) including web-specific instructions.
- Coherence note: despite the i18n-only subject, this commit also changes behavior in `expo-app/sources/components/MessageView.tsx`:
  - thinking messages are now hidden unless `experiments && expShowThinkingMessages` (previously gated only by `experiments`).
  - In a rewritten history, this should likely be split so the per-experiment gating change lands with the rest of the experiments-toggle wiring (e.g. commits 024/030), not inside an i18n sweep.
- No functional behavior change intended beyond string sources; correctness depends on translation keys existing and being accurate.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is good “i18n hygiene” and reduces future untranslated regressions. Consider grouping it near the translation-dictionary updates that add the required keys.
- Depends on: requires the corresponding translation keys (e.g. `modals.microphoneAccessRequired*`, `modals.openSettings`, etc.) to exist in all locales.

### Rewrite Commit Message

- Keep subject: `refactor(i18n): replace remaining UI literals`

---

## 037 2026-01-18 5f5b57df5181 `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=5f5b57df5181 patchFile=/tmp/leeroy-wip-pass2/037.patch patchSha256=61cb1063e70f82b42af8fb8dfedb96e14fb3153f000de76a2ccbd9dabbc5c326 patchBytes=6661 patchLines=145 notesReviewed=yes reviewedAt=2026-01-24T18:57:14+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=UI primitives polish: Item subtitle node support + action-menu timing via InteractionManager + selectable count perf knob; keep near other item/menu refactors in PR02.

- Subject: refactor(ui): improve item rendering and action menu timing
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Improves UI primitives used broadly across settings/lists:
  - `Item.tsx`:
    - broadens `subtitle` type from `string` to `React.ReactNode` and renders non-string subtitles directly (enables richer layouts like inline glyphs).
    - copy-to-clipboard logic now only uses `subtitle` when it’s a string (avoids copying `[object Object]`).
  - `ItemActionsMenuModal.tsx`:
    - replaces a `setTimeout(0)` “close then act” pattern with `InteractionManager.runAfterInteractions(...)` to improve iOS reliability for navigation/actions after closing overlay modals.
  - `ItemGroup.tsx`:
    - adds optional `selectableItemCountOverride` to avoid walking React children for selectable-count detection on every render (perf knob for known lists).
  - `ItemRowActions.tsx` tweaks the compact threshold from 420 → 450.
  - `SessionsList.tsx` removes an unused `experiments` setting read.
- Coherence: mostly consistent “UI primitive polish + reliability”.

### Verdict

- Keep/squash/split/drop/reorder: keep; good incremental UX/perf improvement. Place near other modal/list primitive refactors (e.g. 020, 032).
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `refactor(ui): improve item rendering and action menu timing`

---

## 038 2026-01-18 7976b877259c `experiments`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7976b877259c patchFile=/tmp/leeroy-wip-pass2/038.patch patchSha256=81f96664a2da5fe1b098d2d3765dea96084948a68ec8bbd952ae7cd9b553a03a patchBytes=12038 patchLines=243 notesReviewed=yes reviewedAt=2026-01-24T19:02:45+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=Experiments feature: gate Zen/file-viewer/voice-auth behind per-experiment toggles; split Zen i18n string replacements into the appropriate feature PR/i18n distribution.

- Subject: feat(experiments): gate Zen, file viewer, and voice auth flow
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `7 files changed, 29 insertions(+), 21 deletions(-)`
- Changes:
  - Gates the session file viewer behind a dedicated experiment flag:
    - `SessionView.tsx`: `onFileViewerPress` now requires `experiments && expFileViewer` (previously only `experiments`).
  - Gates the Zen sidebar entry behind a dedicated experiment flag:
    - `SidebarView.tsx`: replaces `useSettings()` with `useSetting('experiments')` + `useSetting('expZen')`, computes `showZen = experimentsEnabled && expZen`, and uses `showZen` for both render gating and the left-justify heuristic.
  - Gates the “voice auth flow” behind a dedicated experiment flag:
    - `RealtimeSession.ts`: introduces `expVoiceAuthFlow` and changes the “no-auth” path from `!experimentsEnabled` to `!(experimentsEnabled && expVoiceAuthFlow)`.
  - Translates Zen UI literals (i18n hygiene, but mixed into this commit):
    - `ZenAdd.tsx`: placeholder → `t('zen.add.placeholder')`
    - `ZenHome.tsx`: empty state → `t('zen.home.noTasksYet')`
    - `ZenView.tsx`: button labels + helper text + “Linked Sessions” → `t('zen.view.*')`
    - `ZenHeader.tsx`: title “Zen” → `t('zen.title')`
- Coherence:
  - This commit mixes two concerns:
    1) experiments gating / safety behavior changes (feature visibility + voice session auth behavior)
    2) i18n string replacement for Zen screens
  - For history rewrite, I’d split the i18n portion into the broader i18n sweep (e.g. near `refactor(i18n): replace remaining UI literals`) and keep the gating changes grouped with the settings/experiments work.
- Risk notes:
  - `RealtimeSession.ts` comment now reads “No experiments = no auth needed” but the actual condition is “experiments enabled AND voice-auth-flow flag enabled”; consider updating the comment during rewrite.
  - This commit assumes `expFileViewer`, `expZen`, and `expVoiceAuthFlow` exist in the settings schema / storage and have sane defaults.

### Verdict

- Keep/squash/split/drop/reorder: keep, but split during rewrite (gating vs i18n). The gating portion is a good safety/reliability change (reduces accidental exposure of unfinished features and avoids auth-path changes when experiments are broadly enabled).
- Depends on: settings keys `expFileViewer`, `expZen`, `expVoiceAuthFlow` existing and being persisted/served by the settings store; translation keys `zen.*` existing across locales.

### Rewrite Commit Message

- Option A (split, recommended):
  - `feat(experiments): add per-feature flags for Zen, file viewer, and voice auth flow`
  - `refactor(i18n): translate Zen UI strings`
- Option B (keep single commit):
  - `feat(experiments): gate Zen, file viewer, and voice auth flow`

---

## 039 2026-01-21 6ed379f82c34 `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=6ed379f82c34 patchFile=/tmp/leeroy-wip-pass2/039.patch patchSha256=3ae4cad6fe7c831b911c1cb57135fbc7f0688022de1a42e0249e1980bb3e81da patchBytes=260667 patchLines=6344 notesReviewed=yes reviewedAt=2026-01-24T19:06:52+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=UI overlay foundation: modal stacking + overlay portal + Popover primitives + consumer migrations; must split into ordered commits inside PR02 (test stubs → portal host → modal stack → Popover → migrations).

- Subject: refactor(ui): add modal + popover overlay primitives
- Reasons: too many files (43)

### Manual Review Notes

- Diffstat: `43 files changed, 4669 insertions(+), 930 deletions(-)`
- Commit message/body vs diff: matches well. The body accurately describes the core intent: unify overlay stacking across web/native, add Popover+portal/spotlight infra, refactor consumers (AgentInput, CommandPalette, item actions), and add tests.
- Major changes (grouped by subsystem):
  - **Overlay portal infrastructure (native + web)**
    - Adds `OverlayPortalProvider` + `OverlayPortalHost` to allow rendering overflow-escaping overlays on native via a centralized host near the root.
    - Adds `ModalPortalTargetProvider` to expose a “portal target inside the active modal subtree” for web portals that must remain inside Radix/Vaul focus stacks.
  - **Popover primitive**
    - Adds a new `Popover` component that measures `anchorRef` (+ optional boundary) and positions content with placement, max size caps, and edge padding.
    - Supports portal rendering:
      - Web: fixed-position portals via `react-dom` (loaded with `requireReactDOM()` so native bundles don’t pull `react-dom`).
      - Native: portals via `OverlayPortalHost` (escape overflow clipping in lists/rows/scrollviews).
    - Adds backdrop options (tap-to-dismiss) with `dim` or `blur` effects; supports “spotlight” / “anchor overlay” to keep the trigger crisp above the effect.
  - **Modal primitives / stacking**
    - `ModalProvider` now renders **all** modals in the stack (not just top), enables backdrop only for the top-most modal, and assigns increasing `zIndexBase` values so backdrops and content layer predictably.
    - `BaseModal` is substantially reworked:
      - Web: uses Radix Dialog (via `require(...)` CJS entrypoints) and wraps content in a DismissableLayer `Branch` to avoid cross-library dismissal/focus issues.
      - Web: provides an internal portal host DOM node and wires it via `ModalPortalTargetProvider` for popovers that should portal “within the modal”.
      - Native: removes RN `<Modal>` path; uses an in-tree overlay root with explicit `zIndex/elevation` and optional `showBackdrop`.
    - `ModalManager.prompt` now uses the custom prompt everywhere (including iOS) for consistent behavior; adds a targeted test ensuring it does **not** call `Alert.prompt`.
  - **Consumers refactored onto the new primitives**
    - `AgentInput`: replaces bespoke absolute-position overlays/backdrop with `Popover` for both autocomplete and settings overlays; replaces manual gradient/fade math with `useScrollEdgeFades` + `ScrollEdgeIndicators`; introduces `ActionListSection` and a `SelectableRow` pattern.
    - Command Palette + item actions: removes legacy modal wrappers (`CommandPaletteModal.tsx`, `ItemActionsMenuModal.tsx`) and routes menus/overlays through the new overlay plumbing.
    - Adds dropdown menu primitives (`components/dropdown/*`) and related types/hooks to standardize selectable menus.
  - **Test infra and coverage**
    - Adds extensive targeted tests around: `Popover` (web + native portal), `BaseModal` (web Radix path), modal provider stacking/backdrop rules, overlay positioning/arrow behavior, and an autocomplete regression test.
    - Updates `expo-app/vitest.config.ts` aliases to use a `react-native` stub and node-safe platform adapters in tests.
- Coherence / history hygiene:
  - This is a “foundation refactor” commit that touches many areas. It’s internally coherent, but it is far too large for an eventual upstream PR as-is.
  - For history rewrite, it should be split into a small set of ordered commits so reviewers can validate each layer independently (test infra → primitives → consumer migrations).
- Risk notes (why it was flagged):
  - This changes core UI behavior: modal stacking/backdrops/z-indexing and popover positioning (web portal vs native portal). These are historically fragile areas.
  - The commit mitigates risk with tests and careful cross-platform guards (`requireReactDOM`, CJS Radix requires, portal opacity gating until measurements exist).

### Verdict

- Keep/squash/split/drop/reorder: **keep, but split + reorder during rewrite**.
  - Recommended split order (high level):
    1) `test(expo-app): harden vitest RN/web platform stubs` (vitest aliases + react-native stub + platform adapters)
    2) `feat(ui): add overlay portal host + modal portal target` (OverlayPortal + ModalPortalTarget)
    3) `refactor(modal): support stacked modals with consistent web/native primitives` (ModalProvider + BaseModal + ModalManager.prompt)
    4) `feat(ui): add Popover with web/native portal backdrops` (+ Popover tests)
    5) `refactor(ui): migrate AgentInput and menus to Popover/overlays` (consumer migrations)
- Depends on:
  - Runtime deps: `@radix-ui/react-dialog`, `@radix-ui/react-dismissable-layer` available on web; `react-dom` available in web bundle.
  - App wiring: `ModalProvider` must remain mounted near the root so `OverlayPortalHost` exists for native portal popovers.
  - Follow-on commits: consumer migrations that assume `Popover` + portal host exist.

### Rewrite Commit Message

- Keep subject (as a bucket title): `refactor(ui): add modal + popover overlay primitives`
- If split (recommended), suggested subjects:
  - `test(expo-app): harden vitest RN/web platform stubs`
  - `feat(ui): add overlay portal host and modal portal target`
  - `refactor(modal): stack modals and unify web/native base modal`
  - `feat(ui): add Popover with portal + backdrop support`
  - `refactor(ui): migrate AgentInput and menus to Popover primitives`

---

## 040 2026-01-21 f0787de5308e `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=f0787de5308e patchFile=/tmp/leeroy-wip-pass2/040.patch patchSha256=33c95731c921933829af09309c46e15d2de6d068618cf82ec5d1088cba2d8d84 patchBytes=184734 patchLines=4333 notesReviewed=yes reviewedAt=2026-01-24T19:11:23+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR09 plannedAt=2026-01-25T12:37:23+0100 manualReviewedForPrPlan=yes notes=Massive mixed commit centered on secrets/vault: SecretString sealing + saved secrets + requirement resolver UI; split out terminal settings primitives to PR18 and sync/backoff + HappyError retry-policy work to PR27; crypto shims may belong in PR01/PR09. Also reconcile legacy profile normalization (011) vs removal noted here during rewrite.

- Subject: feat(sync): add secrets + terminal settings primitives
- Reasons: too many files (60); safety/security-sensitive area

### Manual Review Notes

- Diffstat: `60 files changed, 2348 insertions(+), 636 deletions(-)`
- Commit message/body vs diff: mostly matches. The commit does introduce a few extra “sync reliability” upgrades (sync error tracking, socket error bookkeeping, no-op delta suppression + debug logging) that aren’t called out explicitly in the subject but are consistent with the “sync primitives” umbrella.
- Major changes (grouped by concern):
  - **Field-level secrets (encrypted-at-rest inside settings)**
    - Adds `sync/secretSettings.ts`:
      - Defines `SecretString` (`{ _isSecretValue: true, value?, encryptedValue? }`) and `EncryptedString` payloads (`enc-v1`).
      - Implements `encryptSecretString`/`decryptSecretString` using libsodium `crypto_secretbox_*` with a derived per-account key (`deriveSettingsSecretsKey`).
      - Implements `sealSecretsDeep(...)` to traverse an arbitrary object graph and convert any plaintext `SecretString.value` into `encryptedValue`, dropping `value` (prevents plaintext persistence).
      - Implements `decryptSecretValue(...)` helper that prefers plaintext `value` (input/legacy) but otherwise decrypts `encryptedValue` when a key is available.
    - Extends `sync/sync.ts` to derive `settingsSecretsKey` from `credentials.secret` and:
      - Seal settings deltas at write-time (`applySettings` calls `sealSecretsDeep` before persistence).
      - Provide `encryptSecretValue(...)` and `decryptSecretValue(...)` helpers for settings-like secret containers.
  - **Secrets saved in settings + bindings**
    - Replaces legacy “apiKeys” with generic `SavedSecret`:
      - `sync/settings.ts`: adds `SavedSecretSchema` with `kind` + `encryptedValue: SecretStringSchema` and validates “must have value or encrypted value”.
      - Adds `settings.secrets` + `settings.secretBindingsByProfileId` (profileId → envVarName → secretId), replacing `apiKeys` / `defaultApiKeyByProfileId`.
    - Adds `sync/secretBindings.ts` to prune invalid/dangling bindings:
      - Drops bindings for unknown profiles (custom or built-in), invalid env-var names, env-vars not declared as secret requirements, and deleted secret IDs.
      - Normalizes env-var names to uppercase.
    - Adds `utils/secretSatisfaction.ts` (+ tests) to compute whether a profile’s secret requirements are satisfied given saved secrets and bindings (used later by the “requirements” UI flow).
  - **Terminal/tmux settings primitives**
    - Adds settings schema defaults and types for terminal spawning:
      - `settings.terminalUseTmux`, `terminalTmuxSessionName`, `terminalTmuxIsolated`, `terminalTmuxTmpDir`, and per-machine overrides via `terminalTmuxByMachineId`.
    - Adds `sync/terminalSettings.ts` (+ specs) to resolve spawn options from global settings + per-machine overrides (normalizes empty/whitespace values and default fallbacks).
    - Adds `sync/spawnSessionPayload.ts` (+ tests) to build the RPC payload including optional terminal spawn options.
  - **Cross-platform crypto shims (node/vitest-safe)**
    - Adds `platform/*.{ts,node.ts}` shims for `cryptoRandom`, `digest`, `hmacSha512`, and `randomUUID` so code can run in vitest/node without static `expo-crypto` imports.
    - Updates `utils/oauth.ts` PKCE generation to use the shims (`digest('SHA-256', ...)` etc).
    - Migrates several existing crypto call sites (AES/Base64 app specs, libsodium helper, artifact encryption) from `expo-crypto` static imports to the platform shims.
  - **API/client error typing (HappyError)**
    - Extends `utils/errors.ts` (`HappyError`) with optional `status` and `kind` to support richer UX + sync retry policy.
    - Updates multiple API modules (`apiArtifacts`, `apiFeed`, `apiFriends`, `apiGithub`, `apiKv`, `apiPush`, `apiServices`, `apiUsage`) to throw non-retryable `HappyError(...)` for most 4xx responses (excluding `408` and `429`), and to preserve “auth/config” kinds where it’s known (notably GitHub + usage endpoints).
  - **Sync reliability / observability additions**
    - `sync/sync.ts` + `sync/storage.ts`: adds `syncError` state, socket error fields, and a `lastSyncAt` timestamp; wires `InvalidateSync` hooks to set/clear error state and retry metadata.
    - `sync/debugSettings.ts` + settingsParse logging: adds optional debug logging for settings parse/apply flows.
    - `applySettings` now avoids no-op writes by shallowly checking whether the delta changes references/scalars before persisting.
    - `apiSocket`: adds explicit `onError` listeners and clears last error on successful connect; `syncInit` wires socket errors into `syncError`.
    - `utils/time.ts` + `utils/sync.ts`: extends `createBackoff` with `shouldRetry` + `onRetry` hooks (and safer backoff math), and reworks `InvalidateSync` / `ValueSync` to use backoff with explicit onSuccess/onError/onRetry reporting (avoids deadlocking the invalidation queue on non-retryable errors).
  - **Terminal metadata + helpers**
    - `storageTypes.ts`: extends `MetadataSchema` with a `terminal` payload (`plain|tmux`, requested mode, fallback reason, tmux target/tmpDir) and adds a parsing test.
    - Adds `utils/terminalSessionDetails.ts` (+ tests) for deriving attach commands and tmux-specific metadata in the UI.
  - **Notable schema/migration shifts**
    - `sync/settings.ts`: drops support for legacy provider config objects (e.g. `openaiConfig`, `anthropicConfig`) by removing the normalization/migration step; profiles are expected to use `environmentVariables` + `envVarRequirements`.
    - `getProfileEnvironmentVariables` no longer injects tmux env vars; terminal spawning is now driven by terminal settings (global/per-machine), not profile tmux config.
    - `AIBackendProfileSchema`: removes the `apiKeyEnv` authMode and drops per-profile `tmuxConfig`; profiles now express runtime requirements only via `envVarRequirements` (with `required`), and terminal spawning is controlled by the terminal settings primitives.
  - **Misc wiring changes**
    - `profileRouteParams.ts`: renames `consumeApiKeyIdParam` → `consumeSecretIdParam` to match the secrets UI routing.
    - `sync/reducer.ts`: compares `allowedTools` arrays with “empty-equivalence” semantics (`undefined` / `null` / `[]`) to reduce false-positive updates; tests updated to assert “no duplicates” instead of “no updates”.
- Risk notes:
  - Secrets safety hinges on `settingsSecretsKey` being available: `sealSecretsDeep` is a no-op when the key is null. This is probably correct for “not authenticated yet” states, but it means UI flows should avoid accepting/committing secrets until the key exists (or ensure key derivation always succeeds for authenticated users).
  - The removal of legacy provider config migration should be validated against any existing persisted settings formats before upstreaming (it’s a behavior compatibility change).

### Verdict

- Keep/squash/split/drop/reorder: **keep, but split during rewrite** (this is a good foundational change, but it mixes multiple large concerns).
  - Recommended split (topic-ordered):
    1) `feat(secrets): add SecretString and seal secrets before persistence`
    2) `feat(settings): add saved secrets and per-profile secret bindings`
    3) `feat(terminal): add tmux spawn settings and rpc payload builder`
    4) `refactor(crypto): add node-safe platform shims for vitest`
    5) `refactor(sync): add error tracking and avoid no-op settings writes`
    6) (optional) `refactor(profiles): drop legacy provider config objects` (only if we confirm we still need this change)
- Depends on:
  - libsodium being available/ready (`sodium.ready`) for encryption.
  - `credentials.secret` being present and stable (32 bytes base64url) so `settingsSecretsKey` can be derived.
  - Follow-on UI commits that surface secret requirements, editing, and “never re-display” rules consistently.

### Rewrite Commit Message

- Keep subject (as a bucket title): `feat(sync): add secrets + terminal settings primitives`
- If split (recommended), see the per-topic subjects listed above.

---

## 041 2026-01-21 66ee1eaf88a6 `secrets`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=66ee1eaf88a6 patchFile=/tmp/leeroy-wip-pass2/041.patch patchSha256=afe787a1ab3f76435191f29d469da5336cc1cdbd7be45b44dbf894bd27538a4e patchBytes=285675 patchLines=5209 notesReviewed=yes reviewedAt=2026-01-24T19:17:58+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR09 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=Secrets/vault UX: replace API keys UI with secrets + SecretRequirementModal + env presence hook; split out features/settings reorder bits to PR06, and fold translations into PR09/PR08/PR26 as appropriate during rewrite.

- Subject: feat(secrets): add secrets management + requirement resolver
- Reasons: too many files (29); safety/security-sensitive area

### Manual Review Notes

- Diffstat: `29 files changed, 2315 insertions(+), 1099 deletions(-)`
- Commit message/body vs diff: matches. This is the UI + UX integration layer on top of the underlying secrets primitives added earlier (SavedSecret / SecretString / bindings / machine preview presence).
- Major changes:
  - **Replace legacy API key UI with Secrets**
    - Renames settings screen `settings/api-keys.tsx` → `settings/secrets.tsx` and swaps `ApiKeysList` for `SecretsList`.
    - Renames/moves components:
      - `components/apiKeys/ApiKeysList.tsx` → `components/secrets/SecretsList.tsx`
      - `components/apiKeys/ApiKeyAddModal.tsx` → `components/secrets/SecretAddModal.tsx`
    - Removes `ApiKeyRequirementModal.tsx` entirely (replaced by the new secret requirements flow).
  - **SecretRequirementModal (requirements resolver)**
    - Adds `components/SecretRequirementModal.tsx` (large new modal) that can resolve a required secret env var via:
      - machine environment (presence-only checks via preview-env, never fetching values),
      - selecting a saved secret (optionally setting default binding),
      - entering a session-only secret (“enter once”) when allowed.
    - Supports resolving multiple secret env vars in one modal via a dropdown (when provided `secretEnvVarNames`).
    - Uses new scroll/overlay primitives (`DropdownMenu`, `useScrollEdgeFades`, `ScrollEdgeFades`, `ScrollEdgeIndicators`) for stable UX in constrained modal sizes.
  - **Machine env presence preflight**
    - Adds `hooks/useMachineEnvPresence.ts`:
      - Caches presence results by machine+keyset with TTL and inflight de-dupe.
      - Calls `machinePreviewEnv(..., { sensitiveKeys: keys })` to ensure presence checks never fetch secret values.
  - **Profiles + environment variables UX refresh**
    - `ProfileEditForm.tsx` is heavily refactored to align with the new `envVarRequirements` model (required + kind + vault).
    - `EnvironmentVariableCard.tsx` gains per-source requirement controls:
      - Required toggle + “use secret vault” toggle.
      - When vault is enabled, disables fallback/default values (prevents plaintext persistence) and can prompt to pick a default saved secret.
    - `EnvironmentVariablesList.tsx` gets small wiring changes + a new test (ensures list behavior doesn’t regress).
    - `useProfileEnvRequirements` is updated to derive required keys from `envVarRequirements` (required-only, kind-aware), matching the new schema.
  - **i18n + theme**
    - Adds many new translation keys across locales for “Secrets”, vault wording, requirements flow, and the updated settings/profile screens.
    - Minor theme token tweaks to support the new UI.
  - **Other notable changes mixed in**
    - `settings/features.tsx` is reorganized (standard toggles first, experiments last) and introduces web-only toggles for “enter to send” and “command palette”. This is plausibly related, but it’s still a separate “settings UX” concern that may be worth splitting during rewrite.
    - `hooks/useEnvironmentVariables.ts` switches to `useLayoutEffect` (web) to ensure `isLoading=true` flips before consumer `useEffect` runs, preventing a “missing secret” race; adds a focused hook test.
    - `components/profiles/*`: adds `getProfileDisplayName` (built-in profiles use localized names), changes badge behavior via `ProfileRequirementsBadge` overrides, and updates row actions/popovers (`pinnedActionIds`, `popoverBoundaryRef`) plus item-actions type imports (`components/itemActions/types`).
- Risk notes:
  - This is security-sensitive UX: it must uphold the invariant “never re-display secret values after entry” and avoid persisting plaintext. The design pushes toward that (vault disables fallback entry; machine presence checks are value-free), but it’s worth keeping this commit tightly grouped with the underlying secret-sealing primitives and any follow-up bugfixes (there are several later fixes in the queue).
  - The new requirementVarName logic in `EnvironmentVariableCard` correctly applies requirements to the *source* env var when using template indirection (e.g. `${SOURCE_VAR}`), which is subtle and important for correctness.

### Verdict

- Keep/squash/split/drop/reorder: **keep, but split during rewrite** to make the security surface area reviewable.
  - Recommended split:
    1) `feat(secrets): replace api-keys UI with saved secrets`
    2) `feat(secrets): add SecretRequirementModal and machine env presence hook`
    3) `refactor(profiles): migrate env var requirements UI and vault controls`
    4) `refactor(i18n): add secrets/vault translations`
    5) (optional) `refactor(settings): reorganize features screen and add web-only toggles`
- Depends on:
  - The underlying secrets primitives and settings schema from `feat(sync): add secrets + terminal settings primitives` (040).
  - Overlay/menu primitives from `refactor(ui): add modal + popover overlay primitives` (039), since the modal uses dropdown + scroll edge helpers.
  - `machinePreviewEnv` support on the daemon side; the hook handles unsupported machines by exposing `isPreviewEnvSupported=false`.

### Rewrite Commit Message

- Keep subject (as a bucket title): `feat(secrets): add secrets management + requirement resolver`
- If split (recommended), see the per-topic subjects listed above.

---

## 042 2026-01-21 bdaf6913141f `terminal`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=bdaf6913141f patchFile=/tmp/leeroy-wip-pass2/042.patch patchSha256=87ea409cf0f61ce99cb5597c0bf5cd02cdf225bfe9bea9f1451c10ea3bc92986 patchBytes=39225 patchLines=826 notesReviewed=yes reviewedAt=2026-01-24T19:20:11+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=Terminal/tmux end-to-end: Terminal settings screen + tmux detection + metadata/attach info; keep adjacent to later tmux plumbing.

- Subject: feat(terminal): add Terminal settings + tmux metadata
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Adds a dedicated Terminal settings screen and wires tmux configuration + metadata end-to-end:
  - New screen `/(app)/settings/terminal.tsx`:
    - `terminalUseTmux` master toggle
    - tmux options: session name, isolated server, tmp dir.
  - Keeps `/(app)/settings/tmux.tsx` as a backwards-compatible alias pointing users to the Terminal screen.
  - Updates `SettingsView`:
    - replaces/renames the entry to “Terminal”
    - shows an enabled/disabled subtitle driven by `terminalUseTmux`.
    - Also tightens GitHub connect UX: when `getGitHubOAuthParams` throws a non-retryable `HappyError`, it surfaces the message as the subtitle and disables the connect action.
- Extends CLI detection and machine details to include tmux:
  - `hooks/useCLIDetection.ts` now includes `tmux` availability (boolean|null) and can surface tmux in the `detect-cli` response when present; older daemons treat missing tmux as unknown.
  - Adds `useCLIDetection.hook.test.ts` to validate:
    - tmux is included when present in detect-cli response
    - missing tmux is treated as `null` (unknown) for older daemons
    - fallback bash probing includes a tmux `command -v tmux` check.
  - `components/machine/DetectedClisList.tsx` is updated to render tmux as an entry when provided.
  - Machine details screen adds tmux visibility/override controls (including a machine-specific override map keyed by machine id) and guards enabling tmux when it is not detected.
  - Machine details “spawn session” now passes `terminal` spawn options via `resolveTerminalSpawnOptions({ settings, machineId })` into `machineSpawnNewSession(...)` (so machine-scoped overrides actually affect session creation from this screen).
- Surfaces tmux spawn/attach metadata in session details:
  - Session info UI adds fields to display tmux attach target and a fallback reason (useful for debugging resume workflows).
- Coherence: the commit body accurately describes the changes; most changes are within the terminal/tmux feature area.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a feature-sized commit but cohesive around “Terminal settings + tmux support”.
- Depends on:
  - Settings schema containing the terminal/tmux keys (`terminalUseTmux`, `terminalTmux*`, and machine override map keys) and i18n strings referenced by the new screens.
  - Daemon `detect-cli` optionally returning tmux fields; commit handles older daemons conservatively.

### Rewrite Commit Message

- Suggested tweak (optional, if you want tighter scope): `feat(terminal): add terminal settings and tmux metadata`
- Current is acceptable: `feat(terminal): add Terminal settings + tmux metadata`

---

## 043 2026-01-21 bc779e4f7909 `new-session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=bc779e4f7909 patchFile=/tmp/leeroy-wip-pass2/043.patch patchSha256=056ec1d2a238bf9e0dcebd172b7a1ee7e61faa2f118f3b8c21a224abf61de5f2 patchBytes=135229 patchLines=2803 notesReviewed=yes reviewedAt=2026-01-24T19:27:57+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=New-session: integrate secrets requirement resolution + terminal spawn options + popover boundary; split out search/auth/ui refactors into PR27/PR01/PR02 during rewrite.

- Subject: refactor(new-session): integrate secrets + terminal spawn options
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `17 files changed, 1051 insertions(+), 661 deletions(-)`
- Commit message/body vs diff: mostly matches. The bulk of the diff is the new-session wizard wiring for secrets + terminal spawning. It also includes a handful of adjacent reliability/UX updates (friends search error surfacing, crypto shim usage in auth helpers, MainView header extraction), which are coherent but would be better as separate commits during rewrite.
- Major changes (new-session flow):
  - **Secrets: move from “single api key” to multi-secret resolution**
    - Route params + pick screen rename: `apiKeyId` → `secretId` and `new/pick/api-key.tsx` → `new/pick/secret.tsx`.
    - `new/index.tsx` now:
      - Tracks secret selection per **profile + env var name** via:
        - `selectedSecretIdByProfileIdByEnvVarName` (saved secret IDs or `''` to mean “use machine env”)
        - `sessionOnlySecretValueByProfileIdByEnvVarName` (plaintext in memory only)
      - Uses `useMachineEnvPresence` to preflight which env vars are present on the selected machine (presence-only; never fetches values).
      - Uses `getSecretSatisfaction(...)` to:
        - decide what’s missing,
        - drive `SecretRequirementModal` with the correct target env var,
        - enforce required secrets before session creation.
      - Encrypts session-only secret values before writing drafts (`sync.encryptSecretValue(...)`) so drafts never persist plaintext.
      - On spawn, injects only the secrets that must be provided to the daemon:
        - machine-env satisfied secrets are not injected,
        - saved secrets are decrypted just-in-time via `sync.decryptSecretValue(...)`,
        - session-only secrets are injected from the in-memory values.
  - **Terminal/tmux spawning integration**
    - Resolves terminal spawn options centrally via `resolveTerminalSpawnOptions({ settings, machineId })` and passes `terminal` into `machineSpawnNewSession(...)`.
    - Adds tmux detection UX:
      - `NewSessionWizard`’s CLI availability shape gains `tmux`.
      - Machine selection UI surfaces a “tmux not detected” banner when tmux is requested by settings but the machine doesn’t have it available.
  - **Wizard refactors**
    - Moves section scroll bookkeeping (refs/offsets) into `NewSessionWizard.tsx` to reduce prop surface area on `NewSessionScreen`.
    - Adds explicit refresh behavior that refreshes machines + CLI detection + env presence preflight as a single “refresh machine data” action.
  - **Popover boundary / overlay safety**
    - `new/index.tsx` introduces a `PopoverBoundaryProvider` with a boundary ref; this helps dropdowns/popovers (e.g., requirement modal menus) avoid clipping and measure within a known boundary.
- Adjacent changes included in this commit (consider splitting during rewrite):
  - `useSearch` now returns `{ error }` and uses bounded retries with requestId cancellation to avoid infinite background retries; friends search surfaces the error message.
  - Auth helpers (`authChallenge`, `authQRStart`, app index) switch random byte generation to the new `platform/cryptoRandom` shim.
  - `MainView` swaps inline status logic for `ConnectionStatusControl` (new UI primitive from the overlays work).
  - Sidebar/UI polish: `SidebarView` wraps in `PopoverBoundaryProvider`, replaces inline socket status UI with `ConnectionStatusControl`, and adds a retry/restore banner when sync/socket errors occur; `OptionTiles` gains a `disabled` option (with opacity/press guards).

### Verdict

- Keep/squash/split/drop/reorder: **keep, but split during rewrite** (core new-session changes are important; the adjacent refactors should be isolated).
  - Recommended split:
    1) `refactor(new-session): integrate secret requirement resolution into wizard`
    2) `refactor(new-session): resolve terminal spawn options and tmux warnings`
    3) `refactor(new-session): simplify wizard scroll bookkeeping`
    4) `fix(search): add bounded retries and error state`
    5) `refactor(auth): use platform cryptoRandom shims`
    6) `refactor(ui): use ConnectionStatusControl in MainView` (if not already grouped with the overlay primitives work)
- Depends on:
  - `feat(sync): add secrets + terminal settings primitives` (040) for SecretString/sealing + terminal settings.
  - `feat(secrets): add secrets management + requirement resolver` (041) for `SecretRequirementModal` and `useMachineEnvPresence`.
  - `refactor(ui): add modal + popover overlay primitives` (039) for dropdown/overlay plumbing used by the new flows.

### Rewrite Commit Message

- Keep subject (as a bucket title): `refactor(new-session): integrate secrets + terminal spawn options`
- If split (recommended), see the per-topic subjects listed above.

---

## 044 2026-01-21 cb11d4bdb25a `docs`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=cb11d4bdb25a patchFile=/tmp/leeroy-wip-pass2/044.patch patchSha256=d02b7bee3e56cf79b47b394f3645988462e7bc567333891fb5a54f9b1f614bdf patchBytes=7661 patchLines=118 notesReviewed=yes reviewedAt=2026-01-24T19:28:37+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=Docs-only: AGENTS/CLAUDE guidance and invariants; keep in Foundations/DevX.

- Subject: docs: add AGENTS.md symlink + update CLAUDE.md
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Adds `expo-app/AGENTS.md` as a symlink to `expo-app/CLAUDE.md` to keep agent guidance single-sourced.
- Updates `expo-app/CLAUDE.md` with concrete guidance for:
  - modal/dialog and popover stacking invariants across web (Radix) and native (avoid RN `<Modal>` layering bugs)
  - settings persistence model (server blob + pending delta) and “delta-only pending parsing” constraints
  - secret handling rules (never log; encrypt-at-rest via secret container schemas).
- Coherence: doc-only change, aligns with the surrounding reliability work (modals/settings/secrets).

### Verdict

- Keep/squash/split/drop/reorder: keep; doc-only.
- Depends on: none.

### Rewrite Commit Message

- Suggested Conventional Commit tweak: `docs(expo-app): single-source agent guidance and document modal/settings invariants`

---

## 045 2026-01-21 1bc3b9715776 `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1bc3b9715776 patchFile=/tmp/leeroy-wip-pass2/045.patch patchSha256=677cf555e52a3c402aada9d454ccf354ebe2586511aea82db752f7ae257c2358 patchBytes=3116 patchLines=84 notesReviewed=yes reviewedAt=2026-01-24T19:29:06+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR27 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=Sync robustness: guard JSON parsing for non-JSON 400 (GitHub OAuth), with test.

- Subject: fix(sync): handle non-JSON 400 responses
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Hardens error parsing for GitHub OAuth params:
  - `apiGithub.ts` now guards `response.json()` in the 400 path:
    - default message: `GitHub OAuth not configured`
    - if JSON parse succeeds and includes `error`, uses it
    - if JSON parse fails (non-JSON body, disconnect errors), it still throws a `HappyError` with config kind and correct status.
  - Adds `apiGithub.test.ts` proving that a 400 with non-JSON body becomes a `HappyError(kind='config', status=400)` with the default message.
- Coherence: strong and safety-relevant (robustness against server/proxy error bodies).

### Verdict

- Keep/squash/split/drop/reorder: keep; a clean correctness fix with targeted test.
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `fix(sync): handle non-JSON 400 responses`

---

## 046 2026-01-21 ea2e4d19d633 `deps`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ea2e4d19d633 patchFile=/tmp/leeroy-wip-pass2/046.patch patchSha256=56d31e16958294810504afab058b72991c083212c0a3f8aeae3db8490c255d00 patchBytes=2141 patchLines=54 notesReviewed=yes reviewedAt=2026-01-24T19:29:31+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=Foundations/DevX: react-test-renderer version alignment (test infra maintenance).

- Subject: chore(deps): align react-test-renderer with react
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Bumps `react-test-renderer` from `19.0.0` → `19.1.0` in `expo-app/package.json` and updates `expo-app/yarn.lock`.
- Ensures related transitive versions align (e.g. `react-is` and `scheduler`).
- Coherence: matches subject.

### Verdict

- Keep/squash/split/drop/reorder: keep; belongs near other “test renderer / react version alignment” maintenance (e.g. 002).
- Depends on: none (but should be consistent with the repo’s React version).

### Rewrite Commit Message

- Keep as-is: `chore(deps): align react-test-renderer with react`

---

## 047 2026-01-21 dd041d21ecfc `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=dd041d21ecfc patchFile=/tmp/leeroy-wip-pass2/047.patch patchSha256=792389c9c15b60f99358c4b7d9feed9f15a4debd0b5fbaf9acdbd970342fed78 patchBytes=5691 patchLines=147 notesReviewed=yes reviewedAt=2026-01-24T19:30:12+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR27 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=Sync robustness: guard JSON parsing on disconnect 404 paths (GitHub/services), with tests; keep adjacent to 045.

- Subject: fix(sync): guard JSON parsing on disconnect errors
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Extends the “non-JSON body safe parsing” pattern to disconnect flows:
  - `apiGithub.ts`:
    - 404 “not connected” path now guards `response.json()` similarly to the 400 case.
    - Adds test coverage for 404 non-JSON in `disconnectGitHub`.
  - `apiServices.ts`:
    - 404 “service not connected” path now guards `response.json()` and falls back to `${service} account not connected`.
    - Adds `apiServices.test.ts` proving 404 non-JSON bodies still throw a `HappyError` with a sane message.
- Coherence: strong; this is the natural continuation of 045 and likely should remain adjacent during history rewrite.

### Verdict

- Keep/squash/split/drop/reorder: keep; pairs well with 045 as a “robust error parsing” mini-series.
- Depends on: 045 is conceptually related (shared pattern), but not a hard code dependency.

### Rewrite Commit Message

- Keep as-is: `fix(sync): guard JSON parsing on disconnect errors`

---

## 048 2026-01-21 395da4ff3167 `zen`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=395da4ff3167 patchFile=/tmp/leeroy-wip-pass2/048.patch patchSha256=428a760c29ed02bd7fd265fa22ad4393f4aa9cfaf3ff3d883d17911ef5e37e34 patchBytes=1153 patchLines=27 notesReviewed=yes reviewedAt=2026-01-24T19:30:36+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=Zen tiny refactor; keep with Zen/experiments feature PR (PR06) for coherence.

- Subject: refactor(zen): avoid todo variable shadowing
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Tiny readability refactor in `-zen/ZenHome.tsx`: renames local `t` mapping variable to `todo` to avoid shadowing and improve clarity.
- Coherence: matches subject.

### Verdict

- Keep/squash/split/drop/reorder: keep; can be squashed into other Zen refactors if desired.
- Depends on: none.

### Rewrite Commit Message

- Keep as-is: `refactor(zen): avoid todo variable shadowing`

---

## 049 2026-01-21 2e956f32f220 `new`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2e956f32f220 patchFile=/tmp/leeroy-wip-pass2/049.patch patchSha256=f7fcfdf43ac24be5936137b84318c1fd819003a1f314b111adcfeba3f46e0475 patchBytes=39497 patchLines=974 notesReviewed=yes reviewedAt=2026-01-24T19:31:37+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=New-session iOS navigation reliability: ensure pick screens/profile-edit present above containedModal; includes regression tests.

- Subject: fix(new): keep pick screens above iOS modal
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `13 files changed, 739 insertions(+), 37 deletions(-)`
- What it fixes:
  - When `/new` is presented as `containedModal` on iOS, pushing a default “card” screen from within it can land *behind* the modal (the back stack grows but the pushed screen isn’t visible). This especially affects picker screens and profile-edit navigation.
- Key changes:
  - Forces the relevant screens to also present as `containedModal` on iOS:
    - `app/(app)/new/pick/machine.tsx`, `path.tsx`, `profile.tsx`, `secret.tsx`, and `new/pick/profile-edit.tsx` header config.
    - `app/(app)/_layout.tsx` sets the profile-edit route presentation to `containedModal` on iOS so it always appears above the wizard.
  - Adds explicit `headerLeft` back buttons for iOS `containedModal` screens (so the user always has a reliable “Back” affordance and the navigation stack can unwind correctly).
  - `app/(app)/settings/profiles.tsx` adjusts native navigation behavior:
    - On native, editing a profile navigates to `/new/pick/profile-edit` instead of relying on an inline modal editor (improves consistency with the containedModal stack approach).
  - Adds targeted presentation/navigation tests for all picker screens + the profiles screen (asserts `presentation === 'containedModal'` on iOS and that a header back handler exists).
- Coherence:
  - This commit is cohesive and well-scoped: one concrete iOS navigation/presentation bug, plus tests guarding against regression.

### Verdict

- Keep/squash/split/drop/reorder: keep. This is a valuable reliability fix and already includes meaningful regression tests.
- Depends on: none beyond the app’s navigation patterns (expo-router + platform checks). Tests rely on vitest stubs introduced earlier in the overlay/testing work.

### Rewrite Commit Message

- Keep subject: `fix(new): keep pick screens above iOS modal`

---

## 050 2026-01-21 2da2f9349ec5 `popover`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2da2f9349ec5 patchFile=/tmp/leeroy-wip-pass2/050.patch patchSha256=528033ab018e1175a1abed29c9b3239d2c2eea51d18cee907707829ad0593521 patchBytes=3016 patchLines=48 notesReviewed=yes reviewedAt=2026-01-24T19:32:04+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:42:28+0100 manualReviewedForPrPlan=yes notes=Popover web typings fix ('fixed' positioning); squash into PR02 Popover/web portal hardening.

- Subject: fix(popover): allow web fixed positioning types
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 4 insertions(+), 4 deletions(-)`
- What changed:
  - `components/Popover.tsx` switches several `position: 'fixed'` assignments to `position: ('fixed' as any)` (web-only paths).
- Why:
  - React Native’s `position` style typings typically don’t include `'fixed'`, but on web it’s valid and required to avoid clipping when portalled.
  - This commit is a pragmatic typing workaround without changing runtime behavior.
- Coherence:
  - The diff matches the intent of the subject.
  - Minor nit: `as any` is a blunt tool; during the history rewrite we may want to normalize this pattern (e.g. one helper/cast in one place) to avoid scattered `any` casts.

### Verdict

- Keep/squash/split/drop/reorder: keep; can be squashed into other `Popover` portal/web fixes if we regroup by topic.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(popover): allow web fixed positioning types`

---

## 051 2026-01-21 43f23f2f082e `format`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=43f23f2f082e patchFile=/tmp/leeroy-wip-pass2/051.patch patchSha256=7c584fabdf4202d4ea83a309226e0fd7160771f8ccd65089c171e10c5c4f8674 patchBytes=3206 patchLines=82 notesReviewed=yes reviewedAt=2026-01-24T19:32:27+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Format-only whitespace normalization; squash into adjacent commits that touch the same files to avoid format-noise.

- Subject: chore(format): replace stray tabs with spaces
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 26 insertions(+), 26 deletions(-)`
- What changed:
  - Pure whitespace/indentation normalization in:
    - `components/SearchHeader.tsx` (a `StyleSheet` section was tab-indented)
    - `sync/settings.spec.ts` (a few properties were tab-indented)
- Coherence:
  - Matches the subject; no behavior change.

### Verdict

- Keep/squash/split/drop/reorder: keep, but consider squashing into a nearby commit that already touches these files to reduce “format-only” noise.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `chore(format): replace stray tabs with spaces`

---

## 052 2026-01-21 e1d55fdbd9ee `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e1d55fdbd9ee patchFile=/tmp/leeroy-wip-pass2/052.patch patchSha256=37114d44d75a7a9196bb9a2dc65e0a06dd3772e32188cfaa80d83a38b08936d0 patchBytes=2064 patchLines=40 notesReviewed=yes reviewedAt=2026-01-24T19:32:51+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Dev tooling correctness: untranslated-literals scanner parsing fixes; keep with tooling (PR01) and not inside feature PRs.

- Subject: fix(i18n): improve untranslated literal scan parsing
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 12 insertions(+), 2 deletions(-)`
- What changed:
  - `scripts/findUntranslatedLiterals.ts`:
    - Uses a more correct `ts.ScriptKind` detection for `.ts`, `.tsx`, `.js`, `.jsx` instead of a coarse `endsWith('x')` check.
    - Fixes grouping key parsing to preserve texts containing `:` (previous `split(':', 2)` would drop everything after the second segment).
- Coherence:
  - Strong match to subject; this is a correctness fix for a developer tooling script.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(i18n): improve untranslated literal scan parsing`

---

## 053 2026-01-21 c210c1115598 `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c210c1115598 patchFile=/tmp/leeroy-wip-pass2/053.patch patchSha256=fcc3390b3405216d1626e828703be21bfaf1dd90d3c6382756e29b77390f4828 patchBytes=1106 patchLines=29 notesReviewed=yes reviewedAt=2026-01-24T19:33:20+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR27 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Test robustness for sync API tests: un-stub globals between tests to avoid leakage/order dependence.

- Subject: test(sync): restore stubbed fetch between tests
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 5 insertions(+), 1 deletion(-)`
- What changed:
  - `sync/apiGithub.test.ts` adds `afterEach(() => vi.unstubAllGlobals())` to ensure any `vi.stubGlobal(...)` calls are undone between tests.
- Why:
  - Prevents test order dependence and “leaky global” behavior when multiple tests stub `fetch` or other globals.
- Coherence:
  - Matches subject.

### Verdict

- Keep/squash/split/drop/reorder: keep; can be squashed into the adjacent GitHub API test series if desired.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `test(sync): restore stubbed fetch between tests`

---

## 054 2026-01-21 66c678451335 `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=66c678451335 patchFile=/tmp/leeroy-wip-pass2/054.patch patchSha256=8ef52fb7589c3b292d7a1eba66f35710fc0471b938034409381ee0401ae2da85 patchBytes=8880 patchLines=191 notesReviewed=yes reviewedAt=2026-01-24T19:33:50+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Mixed small fixes across UI/new/terminal/dropdown/sync; keep but split into topic commits during rewrite (model-mode, picker params, dropdown chevron opacity, etc).

- Subject: fix(ui): apply small review fixes
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `8 files changed, 23 insertions(+), 25 deletions(-)`
- What changed (high-level):
  - `-session/SessionView.tsx`:
    - Hoists `CONFIGURABLE_MODEL_MODES` + the `isConfigurableModelMode` type guard to module scope (avoids recreating constants/types per render and removes an unnecessary `useCallback`).
    - Simplifies `updateModelMode` callback dependencies.
  - `new/pick/machine.tsx`: uses `CommonActions.setParams` instead of a manual `SET_PARAMS` action object (more idiomatic react-navigation).
  - `settings/terminal.tsx`: guards `TextInput` value (`tmuxSessionName ?? ''`) to avoid uncontrolled/controlled warnings.
  - `components/ConnectionStatusControl.tsx`: removes unused `isUsingCustomServer` import + local variable.
  - `components/OptionTiles.tsx`: ensures title text uses `theme.colors.text` (color correctness).
  - `components/SettingsView.tsx`: clears `githubUnavailableReason` before attempting to connect (so a prior failure reason doesn’t linger on a retry).
  - `components/dropdown/DropdownMenu.tsx`: fixes a logic bug where `opacity` was always `0` (chevron never visible unless overridden).
  - `utils/sync.ts`: changes `_backoff` to a definite-assignment field and initializes it in the constructor (keeps behavior but avoids initializing with partially configured callbacks).
- Coherence:
  - This is a “misc small fixes” sweep across multiple areas. Each individual change is small and defensible, but as a single commit it’s mixed.

### Verdict

- Keep/squash/split/drop/reorder: split or squash during history rewrite (recommended). If we keep it as-is, it should stay adjacent to the PR review context that motivated it.
- Depends on: none (but many of these are follow-ups to nearby feature commits: session model mode, new picker, settings, dropdown, sync backoff).

### Rewrite Commit Message

- Keep subject (if not splitting): `fix(ui): apply small review fixes`
- If splitting by topic: convert into multiple commits scoped to the touched features (session model mode, new picker, settings terminal, dropdown chevron opacity, sync backoff).

---

## 055 2026-01-21 2472b376775c `modal`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2472b376775c patchFile=/tmp/leeroy-wip-pass2/055.patch patchSha256=e3dd966bbbd9e34eeb832e502d4f13767f031955315a7ff627dd657fe58fe726 patchBytes=1127 patchLines=24 notesReviewed=yes reviewedAt=2026-01-24T20:01:38+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Modal test robustness: safer z-index assertion; squash into modal test series.

- Subject: test(modal): make backdrop z-index assertion safer
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 3 insertions(+), 1 deletion(-)`
- What changed:
  - `modal/ModalProvider.test.ts`:
    - Adds `expect(top).toBeDefined()` / `expect(bottom).toBeDefined()` before asserting on their props.
    - Uses optional access in the final numeric comparison to avoid crashing with a confusing error if earlier assumptions break.
- Coherence:
  - Matches subject; improves test failure messages and robustness.

### Verdict

- Keep/squash/split/drop/reorder: keep; can be squashed into the modal test series.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `test(modal): make backdrop z-index assertion safer`

---

## 056 2026-01-21 02450d179aaf `profiles`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=02450d179aaf patchFile=/tmp/leeroy-wip-pass2/056.patch patchSha256=ebf5a695b86c41c4206d012607f796f807d86cf30fffdbcdf2a9bd00cc44ba6e patchBytes=1242 patchLines=26 notesReviewed=yes reviewedAt=2026-01-24T20:02:13+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR08 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Profiles correctness: align experiments/Gemini gating for built-in profiles.

- Subject: fix(profiles): align experimentsEnabled for built-in profiles
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 2 deletions(-)`
- What changed:
  - `components/profiles/ProfilesList.tsx` passes `allowGemini` instead of `props.experimentsEnabled` to the row renderer.
- Why (inferred from context):
  - Ensures built-in profile rows receive the same “Gemini/experiments are allowed” boolean that the UI uses elsewhere, rather than the broader “experiments enabled” flag.
  - Prevents UI inconsistencies where built-in profiles might appear experimental-enabled even when the underlying capability is not allowed.
- Coherence:
  - Matches subject; small correctness fix.

### Verdict

- Keep/squash/split/drop/reorder: keep; likely belongs near other profile picker / built-in profile series changes.
- Depends on: the `allowGemini` computation already existing in `ProfilesList` (it does; this is just wiring).

### Rewrite Commit Message

- Keep subject: `fix(profiles): align experimentsEnabled for built-in profiles`

---

## 057 2026-01-21 ff7bec72358f `secrets`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ff7bec72358f patchFile=/tmp/leeroy-wip-pass2/057.patch patchSha256=f833cd26ae6c7c72e2ccdf89cd904485fb84d23f610c9930e30b4e385b138dc8 patchBytes=5039 patchLines=117 notesReviewed=yes reviewedAt=2026-01-24T20:02:41+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR09 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Secrets list reliability: tighten callback deps/indentation; small follow-up to secrets UI.

- Subject: fix(secrets): tighten callback deps and fix indentation
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `1 file changed, 29 insertions(+), 21 deletions(-)`
- What changed:
  - `components/secrets/SecretsList.tsx`:
    - Destructures props up-front and tightens `useCallback` dependency arrays so callbacks don’t capture the entire `props` object (reduces stale closure risk and avoids unnecessary callback identity churn).
    - Cleans up a small indentation issue in the `SavedSecret` construction.
- Coherence:
  - Single-purpose UI reliability cleanup for the newly introduced secrets list.

### Verdict

- Keep/squash/split/drop/reorder: keep (small but worthwhile).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(secrets): tighten callback deps and fix indentation`

---

## 058 2026-01-21 1bb65f5dd494 `new-session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1bb65f5dd494 patchFile=/tmp/leeroy-wip-pass2/058.patch patchSha256=12b9a312227605d1ea0d8240e4e17af44c7a086708cc93c3324d749e80105e20 patchBytes=1587 patchLines=34 notesReviewed=yes reviewedAt=2026-01-24T20:03:11+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=New-session reliability: fix stuck “secret requirement modal open” guard ref.

- Subject: fix(new-session): avoid stuck secret requirement modal guard
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `1 file changed, 5 insertions(+), 3 deletions(-)`
- What changed:
  - `new/index.tsx`: fixes a subtle state guard bug for the “secret requirement modal is open” ref.
    - Before: the ref was set to `true` before computing `targetEnvVarName`, and the function could early-return with the ref still `true`, effectively blocking future modal opens.
    - After: the ref is only set to `true` once a valid `targetEnvVarName` exists, and it is explicitly reset to `false` on the early-return path.
- Coherence:
  - Small, high-value reliability fix to the new secret requirement flow.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the preceding new-session secret integration commit(s).

### Rewrite Commit Message

- Keep subject: `fix(new-session): avoid stuck secret requirement modal guard`

---

## 059 2026-01-21 f7f81497444d `modal`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=f7f81497444d patchFile=/tmp/leeroy-wip-pass2/059.patch patchSha256=de24ba6a9db7bcf2b17709c54824a22ef25633a072ed5f88997a830411be70e8 patchBytes=25402 patchLines=574 notesReviewed=yes reviewedAt=2026-01-24T20:04:08+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Mixed: WebAlertModal default OK button + big Popover hardening + ConnectionStatusControl portal; must split into modal vs popover vs connection commits within PR02 and reorder to satisfy helper deps.

- Subject: fix(modal): prevent non-dismissible WebAlertModal
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `6 files changed, 368 insertions(+), 36 deletions(-)`
- What changed (high-level):
  - `modal/components/WebAlertModal.tsx`:
    - Treats `config.buttons: []` as “no buttons provided” and falls back to a default `OK` button.
    - Prevents a broken configuration from rendering a modal with no actions (effectively non-dismissible).
  - `components/Popover.tsx` (substantial web + iOS portal hardening):
    - Web: prefers DOM `getBoundingClientRect()` measurement when available, and falls back to DOM when `measureInWindow` is missing/invalid.
    - Web: retries anchor measurement a couple of times when measurements are invalid (avoids a “stuck invisible portal popover until resize” failure mode).
    - Web: uses fixed positioning for the backdrop edge on web to avoid affecting scrollHeight/scrollWidth in modal contexts.
    - Native iOS: adjusts `FullWindowOverlay` pointerEvents based on `backdropEnabled` and `portalOpacity` to avoid “click-through overlay” behavior.
    - Adds `isMountedRef` guard + switches to `useLayoutEffect` for `recompute()` to avoid late/stale portal state updates.
  - `components/Popover.test.ts`:
    - Updates existing assertions to reflect the new fixed backdrop positioning.
    - Adds targeted tests for DOM measurement fallback and retry behavior.
  - `components/ConnectionStatusControl.tsx` + new `ConnectionStatusControl.popover.test.ts`:
    - Enables `portal.native: true` so the popover menu is not constrained to the anchor container width on native.
  - `__tests__/app/new/pick/machine.presentation.test.ts`:
    - Adds a `CommonActions.setParams` mock to match earlier navigation refactors.
- Coherence:
  - The WebAlertModal fix matches the subject, but the commit is materially broader: it bundles a large Popover hardening series + tests + a ConnectionStatusControl portal behavior change.

### Verdict

- Keep/squash/split/drop/reorder: split during history rewrite (strongly recommended).
  - Split candidates:
    - `fix(modal): default OK button when buttons empty`
    - `fix(popover): harden web anchor measurement + fixed positioning`
    - `fix(popover): fix iOS FullWindowOverlay pointerEvents`
    - `test(popover): add DOM measurement fallback/retry tests`
    - `fix(connection): enable native portal for status popover`
- Depends on:
  - A `requireReactNativeScreens` helper existing (added later at 062), so this commit is not self-contained as-is and should be reordered/squashed for bisectability.
  - Earlier navigation refactors that introduced `CommonActions.setParams` usage (tests mock it here).

### Rewrite Commit Message

- If kept as a single commit: rewrite subject to reflect the actual scope (Popover + modal + portal).
- Preferred: split as described above and keep the original subject only for the WebAlertModal change.

---

## 060 2026-01-22 bb09f91de715 `settings`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=bb09f91de715 patchFile=/tmp/leeroy-wip-pass2/060.patch patchSha256=50913ce23f328820d8efa6165396c538c34abe2bab12661a2f7ba2c706c1bbd9 patchBytes=2955 patchLines=74 notesReviewed=yes reviewedAt=2026-01-24T20:04:42+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR09 plannedAt=2026-01-25T12:46:37+0100 manualReviewedForPrPlan=yes notes=Secrets settings robustness: tolerant parsing keeps valid secrets when one entry invalid; keep with secrets feature PR (PR09).

- Subject: fix(settings): keep valid secrets when one entry is invalid
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `2 files changed, 44 insertions(+)`
- What changed:
  - `sync/settings.ts`: `settingsParse` now special-cases the `secrets` field:
    - If `settings.secrets` is an array, it validates each entry with `SavedSecretSchema.safeParse`.
    - Keeps valid entries and drops invalid ones instead of treating the whole `secrets` field as invalid and falling back to defaults.
    - In dev/debug, logs a warning when dropping an invalid secret entry.
  - `sync/settings.spec.ts`: adds a regression test ensuring one malformed secret does not wipe other valid secrets.
- Why it matters:
  - This matches the “tolerant parse” philosophy already used for profiles and prevents accidental data loss in the presence of partially corrupted settings blobs.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the prior settings/secrets schema changes (`SavedSecretSchema`) from the secrets primitives work.

### Rewrite Commit Message

- Keep subject: `fix(settings): keep valid secrets when one entry is invalid`

---

## 061 2026-01-22 765423af52b4 `agent-input`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=765423af52b4 patchFile=/tmp/leeroy-wip-pass2/061.patch patchSha256=4a1ce74a09d5c2f1cdbeddeac4e823dcd4bd37faa8f6a2ee17bb6f0f544ac920 patchBytes=6745 patchLines=114 notesReviewed=yes reviewedAt=2026-01-24T20:05:10+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Permissions safety: canonical next-mode cycling helper + tests; later reconcile with 064 (reverted cycling) into one final implementation.

- Subject: fix(agent-input): cycle permission mode from normalized state
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `3 files changed, 43 insertions(+), 10 deletions(-)`
- What changed:
  - Adds `getNextPermissionModeForAgentFlavor(...)` in `sync/permissionTypes.ts`:
    - Cycles through the correct mode order for `codex|gemini` vs `claude`.
    - Normalizes/clamps the current mode first, so “foreign” modes (e.g. claude-only `plan` while in codex flavor) don’t break cycling.
  - Updates `AgentInput.tsx` Shift+Tab handling to call the new helper using the already-computed `normalizedPermissionMode` instead of re-deriving a per-component mode list from `props.permissionMode`.
  - Adds focused unit tests validating cycling order and the “invalid current mode” clamp behavior.
- Why it matters:
  - Permission mode is a safety-sensitive control. Cycling must be deterministic and flavor-correct even when persisted state contains a mode that’s not valid for the current agent flavor.

### Verdict

- Keep/squash/split/drop/reorder: keep (small, correct, and tested).
- Depends on: permission mode normalization (`normalizePermissionModeForAgentFlavor`) already present.

### Rewrite Commit Message

- Keep subject: `fix(agent-input): cycle permission mode from normalized state`

---

## 062 2026-01-22 e533db1813a5 `popover`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e533db1813a5 patchFile=/tmp/leeroy-wip-pass2/062.patch patchSha256=06650937b0fb3c66a48f1cb6a678ceadfc1a0bcbe43b3eebb02183d52f85641a patchBytes=4238 patchLines=69 notesReviewed=yes reviewedAt=2026-01-24T20:05:39+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Popover infra: add requireReactNativeScreens() CJS helper and centralize web 'fixed' typing cast; must land before 059 for bisectability.

- Subject: fix(popover): add screens cjs helper and fix fixed positioning types
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 14 insertions(+), 4 deletions(-)`
- What changed:
  - Adds `utils/reactNativeScreensCjs.ts`:
    - `requireReactNativeScreens()` wraps a CJS `require('react-native-screens')` so cross-platform modules can import the helper without pulling `react-native-screens` into non-native bundles (callers must only invoke on native).
  - `components/Popover.tsx`:
    - Introduces a single `fixedPositionOnWeb` constant typed as `ViewStyle['position']` (using a controlled `as any` cast) and reuses it for all “web fixed positioning” uses.
    - This centralizes the earlier scattered `('fixed' as any)` casts and makes the intent/type workaround explicit and consistent.
- Coherence:
  - Matches subject; this is an incremental hardening/cleanup step for the Popover portal work.

### Verdict

- Keep/squash/split/drop/reorder: keep, but reorder/squash for bisectability.
- Depends on:
  - Later Popover/iOS overlay work imports `requireReactNativeScreens`; to make history bisect-friendly, this commit should land *before* any commits that import that helper (notably 059).

### Rewrite Commit Message

- Keep subject: `fix(popover): add screens cjs helper and fix fixed positioning types`

---

## 063 2026-01-22 2ed3100311c7 `secrets`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2ed3100311c7 patchFile=/tmp/leeroy-wip-pass2/063.patch patchSha256=7b77ffb2a149148d1285b141b086978059d6a5debd57819b4f67b2e0e364615a patchBytes=14045 patchLines=227 notesReviewed=yes reviewedAt=2026-01-24T20:06:18+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR09 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Secrets/vault privacy: enforce UI hiding when using vault; keep with secrets feature PR and fold i18n key into PR09.

- Subject: fix(secrets): hide values when using secret vault
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `10 files changed, 31 insertions(+), 9 deletions(-)`
- What changed:
  - `EnvironmentVariableCard.tsx` now treats “use secret vault” as an enforced privacy mode:
    - Introduces `hideValueInUi = isSecret || useSecretVault` and uses it consistently for:
      - the lock icon,
      - `secureTextEntry`,
      - suppressing UI warnings/preview sections that would reveal values.
    - When `useSecretVault` is enabled, the component forces the secret toggle on (unless daemon already enforces sensitivity) via `onUpdateSecretOverride(index, true)` and disables editing the secret toggle (`canEditSecret` now false under vault).
    - Improves subtitle copy to distinguish “enforced by daemon” vs “enforced by vault”.
  - Adds the new i18n key `profiles.environmentVariables.card.secretToggleEnforcedByVault` across locales.
- Why it matters:
  - Vault-backed secrets should never be casually exposed in UI previews. This closes a gap where the vault flow could still show plaintext-ish values or allow the user to disable hiding.

### Verdict

- Keep/squash/split/drop/reorder: keep (security/privacy hardening).
- Depends on: the vault/requirements UX introduced in the secrets management commits.

### Rewrite Commit Message

- Keep subject: `fix(secrets): hide values when using secret vault`

---

## 064 2026-01-22 e5848c480522 `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e5848c480522 patchFile=/tmp/leeroy-wip-pass2/064.patch patchSha256=922df47b48ab85bc86e1c6a755752947ed57ec0590af7b2aec084420ff2c16f0 patchBytes=16134 patchLines=400 notesReviewed=yes reviewedAt=2026-01-24T20:07:04+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Mixed overlay + permission cycling hardening: keep in PR02 but split; move final permission cycling logic into PR10, and keep NewSessionWizard onPermissionModeChange propagation with PR26; tests stay with overlay primitives.

- Subject: fix(ui): harden overlays and permission cycling
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `6 files changed, 302 insertions(+), 8 deletions(-)`
- What changed (by area):
  - **New-session wizard wiring**
    - `NewSessionWizard.tsx` now passes `onPermissionModeChange` into `AgentInput`, ensuring permission mode changes (including Shift+Tab cycling) can actually propagate from the input.
  - **AgentInput overlay behavior**
    - Settings popover now opts into web portal mode (`portal={{ web: true }}`), which helps avoid clipping/focus issues when the input is inside overflow-constrained containers on web.
  - **Permission mode cycling (back-and-forth)**
    - This commit reverts the helper-based cycling introduced in `fix(agent-input): cycle permission mode from normalized state` (061) and restores the inline `modeOrder` cycling based on `props.permissionMode`.
    - This indicates a regression or behavior mismatch was found after 061; for history rewrite we should reconcile these two commits into a single “final” implementation (likely: keep the helper + tests, but ensure the wizard passes `onPermissionModeChange` and that cycling uses the correct source of truth).
  - **Overlay/interaction reliability**
    - `ItemRowActions.tsx`: makes `closeThen(...)` more robust by:
      - attempting `InteractionManager.runAfterInteractions(...)` when available, but
      - adding a `setTimeout(..., 0)` fallback to avoid actions being delayed indefinitely by long/continuous interactions.
  - **Tests added/extended**
    - Adds `ItemRowActions.test.ts` (ensures menu actions wiring doesn’t regress under the new popover/overlay plumbing).
    - Adds `OverlayPortal.test.ts` asserting portal node updates do not re-render provider children (important perf invariant for overlay portals).
    - Extends `Popover.nativePortal.test.ts` to assert portal content is wrapped in `FullWindowOverlay` on native when backdrop is enabled (touch interception behavior) and remains non-interactive when backdrop is disabled.

### Verdict

- Keep/squash/split/drop/reorder: keep, but during rewrite reconcile with 061 (these two commits should collapse into one coherent permission-cycling implementation plus the overlay hardening pieces).
- Depends on: the overlay primitives introduced in 039 (`Popover`, `OverlayPortal`) and the new-session wizard refactors that pass AgentInput props (043).

### Rewrite Commit Message

- Keep subject (bucket): `fix(ui): harden overlays and permission cycling`
- During rewrite, consider splitting:
  - `fix(ui): make ItemRowActions closeThen resilient`
  - `test(ui): add OverlayPortal and popover portal coverage`
  - `fix(agent-input): ensure permissionMode changes propagate and cycle correctly` (fold 061+064 into one)

---

## 065 2026-01-13 69fdcff96a4c `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=69fdcff96a4c patchFile=/tmp/leeroy-wip-pass2/065.patch patchSha256=20a7e9d9f2e6a834cd2c443d87fe87477985fb632f02551837797f48a915583c patchBytes=7673 patchLines=130 notesReviewed=yes reviewedAt=2026-01-24T20:07:37+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Permissions persistence: infer session permission mode from last user message + local updatedAt arbitration; safety-critical, keep with PR10.

- Subject: fix(sync): restore session permission mode from last message
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `2 files changed, 56 insertions(+), 5 deletions(-)`
- What changed:
  - Adds a “message-derived permission mode” recovery/inference path so permission mode follows a session across devices without adding new server fields:
    - When processing messages for a session, scans the most recent `user-text` message and reads `message.meta.permissionMode`.
    - If the inferred message timestamp is newer than `session.permissionModeUpdatedAt`, updates the session’s `permissionMode` and sets `permissionModeUpdatedAt` to the message `createdAt`.
    - Persists the inferred mode (non-default only) via `saveSessionPermissionModes(...)` so it loads instantly on app restart.
  - Introduces a local coordination timestamp:
    - `storageTypes.Session` gains `permissionModeUpdatedAt?: number | null`.
    - `updateSessionPermissionMode(...)` now sets `permissionModeUpdatedAt = Date.now()` when the user changes the mode locally, preventing older message inference from overriding a newer local selection.
  - Minor type widening: `permissionMode`/`modelMode` are now allowed to be `null` in `Session` (caller code must handle nullish values).
- Why it matters:
  - Permission mode is safety-critical. This makes the app more consistent across restarts and multi-device usage, and prevents “silent regression to default” when local-only state is lost.

### Verdict

- Keep/squash/split/drop/reorder: keep (safety + UX reliability), but ensure follow-up commits handle edge cases (there are additional permission-mode persistence commits in the queue).
- Depends on: user messages reliably including `meta.permissionMode` when sent; local persistence helpers (`saveSessionPermissionModes`) working correctly.

### Rewrite Commit Message

- Keep subject: `fix(sync): restore session permission mode from last message`

---

## 066 2026-01-13 9b499c5dccce `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9b499c5dccce patchFile=/tmp/leeroy-wip-pass2/066.patch patchSha256=f2d632496716cf934e43cf3a03bef46afcb73ba62a6d3898a6c1f54135484763 patchBytes=7448 patchLines=128 notesReviewed=yes reviewedAt=2026-01-24T20:08:03+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Permissions persistence: persist permissionModeUpdatedAt map for restart-safe arbitration; keep adjacent to 065.

- Subject: fix(sync): persist permission mode timestamp for restart-safe arbitration
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `2 files changed, 39 insertions(+), 2 deletions(-)`
- What changed:
  - Persists `permissionModeUpdatedAt` alongside `permissionMode` so the “message-inferred mode vs locally-selected mode” arbitration survives app restarts.
    - `sync/persistence.ts` adds:
      - `loadSessionPermissionModeUpdatedAts()` / `saveSessionPermissionModeUpdatedAts()`
      - stored under the MMKV key `session-permission-mode-updated-ats`
      - tolerant parse with console error on malformed JSON.
    - `sync/storage.ts`:
      - Loads `sessionPermissionModeUpdatedAts` at init.
      - When merging sessions on first load, uses `existingPermissionModeUpdatedAt ?? savedPermissionModeUpdatedAt ?? null`.
      - When persisting modes (both from message inference and from explicit user updates), also persists the updated-at map.
- Why it matters:
  - Without persisting updated timestamps, a restart would reset arbitration state and allow older message-based inference to override a user’s most recent local selection after relaunch.

### Verdict

- Keep/squash/split/drop/reorder: keep (required follow-up to 065).
- Depends on: 065’s introduction of `permissionModeUpdatedAt` and the inference logic.

### Rewrite Commit Message

- Keep subject: `fix(sync): persist permission mode timestamp for restart-safe arbitration`

---

## 067 2026-01-13 def8852509d0 `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=def8852509d0 patchFile=/tmp/leeroy-wip-pass2/067.patch patchSha256=243a4d97d0be485870473f3400dcddaf3f3c612ab127a56750817644b72cc042 patchBytes=20586 patchLines=422 notesReviewed=yes reviewedAt=2026-01-24T20:09:07+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Permissions cross-device reliability: canonical PERMISSION_MODES + best-effort server time calibration + metadata fallback; keep with PR10.

- Subject: fix(sync): persist permission mode reliably across devices
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `11 files changed, 141 insertions(+), 48 deletions(-)`
- What changed (high-level):
  - This is a follow-up to the permission-mode “infer from last user message” work (065/066) that makes ordering and cross-device consistency more reliable by using a best-effort server-aligned clock and by adding additional fallback signals.
- Key changes:
  - **Canonical permission mode type/constants**
    - Adds `constants/PermissionModes.ts` exporting `PERMISSION_MODES` and `PermissionMode`.
    - Reuses `PERMISSION_MODES` in Zod schemas (`settings`, `typesMessageMeta`, `typesRaw`) to avoid duplicated enum lists diverging over time.
    - Updates `SessionView` to type `updatePermissionMode` as `PermissionMode`.
  - **Best-effort server time calibration**
    - Adds `sync/time.ts`:
      - `observeServerTimestamp(serverTimestampMs)` stores a rolling `serverTimeOffsetMs` (serverNow - Date.now()).
      - `nowServerMs()` returns `Date.now() + offset` (falls back to `Date.now()` until observed).
    - `apiSocket.request(...)` now parses the HTTP `Date` header on responses and calls `observeServerTimestamp(...)` (best-effort only).
    - `sync/sync.ts` uses `nowServerMs()` for local optimistic message timestamps (`createdAt`) so locally-created messages sort correctly against server-created messages across devices.
  - **Permission mode arbitration becomes clock-safe**
    - `storage.updateSessionPermissionMode(...)` now stamps `permissionModeUpdatedAt = nowServerMs()` instead of `Date.now()`, aligning local “last change” with server time for consistent ordering across devices.
    - Message inference now validates `message.meta.permissionMode` against `PERMISSION_MODES` before applying.
  - **Additional fallback: CLI-published metadata**
    - `storageTypes.MetadataSchema` gains optional `permissionMode` and `permissionModeUpdatedAt` fields (published by `happy-cli`).
    - `storage.ts` merge path prefers newer metadata permission mode over local/saved values when `metadataPermissionModeUpdatedAt` is newer than the local arbitration timestamp. This helps “local-only starts” before any app-sent user message includes meta.permissionMode.
  - **Persistence hardening**
    - `loadSessionPermissionModeUpdatedAts()` now filters parsed JSON to `{ [sessionId]: finite number }` and rejects arrays/invalid shapes.
    - Centralizes permission mode persistence into a helper (`persistSessionPermissionData`) and ensures deletion clears both modes and updated-at maps.
- Why it matters:
  - Permission mode is safety-critical and should converge across devices. This commit reduces reliance on local clocks and adds additional signals so the app can reconstruct the correct mode reliably after restarts and across machines.

### Verdict

- Keep/squash/split/drop/reorder: keep (important reliability/safety follow-up).
- Depends on: HTTP responses including a usable `Date` header (best-effort; logic still works without it) and CLI publishing metadata fields where applicable.

### Rewrite Commit Message

- Keep subject: `fix(sync): persist permission mode reliably across devices`

---

## 068 2026-01-12 b1c8a6fecd01 `session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=b1c8a6fecd01 patchFile=/tmp/leeroy-wip-pass2/068.patch patchSha256=24a5192bad1dcf9d42a4810b4e66e4842627b56da16beb6f3bc4a7819f3c8983 patchBytes=5900 patchLines=126 notesReviewed=yes reviewedAt=2026-01-24T20:09:38+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Sessions reliability: archive fallback when kill RPC unavailable; group with sessions/message UX PR.

- Subject: fix(session): archive when kill RPC unavailable
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `4 files changed, 41 insertions(+), 7 deletions(-)`
- What changed:
  - UI “archive” actions in:
    - `app/(app)/session/[id]/info.tsx`
    - `components/ActiveSessionsGroup.tsx`
    - `components/ActiveSessionsGroupCompact.tsx`
    now call `sessionArchive(...)` instead of `sessionKill(...)`.
  - `sync/ops.ts` adds `sessionArchive(sessionId)`:
    - Primary behavior: calls the existing `sessionKill(...)`.
    - Fallback: if kill fails with an “RPC method not available” style error (string match on the message), it sends a best-effort `apiSocket.send('session-end', ...)` to mark the session inactive server-side and returns success.
- Why it matters:
  - When a session has crashed/disconnected, the “kill” RPC can be unavailable; without a fallback, the UI can’t reliably archive sessions and they may appear stuck online.
- Coherence:
  - The diff matches the subject; changes are cohesive and in the right layer (ops wrapper + UI callsites).

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a pragmatic reliability fix.
- Depends on: server handling `session-end` events (best-effort) and the exact error message text for the RPC-unavailable detection (slightly brittle).

### Rewrite Commit Message

- Keep subject: `fix(session): archive when kill RPC unavailable`

---

## 069 2026-01-06 c06b6202b5d4 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c06b6202b5d4 patchFile=/tmp/leeroy-wip-pass2/069.patch patchSha256=2da06e303c3470a4ba84a9c6108e68d93474b45de8034cb6dd9aa0ad48b6a5ed patchBytes=4354 patchLines=130 notesReviewed=yes reviewedAt=2026-01-24T20:15:28+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=MessageView UX: copy-to-clipboard button for message blocks; keep with sessions/message list UX PR.

- Subject: Add copy-to-clipboard button to message blocks
- Reasons: non-Conventional-Commits subject

### Manual Review Notes

- Diffstat: `1 file changed, 84 insertions(+), 2 deletions(-)`
- What changed:
  - `components/MessageView.tsx`:
    - Adds an inline `CopyMessageButton` component that uses `expo-clipboard` to copy message markdown text.
    - Renders the copy button for both `UserTextBlock` and `AgentTextBlock` (copies `displayText || text` for user messages and `text` for agent messages).
    - Provides visual feedback (icon toggles to “check” for ~1.2s) and shows a modal alert on failure.
    - Adds minimal styling for the action row and button.
- Coherence:
  - Feature is cohesive and small. It is “UI convenience” rather than core infra.
  - Minor style note: the new button component is defined inside `MessageView.tsx`; during rewrite we may choose to extract it into `components/CopyMessageButton.tsx` for readability, but it’s not required for functional correctness.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `expo-clipboard` availability and `theme.colors.success` existing in the theme.

### Rewrite Commit Message

- Rewrite subject to Conventional Commits: `feat(expo-app): add copy-to-clipboard button to message blocks`

---

## 070 2026-01-21 9fc7d3d412b9 `sessions`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9fc7d3d412b9 patchFile=/tmp/leeroy-wip-pass2/070.patch patchSha256=c3bb8024d077a6d2124a731b8fdca25b6b61d9f5fbf0a8f6f973bab2399a5473 patchBytes=38406 patchLines=811 notesReviewed=yes reviewedAt=2026-01-24T20:17:34+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T12:50:14+0100 manualReviewedForPrPlan=yes notes=Sessions UX: group inactive sessions by project (setting + view-data builder + tests); keep with sessions/message UX PR and fold i18n keys into it.

- Subject: feat(sessions): group inactive sessions by project
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `16 files changed, 404 insertions(+), 117 deletions(-)`
- What changed (high-level):
  - Adds a new setting: `groupInactiveSessionsByProject` (default `false`) with a toggle in `settings/features.tsx` and i18n strings across locales.
  - Introduces `sync/sessionListViewData.ts`:
    - Extracts session list view shaping into a dedicated pure function.
    - When enabled, groups inactive sessions by `machineId + path` (a “project”), emits a `project-group` header, then lists sessions beneath it.
    - Keeps the legacy “group by date (Today/Yesterday/N days ago)” behavior when disabled.
  - Adds `sync/sessionListViewData.test.ts` covering the project grouping shape.
  - Refactors `sync/storage.ts` and `components/SessionsList.tsx` to use the new view-data builder and to thread the new setting into the derived list data.
- Coherence:
  - The diff matches the subject and is mostly cohesive (one feature + the necessary refactor to support it).
  - Minor note: `sessionListViewData.ts` uses hard-coded English date headers (`Today`, `Yesterday`, `${n} days ago`) which may need i18n follow-up if we want parity with the rest of the app.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: session metadata including `machineId` + `path` for meaningful grouping; otherwise groups collapse under “unknown”.

### Rewrite Commit Message

- Keep subject: `feat(sessions): group inactive sessions by project`

---

## 071 2026-01-21 62a8b1b07c3e `sessions`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=62a8b1b07c3e patchFile=/tmp/leeroy-wip-pass2/071.patch patchSha256=8f985046e31b2f570813303dd7abe624e45ec225c5b3eee071652d1f7bf494ba patchBytes=22794 patchLines=482 notesReviewed=yes reviewedAt=2026-01-24T20:18:40+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Sessions UX: unread badge (persist last-viewed + helper + tests + avatar badge wiring); keep with sessions/message list UX PR.

- Subject: feat(sessions): add unread badge
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `9 files changed, 199 insertions(+), 39 deletions(-)`
- What changed (high-level):
  - Adds persistent “last viewed” state:
    - `sync/persistence.ts` adds `loadSessionLastViewed()` / `saveSessionLastViewed()` under the MMKV key `session-last-viewed`.
    - `sync/storage.ts` loads this map at init and exposes `markSessionViewed(sessionId)` (writes `Date.now()` and persists).
  - Adds unread computation:
    - New `sync/unread.ts` exports `hasUnreadMessages({ lastViewedAt, messages })`.
    - New `useHasUnreadMessages(sessionId)` selects `sessionLastViewed[sessionId]` + message list and computes unread state.
    - Includes `sync/unread.test.ts` for the helper behavior.
  - Wires UI:
    - `-session/SessionView.tsx` calls `storage.getState().markSessionViewed(sessionId)` on session mount.
    - `components/Avatar.tsx` gains `hasUnreadMessages?: boolean` and renders a small badge overlay when true.
    - Active session list components pass unread state to avatars (including the compact project header avatar).
- Coherence:
  - Matches subject and includes meaningful unit tests + persistence wiring.
- Reliability notes:
  - `hasUnreadMessages` only checks the first/last message timestamps (works if messages are consistently sorted asc/desc, but can miss unread if a newer message sits in the middle of an unsorted array). If we ever relax sorting guarantees, switch to a full `Math.max(...)` scan.
  - `markSessionViewed` only runs on session open; if new messages arrive while the session is already open, unread state may not auto-clear unless another path updates lastViewedAt. (May be acceptable for a first cut, but worth noting.)

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a user-visible feature with reasonable persistence and unit tests.
- Depends on: session messages being present and consistently ordered (for the current unread heuristic).

### Rewrite Commit Message

- Keep subject: `feat(sessions): add unread badge`

---

## 072 2026-01-21 82d74454c3c4 `typecheck`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=82d74454c3c4 patchFile=/tmp/leeroy-wip-pass2/072.patch patchSha256=1a8dd521ceab0660a8ba515ae4e40adbd7afdd1fd29a4404b225ef6028f63431 patchBytes=2497 patchLines=43 notesReviewed=yes reviewedAt=2026-01-24T20:19:14+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Typecheck cleanup for permission-mode work (imports/exports); squash into the owning permission commits in PR10 during rewrite (subject mentions Popover but diff is permission typing).

- Subject: fix(typecheck): restore permission imports and Popover web styles
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `3 files changed, 2 insertions(+), 2 deletions(-)`
- What changed:
  - `-session/SessionView.tsx`: restores a missing `PermissionMode` type import (used in the permission mode callback typing).
  - `sync/storage.ts`: imports `PERMISSION_MODES` (used for runtime validation of message meta permission modes).
  - `sync/ops.ts`: removes `SessionArchiveResponse` from an exported type list (likely because it’s no longer defined/used and was causing a TS error).
- Note:
  - The subject mentions “Popover web styles”, but this particular commit doesn’t appear to touch Popover styling. During rewrite, consider renaming the subject/body to reflect the actual diff.

### Verdict

- Keep/squash/split/drop/reorder: keep (small typecheck/compile fix).
- Depends on: earlier commits introducing `PERMISSION_MODES` and using it in storage.

### Rewrite Commit Message

- Rewrite subject to match the diff: `fix(typecheck): restore permission-mode type imports`

---

## 073 2026-01-11 4f2b533f99c2 `tools`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4f2b533f99c2 patchFile=/tmp/leeroy-wip-pass2/073.patch patchSha256=0fe31f7bea6a875d5eba5bd634ad16094893c27f942ce0ec933802836f0bfaf7 patchBytes=17604 patchLines=401 notesReviewed=yes reviewedAt=2026-01-24T20:20:20+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Tool UX: ExitPlanMode approve/reject buttons + i18n; keep in AskUserQuestion/ExitPlan end-to-end PR.

- Subject: feat(tools): add approve/reject buttons to ExitPlanMode
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `10 files changed, 249 insertions(+), 13 deletions(-)`
- Commit message body is detailed and matches the diff intent.
- What changed:
  - `components/tools/views/ExitPlanToolView.tsx` is rewritten to include an interactive UI:
    - “Approve plan” and “Reject” buttons with loading/disabled states.
    - “Response sent” state once the user has responded (also treats `tool.state === 'completed'` as responded).
    - Uses the existing `sessionDeny(...) + sync.sendMessage(...)` pattern (same as `AskUserQuestion`) to close the tool’s permission gate and then send the user’s response into the transcript.
  - Adds new i18n keys under `tools.exitPlanMode.*` across locales.
- Coherence:
  - This commit is cohesive: one UI improvement + necessary translations.
- Notes:
  - Several non-English locales appear to contain English strings for these new keys (likely placeholders). During rewrite we can decide whether to keep as-is, translate properly, or mark as intentionally English-only.
  - There are no dedicated UI tests here; that may be acceptable given existing tool UI coverage, but it’s worth noting.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the semantics of `sessionDeny` for closing tool-permission prompts and the `sync.sendMessage` flow.

### Rewrite Commit Message

- Keep subject: `feat(tools): add approve/reject buttons to ExitPlanMode`

---

## 074 2026-01-07 c86cbd26b040 `queue`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c86cbd26b040 patchFile=/tmp/leeroy-wip-pass2/074.patch patchSha256=a371a5fb976f8323071238d5478e67452f8101074b1e350c72156dec64a3e895 patchBytes=45107 patchLines=1043 notesReviewed=yes reviewedAt=2026-01-24T20:22:09+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Pending queue feature: message send modes + pending messages modal/indicator + protocol/storage; keep in MessageQueueV1/Pending end-to-end PR (clean up theme/i18n during rewrite).

- Subject: feat(queue): add pending message queue UI and send modes
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `12 files changed, 703 insertions(+), 59 deletions(-)`
- What changed (high-level):
  - Adds a new “message send mode” setting (`messageSendMode`) with three options:
    - `agent_queue` (current behavior): write to transcript immediately; agent processes when ready.
    - `interrupt`: abort the current agent turn before sending immediately.
    - `server_pending`: enqueue the user message server-side as “pending” instead of sending it immediately.
  - Adds UI:
    - New settings screen: `app/(app)/settings/message-sending.tsx`.
    - New pending queue UI:
      - `components/PendingQueueIndicator.tsx` (shows “Pending (N)” entry)
      - `components/PendingMessagesModal.tsx` (list/edit/remove/send-now pending messages)
  - Adds state/storage:
    - `sync/storage.ts` gains a per-session pending messages slice + ephemeral `pendingCount` (from server push updates).
    - Adds selector hooks like `useSessionPendingMessages(...)`.
  - Adds sync protocol support:
    - `sync/sync.ts` introduces `submitMessage(...)` that honors `messageSendMode` and implements:
      - `abortSession(...)` + `sendMessage(...)` for `interrupt`
      - `enqueuePendingMessage(...)` + `fetchPendingMessages(...)` + `updatePendingMessage(...)` + `deletePendingMessage(...)` for server-pending
    - Handles socket update payloads of type `pending-queue` to keep `pendingCount` updated.
- Coherence:
  - This is a single “pending message queue + send modes” feature, but it spans UI + storage + sync-protocol changes (expected for this feature).
- Reliability notes:
  - Several new UI strings are hard-coded (not i18n) and some UI elements use fixed colors (e.g. `#007AFF`). For a clean upstream PR, we may want to follow the project’s existing theme + i18n conventions.
  - `PendingMessagesModal` “Send now” deletes the pending message before abort+send; if abort/send fails, that pending item may be lost. In a rewrite, consider deferring deletion until after a successful materialization into the transcript (or keep it until an ACK).

### Verdict

- Keep/squash/split/drop/reorder: keep, but consider splitting UI-only cleanup (theme/i18n) into follow-up commits during history rewrite.
- Depends on: server support for the `pending-*` socket events/acks (`pending-list`, `pending-enqueue`, `pending-update`, `pending-delete`, and `pending-queue` updates).

### Rewrite Commit Message

- Keep subject: `feat(queue): add pending message queue UI and send modes`

---

## 075 2026-01-07 42da6dae8fe4 `session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=42da6dae8fe4 patchFile=/tmp/leeroy-wip-pass2/075.patch patchSha256=0d8e35af710d88633ab53ea25e01f8d2491e5fb7dc977ac4c2204b83eb1d80e2 patchBytes=16372 patchLines=353 notesReviewed=yes reviewedAt=2026-01-24T20:23:19+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Sessions UX: session rename via update-metadata + i18n; keep with sessions/message list UX PR.

- Subject: feat(session): add session rename support
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `11 files changed, 192 insertions(+), 6 deletions(-)`
- What changed:
  - Adds a new quick action in `app/(app)/session/[id]/info.tsx`:
    - “Rename Session” prompt that calls `sessionRename(session.id, newName)` and shows an error alert on failure.
    - Also adds “copy resume command” items for Claude/Codex resume IDs (small related UX improvement).
  - Adds `sessionRename(sessionId, title)` in `sync/ops.ts`:
    - Reads the current session from storage to get `metadataVersion` + metadata.
    - Writes `metadata.summary = { text: title, updatedAt: Date.now() }`, encrypts metadata, and sends an `update-metadata` acked socket event with `expectedVersion`.
  - Adds i18n keys across locales (`sessionInfo.renameSession*`, etc.).
- Coherence:
  - The commit matches the subject and is cohesive (rename support + its UI + translations).
- Notes / potential follow-ups:
  - `sync/ops.ts` has a redundant “Get current session” block that re-calls `getSessionEncryption` (it doesn’t fetch metadata version). This is harmless but should be cleaned up during rewrite.
  - On version mismatch it currently returns “please try again” rather than refetching and retrying; that’s acceptable but not ideal.
  - There is no explicit success toast/modal despite a `sessionRenamed` translation key being added (likely unused right now).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `update-metadata` server behavior and correct metadata encryption for the session.

### Rewrite Commit Message

- Keep subject: `feat(session): add session rename support`

---

## 076 2026-01-12 057102f67904 `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=057102f67904 patchFile=/tmp/leeroy-wip-pass2/076.patch patchSha256=a9dd95c610ad982c66168fab4e09c50f2c2ceb6de30962f5cc166a3e6c677d1b patchBytes=38206 patchLines=864 notesReviewed=yes reviewedAt=2026-01-24T20:27:11+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Resume feature: new-session resume picker + resume payload + capability gating + draft persistence; fold any unrelated Codex-session-id translations into the commit that uses them.

- Subject: feat(resume): add resume session option for new sessions
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `17 files changed, 479 insertions(+), 13 deletions(-)`
- Commit message body describes the intent and mostly matches the diff.
- What changed (high-level):
  - Adds a new “resume session” selection flow to the New Session wizard:
    - `app/(app)/new/pick/resume.tsx` (new screen) lets the user paste/select a resume ID and returns it to `/new` via `resumeSessionId` params.
    - `app/(app)/new/index.tsx` persists `resumeSessionId` (temp draft + persisted draft + route param), and, when supported by the selected agent, includes `resume` in the spawn payload.
    - `app/(app)/new/NewSessionWizard.tsx` allows rendering an extra UI element in the footer (initial implementation passed a `resumePicker` node).
  - Capability gating:
    - Adds/extends `utils/agentCapabilities.ts` with an `AgentType` and `canAgentResume(...)` gating so the UI only shows resume controls for supported agents (Claude-only by default; upstream-friendly).
  - Payload changes:
    - `sync/spawnSessionPayload.ts` adds an optional `resume?: string` field to the spawn RPC params.
  - Draft persistence:
    - `sync/persistence.ts` `NewSessionDraft` gains `resumeSessionId?: string` so the selection survives navigation.
  - Adds i18n keys for the new resume picker UI across locales.
- Coherence:
  - This is a cohesive feature commit (new session resume UI + payload + gating + persistence).
  - Minor mismatch with the body bullet “Expose Codex session id in Session Info”: this commit adds translations for Codex session ID copy UX, but the actual Session Info UI wiring appears in later commits (see 080).
- Implementation details / risks:
  - In this commit, `resumeSessionId` is persisted to `NewSessionDraft` but does not appear to be written back into the temp session data store; `NewSessionData` is extended but the screen only *reads* `tempSessionData?.resumeSessionId`.
  - The resume picker returns the value by dispatching `CommonActions.setParams({ resumeSessionId })` to the previous route’s key via `navigation.getState()`. This should work for normal navigation, but is potentially brittle for deep-links/unusual navigation state (it falls back to `router.back()`).

### Verdict

- Keep/squash/split/drop/reorder: keep; belongs with the broader “resume sessions” topic.
- Depends on: the daemon/server supporting `resume` in spawn/session resume flows (the UI only gates display, not server capability).

### Rewrite Commit Message

- Keep subject: `feat(resume): add resume session option for new sessions`
- During rewrite, consider splitting the “Codex session id translations” into the commit where the Session Info UI actually uses them.

---

## 077 2026-01-14 05532bd65c6b `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=05532bd65c6b patchFile=/tmp/leeroy-wip-pass2/077.patch patchSha256=829d9f8e6db35a17bed895b7a14404c67c9f51fca3c8630aff3452d0eb152f44 patchBytes=10791 patchLines=200 notesReviewed=yes reviewedAt=2026-01-24T20:27:54+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Resume UX: show resume chip under path in AgentInput/new-session; keep adjacent to 076.

- Subject: feat(resume): show resume chip under path in new session
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `3 files changed, 57 insertions(+), 44 deletions(-)`
- What changed:
  - Moves the resume selector UI from an external `resumePicker` footer slot into `AgentInput` itself:
    - `NewSessionWizardFooterProps` changes from `resumePicker?: ReactNode` to `resumeSessionId?: string | null` + `onResumeClick?: () => void`.
    - `AgentInput` gains a “Row 3: Resume selector” chip rendered below the path chip (when not collapsed and when `onResumeClick` is provided).
  - `new/index.tsx` removes the inline `renderResumePicker(...)` implementation and instead passes `resumeSessionId` + `onResumeClick` into `AgentInput`.
- Coherence:
  - Matches subject and is a reasonable refactor for UX consistency (keeps “session context chips” together).
  - Minor formatting nit: the diff introduces a few tab-indented lines in `AgentInput.tsx` (should be normalized during rewrite).

### Verdict

- Keep/squash/split/drop/reorder: keep; should stay adjacent to 076.
- Depends on: `AgentInput` having a stable action bar layout (this commit adds a new row that may need follow-up styling constraints).

### Rewrite Commit Message

- Keep subject: `feat(resume): show resume chip under path in new session`

---

## 078 2026-01-11 cd5771e50881 `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=cd5771e50881 patchFile=/tmp/leeroy-wip-pass2/078.patch patchSha256=307c27d9124dffbb4b057f095eac60ef354f8c8686e2078d9b88b189611d2a90 patchBytes=14375 patchLines=360 notesReviewed=yes reviewedAt=2026-01-24T20:29:19+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Resume feature: initial UI support for resuming inactive Claude sessions (SessionView) + ops wiring + design doc; squash/reorder with 079 so intermediate commits are typecheck-safe and avoid hardcoded strings.

- Subject: feat: resume inactive Claude sessions from UI
- Reasons: non-Conventional-Commits subject

### Manual Review Notes

- Diffstat: `3 files changed, 261 insertions(+), 3 deletions(-)`
- Commit message body includes a design document and largely matches the diff.
- What changed (high-level):
  - Adds `expo-app/INACTIVE_SESSION_RESUME.md` describing the intended end-to-end resume flow (UI → server → daemon → CLI).
  - Updates `-session/SessionView.tsx`:
    - Detects inactive sessions and introduces resumable state (`isResumable`, `isResuming`).
    - When sending a message for an inactive, resumable session, calls `resumeSession(...)` instead of the normal send flow.
    - Adds a profile “fixed per session” modal via `profileId`/`onProfileClick` and a hardcoded English message (not translated).
    - Shows resume status in `connectionStatus` text/pulsing when resuming (`t('session.resuming')`) and uses `t('session.inactiveResumable')`/`t('session.inactiveNotResumable')`.
  - Adds `resumeSession` RPC operation in `sync/ops.ts` (machine RPC). `SessionView.tsx` also starts importing `canResumeSession(...)`/`getAgentSessionId(...)` from `utils/agentCapabilities.ts` (implemented in later commits).
- Coherence:
  - This is the initial end-user “resume inactive sessions” feature landing. It is cohesive for the resume topic.
  - As written here, the UI passes `agentSessionId` across the wire; later commits refactor this away (see 079). During rewrite, squash/reconcile to the final design.
  - The commit message body mentions “translations for session resume states”, but this commit does not add translation entries; it only introduces new `t('session.*')` usages + a hardcoded English modal string.
  - `SessionView.tsx` imports `canResumeSession`/`getAgentSessionId` from `utils/agentCapabilities.ts`, but this commit does not modify that module; it likely relies on later commits to add these exports. During rewrite, this should be squashed/reordered to keep intermediate commits typecheck-safe.

### Verdict

- Keep/squash/split/drop/reorder: keep, but expect to squash with 079+080+076/077 into a single coherent resume topic series.
- Depends on: server/daemon support for the resume RPC path (some parts appear to be design-forward and may rely on downstream changes).

### Rewrite Commit Message

- Rewrite subject to Conventional Commits: `feat(resume): resume inactive Claude sessions from session view`

---

## 079 2026-01-12 456e2ad8e2b8 `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=456e2ad8e2b8 patchFile=/tmp/leeroy-wip-pass2/079.patch patchSha256=1038412aa6be537c540443b64b6437f4eb00d0315c79e4e819e2c68d0eb0a234 patchBytes=10666 patchLines=265 notesReviewed=yes reviewedAt=2026-01-24T20:30:50+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Resume architecture: remove agentSessionId requirement from UI; add agentCapabilities helpers + translations; keep with resume end-to-end PR.

- Subject: refactor(resume): don't require agentSessionId from UI
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `12 files changed, 84 insertions(+), 11 deletions(-)`
- What changed:
  - `sync/ops.ts` / `SessionView.tsx`:
    - Removes `agentSessionId` from the resume request shape; UI resumes by Happy session id and agent type only.
    - This pushes “derive vendor resume id” responsibility to the machine/daemon (more secure + less UI coupling to vendor internals).
    - Likely leaves an unused `getAgentSessionId` import in `SessionView.tsx` (the callsite is removed but the patch does not remove the import); clean up during history rewrite.
  - `utils/agentCapabilities.ts`:
    - Broadens `canAgentResume(...)` to accept unknown strings safely.
    - Adds `SessionMetadata` shape + helpers:
      - `getAgentSessionIdField(...)`, `canResumeSession(...)`, and `getAgentSessionId(...)`.
  - Adds session-state translation keys (`session.resuming`, `session.resumeFailed`, etc.).
    - Noted: Spanish `inactiveResumable` uses `reanundable` (likely a typo for `reanudable`).
- Coherence:
  - Matches subject and is a solid architectural improvement for the resume feature.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be adjacent to 078 and any daemon/server-side follow-ups that implement vendor-id derivation.
- Depends on: daemon-side resume implementation being updated accordingly (otherwise resume would break).

### Rewrite Commit Message

- Keep subject: `refactor(resume): don't require agentSessionId from UI`

---

## 080 2026-01-11 9b0069e3d30c `fork`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9b0069e3d30c patchFile=/tmp/leeroy-wip-pass2/080.patch patchSha256=d0775d17447e5f09b0c0043294929d4b05c4f7a900063c95323c37df142a214b patchBytes=13508 patchLines=247 notesReviewed=yes reviewedAt=2026-01-24T20:32:11+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T12:53:37+0100 manualReviewedForPrPlan=yes notes=Resume (Codex): enable Codex as resumable agent + show/copy Codex session id/commands. Treat as upstreamable by removing “fork-only” gating/notes during rewrite and ensuring semantics are correct for upstream.

- Subject: feat(fork): enable Codex resume support
- Reasons: fork-only behavior (should not go upstream as-is)

### Manual Review Notes

- Diffstat: `12 files changed, 52 insertions(+), 29 deletions(-)`
- What changed:
  - `utils/agentCapabilities.ts` adds `'codex'` to `RESUMABLE_AGENTS` with an explicit “Fork” note.
  - Session Info UX in `app/(app)/session/[id]/info.tsx`:
    - Adds a “Codex Session ID” item with copy-to-clipboard behavior (uses translation keys added earlier).
    - Refactors copy-command handlers (`handleCopyCommand` helper) and unifies “copy resume command” into `happy resume <happySessionId>` (shown only for inactive sessions that have an agent session ID in metadata).
  - `sync/storageTypes.ts` appears to add/validate `codexSessionId` metadata shape (schema alignment).
  - Adds i18n key `sessionInfo.copyResumeCommand` across locales (Codex Session ID translation keys were added earlier in 076).
- Coherence:
  - This is still within the “resume support” topic, but it’s fork-specific (Codex resume enablement) and should be isolated from any upstream PR stack.

### Verdict

- Keep/squash/split/drop/reorder: keep, but isolate into a fork-only branch/PR (or guard with a build flag/env) during history rewrite.
- Depends on: a fork build that actually supports Codex resume semantics (as hinted by the commit message).

### Rewrite Commit Message

- Keep subject (fork-only): `feat(fork): enable Codex resume support`

---

## 081 2026-01-22 4ddaef02210e `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4ddaef02210e patchFile=/tmp/leeroy-wip-pass2/081.patch patchSha256=4898c477cf549a2bf357661b4ade980ba45511fb0c0b93ad24c80baa3e93323d patchBytes=1566 patchLines=43 notesReviewed=yes reviewedAt=2026-01-24T20:33:19+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=Queue/send-mode correctness: omit irrelevant model meta for queued/outgoing messages; keep adjacent to pending queue feature PR.

- Subject: fix(sync): omit model meta for queued messages
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 7 insertions(+), 6 deletions(-)`
- What changed:
  - `sync/sync.ts`:
    - Computes `model` from `(session.metadata.flavor, session.modelMode)`:
      - For Gemini: defaults `modelMode` to `'gemini-2.5-pro'` when unset, and includes `model` in outgoing message meta when `modelMode !== 'default'`.
      - For non-Gemini: omits `model` (`undefined`) from outgoing meta.
    - Switches meta construction to `buildOutgoingMessageMeta({ ... })` and drops `fallbackModel`.
- Coherence:
  - Matches subject; this is a small correctness tweak to avoid sending irrelevant/incorrect model metadata in queued/outgoing messages.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be adjacent to the message queue/send-mode series (074).
- Depends on: `buildOutgoingMessageMeta(...)` being the canonical outbound meta builder (it is used elsewhere).

### Rewrite Commit Message

- Keep subject: `fix(sync): omit model meta for queued messages`

---

## 082 2026-01-12 3fbdc10018fe `tmux`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=3fbdc10018fe patchFile=/tmp/leeroy-wip-pass2/082.patch patchSha256=f2cc8568717d548920cea037c3b6e362272f870517c93ed7e2a86d9115651d03 patchBytes=30647 patchLines=723 notesReviewed=yes reviewedAt=2026-01-24T20:34:54+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=TMUX feature correctness: env parsing + command targeting + tests; commit is mixed (scripts/tests/etc) so split during rewrite to keep tmux fixes isolated.

- Subject: fix: tmux parsing/targeting + review feedback
- Reasons: safety/reliability-sensitive area (process control) + non-Conventional-Commits subject

### Manual Review Notes

- Diffstat: `15 files changed, 153 insertions(+), 127 deletions(-)`
- Commit message body is detailed and includes validation notes (tmux env inspection and manpage checks).
- What changed (high-level):
  - `cli/src/utils/tmux.ts` + `cli/src/utils/tmux.test.ts`:
    - Corrects TMUX env parsing: treats TMUX as `socket_path,server_pid,pane_id`, and prefers `TMUX_PANE` for the pane identifier.
    - Removes incorrect parsing that tried to derive session/window from TMUX.
    - Avoids injecting `-t` targets into tmux commands when a target is already present, and explicitly avoids treating `new-session` as “targetable” (tmux semantics differ).
    - Fixes kill-window targeting to `-t session:window`.
    - Removes env-derived session/window fields from `TmuxSessionInfo` (only keeps `env_pane` + socket path, and relies on tmux query for session/window).
  - Additional review feedback items are included (mixed into the same commit):
    - Aligns `Machine.metadata` typing to runtime (nullable).
    - Tightens script/test behavior (`ripgrep_launcher`, `env-wrapper`, continue-test script) and clarifies failure modes (missing args, signal termination).
    - Adjusts CLI API sending behavior: introduces `ApiSessionClient.canSend(...)` and now returns early (drops the emit) when the socket is disconnected for multiple message/event types (may change socket.io buffering semantics vs prior behavior).
    - Improves resume/continue ergonomics and cancellation: supports `-r` as `--resume` alias and wires SIGINT/SIGTERM to abort offline `claudeLocal` runs.
    - Edits a CLI doc file (`cli/docs/bug-fix-plan-2025-01-15-athundt.md`) rather than deleting it.
- Coherence:
  - The tmux portion is cohesive and important for correctness.
  - The extra “misc scripts/tests/typing” changes make the commit mixed; during rewrite we should consider splitting to keep tmux fixes isolated and bisectable.

### Verdict

- Keep/squash/split/drop/reorder: split during history rewrite (recommended).
  - Split candidates:
    - `fix(tmux): parse TMUX env and prefer TMUX_PANE`
    - `fix(tmux): avoid invalid -t injection and fix kill-window target`
    - `test(tmux): cover TMUX_PANE and socket parsing`
    - Any unrelated script/test/type cleanup as separate `chore`/`test` commits.
- Depends on: tmux semantics and consistent env formatting (this commit documents the assumptions).

### Rewrite Commit Message

- Rewrite subject to Conventional Commits: `fix(tmux): correct TMUX parsing and command targeting`

---

## 083 2026-01-13 7292a29822bb `daemon`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7292a29822bb patchFile=/tmp/leeroy-wip-pass2/083.patch patchSha256=40cb173cb1aa03a44ac0c18b4e07de6b8872336399fb768bf21f7a60daef97ab patchBytes=5066 patchLines=99 notesReviewed=yes reviewedAt=2026-01-24T20:36:02+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=Daemon safety boundary: do not apply CLI active profile to GUI-spawned sessions (prevents env leakage).

- Subject: fix(daemon): do not apply CLI active profile to GUI-spawned sessions
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 5 insertions(+), 54 deletions(-)`
- What changed:
  - `cli/src/daemon/run.ts` removes the “fallback to CLI local active profile” behavior for daemon-spawned sessions.
  - The daemon now only injects profile environment variables when explicitly provided by the caller (typically the GUI), and logs when it skips profile env injection.
- Why it matters:
  - Prevents surprising/unsafe env var leakage from a user’s CLI-local profile into GUI-driven session spawns, and aligns behavior with “GUI must opt-in” expectations.
- Coherence:
  - Matches subject and is a clear reliability/security boundary fix.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: GUI explicitly passing profile env vars when desired.

### Rewrite Commit Message

- Keep subject: `fix(daemon): do not apply CLI active profile to GUI-spawned sessions`

---

## 084 2026-01-13 ce94cfa512ac `session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ce94cfa512ac patchFile=/tmp/leeroy-wip-pass2/084.patch patchSha256=1386819edc6fc6248c2b34ad7ce7260a18396dfd78afa8cbf88eb473d8fc0c87 patchBytes=5925 patchLines=128 notesReviewed=yes reviewedAt=2026-01-24T20:38:18+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=Daemon spawn metadata: thread profileId through spawn pipeline into session metadata; keep near other spawn/profile daemon changes and ensure log redaction (086) lands together.

- Subject: feat(session): persist profileId in session metadata via daemon spawn
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `6 files changed, 33 insertions(+), 5 deletions(-)`
- What changed:
  - Passes `profileId` through the daemon spawn pipeline as a non-secret identity:
    - `cli/src/modules/common/registerCommonHandlers.ts` adds `profileId?: string` to spawn options.
    - `cli/src/daemon/run.ts` exports `options.profileId` via `HAPPY_SESSION_PROFILE_ID` env var (empty string preserved as “no profile”).
    - `cli/src/utils/createSessionMetadata.ts` reads `HAPPY_SESSION_PROFILE_ID` and stores `metadata.profileId` (null when empty, omitted when unset).
    - `cli/src/api/types.ts` adds `profileId?: string | null` to Metadata.
    - `cli/src/api/apiMachine.ts` threads `profileId` into `spawnSession(...)`.
  - Also updates `cli/src/claude/runClaude.ts` to include `profileId` in the metadata when the env var is present (duplicate of createSessionMetadata behavior; likely a safety belt).
- Coherence:
  - This commit is cohesive and clarifies “profile identity” vs “profile contents/env vars”.
- Notes:
  - `cli/src/api/apiMachine.ts` logs spawn params via `JSON.stringify(params)` (may include secrets); later security commits redact this (see 086). During rewrite, we should ensure redaction lands before or together with spawn changes.

### Verdict

- Keep/squash/split/drop/reorder: keep; should stay near other “profiles + spawn” daemon changes (083/085/086).
- Depends on: the server/GUI passing `profileId` through spawn RPCs (and the app using `metadata.profileId` for display).

### Rewrite Commit Message

- Keep subject: `feat(session): persist profileId in session metadata via daemon spawn`

---

## 085 2026-01-13 e58e97fd7ff4 `pr107`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e58e97fd7ff4 patchFile=/tmp/leeroy-wip-pass2/085.patch patchSha256=d61afad1a74bec40de6ef5fd636e9b200d023a14d4216037b207f4886e009b55 patchBytes=5232 patchLines=122 notesReviewed=yes reviewedAt=2026-01-24T20:38:46+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=Daemon spawn hardening + profile schema alignment + env template expansion; reconcile with 090 (startupBashScript removal) during rewrite to avoid schema oscillation.

- Subject: fix(pr107): harden daemon spawn + align profile schema
- Reasons: safety/reliability-sensitive area

### Manual Review Notes

- Diffstat: `3 files changed, 38 insertions(+), 11 deletions(-)`
- What changed:
  - `cli/src/daemon/run.ts`:
    - Restores `await` when writing Codex auth token files (`auth.json`) before spawning (fixes a potential race).
  - `cli/src/persistence.ts`:
    - Aligns profile schema with GUI expectations:
      - Profile `id` accepts non-UUID strings (supports built-in profiles).
      - Adds `startupBashScript?: string` field (persisted; execution handled elsewhere).
      - URL fields (`baseUrl` / `endpoint`) accept either real URLs or `${VAR}` / `${VAR:-default}` template strings.
  - `cli/src/utils/expandEnvVars.ts`:
    - Adds support for `${VAR:=default}` in addition to `${VAR:-default}` when expanding environment variables.
- Notes:
  - `expandEnvVars` supports `${VAR:=default}`, but `URL_OR_TEMPLATE_REGEX` only validates `${VAR}` / `${VAR:-default}` (and the error string doesn’t mention `:=`). If the GUI starts emitting `:=` templates for URL fields, CLI schema validation would still reject them; consider aligning during history rewrite.
- Coherence:
  - All changes relate to daemon spawn robustness and profile schema/env handling; matches subject.

### Verdict

- Keep/squash/split/drop/reorder: keep (especially the restored `await`).
- Depends on: consistent schema alignment between GUI and CLI (this commit explicitly pursues that).

### Rewrite Commit Message

- Keep subject: `fix(pr107): harden daemon spawn + align profile schema`

---

## 086 2026-01-13 86330e263595 `security`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=86330e263595 patchFile=/tmp/leeroy-wip-pass2/086.patch patchSha256=224fab84d0ef8c04aec8a6ed629c18d85daddae88165e4534d53eff01447f39b patchBytes=3404 patchLines=70 notesReviewed=yes reviewedAt=2026-01-24T20:39:34+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=Security: redact spawn secrets from daemon logs; split off/undo fs.writeFile await removal if unintended (keep spawn sequencing safe).

- Subject: fix(security): redact spawn secrets from daemon logs
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `2 files changed, 30 insertions(+), 3 deletions(-)`
- What changed:
  - `cli/src/api/apiMachine.ts`:
    - Replaces `logger.debug(JSON.stringify(params))` with a structured log that omits secret values:
      - logs directory/sessionId/machineId/agent/profileId, plus `hasToken` and env var key counts/names.
      - does **not** log token contents or env var values.
  - `cli/src/daemon/run.ts`:
    - Replaces `logger.debugLargeJson(..., options)` with a redacted object (same idea: omit token/env var values, log only metadata + env var keys).
    - Also changes `await fs.writeFile(..., options.token)` → `fs.writeFile(..., options.token)` (no await).
- Security/reliability notes:
  - The redaction itself is strongly positive (prevents secrets from leaking into daemon logs).
  - Logging env var *names* is usually acceptable, but it may still be sensitive in some environments (e.g., names that encode internal service identifiers). If that’s a concern, log only counts (or a filtered/hashed list) during rewrite.
  - The removal of `await` from `fs.promises.writeFile(...)` is a potential reliability regression:
    - the daemon may proceed to spawn the agent before the token file is fully written, causing intermittent auth failures.
    - Unless there is a deliberate reason, this should likely remain awaited (or replaced with a safe “write then spawn” sequencing strategy).

### Verdict

- Keep/squash/split/drop/reorder: keep the redaction, but during rewrite separate and re-evaluate the `fs.writeFile` await removal (it looks accidental/unrelated to redaction).
- Depends on: logger usage patterns; spawn flows that may pass secrets in `token` / `environmentVariables`.

### Rewrite Commit Message

- Keep subject: `fix(security): redact spawn secrets from daemon logs`
- If splitting during rewrite:
  - `fix(security): redact spawn secrets from daemon logs`
  - (optional) `fix(daemon): await auth token file write before spawning` (if we decide the await removal was unintended)

---

## 087 2026-01-15 a25acab22b64 `socket`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a25acab22b64 patchFile=/tmp/leeroy-wip-pass2/087.patch patchSha256=ed7c94b65ed87f75dc10b59af9dc200f2ced915784eb5334567e70f9ed48a4ce patchBytes=6610 patchLines=170 notesReviewed=yes reviewedAt=2026-01-24T20:41:19+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=CLI reliability: restore offline buffering for socket sends + tests; fix canSend/logSendWhileDisconnected inconsistency for bisectability during rewrite.

- Subject: fix(socket): restore offline buffering for sends
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `3 files changed, 39 insertions(+), 31 deletions(-)`
- What changed:
  - `cli/src/api/apiSession.ts` removes the “hard guard” that refused to `emit('message', ...)` while the socket is disconnected, relying on socket.io client buffering instead.
    - It still keeps a disconnected guard for `sendSessionEvent(...)` (returns early before doing encryption/UUID work), but that guard still calls `this.canSend(...)` even though the `canSend` method is removed in this commit’s diff (possible compile error unless `canSend` exists elsewhere in the file/version; verify during rewrite).
    - Introduces calls to `this.logSendWhileDisconnected(...)` for Codex + ACP messages, but that helper is not defined/added in this diff (likely already exists or is added in a neighboring commit).
  - Adds test coverage in `cli/src/api/apiSession.test.ts` asserting that `sendClaudeSessionMessage(...)` still calls `socket.emit(...)` even when `socket.connected = false`.
  - `cli/src/claude/runClaude.ts` moves abort-controller + signal handler registration outside the `try` block (behaviorally minor; likely to ensure handlers are always registered before `claudeLocal`).
  - Minor related change in `cli/src/claude/runClaude.ts`: moves signal wiring outside the try block (no behavior change intended).
- Coherence:
  - The intent is clear and the tests cover the socket buffering assumption.
  - ⚠️ Potential issue discovered in the diff: `apiSession.ts` still calls `this.canSend(...)` in the “session event” path, but the `canSend` method was removed in this commit. That would make this commit non-typecheckable on its own (and should be corrected/squashed during history rewrite).

### Verdict

- Keep/squash/split/drop/reorder: keep, but fix the `canSend` inconsistency (either restore the helper or remove the remaining call).
- Depends on: socket.io client buffering semantics for `emit` while disconnected.

### Rewrite Commit Message

- Keep subject: `fix(socket): restore offline buffering for sends` (but ensure the commit is bisectable).

---

## 088 2026-01-15 a7d23148c311 `logging`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a7d23148c311 patchFile=/tmp/leeroy-wip-pass2/088.patch patchSha256=0f5bed2f94785e97c26294a861d2e95871e3b1775459989ee1f0f821d6dd42a5 patchBytes=10775 patchLines=280 notesReviewed=yes reviewedAt=2026-01-24T20:42:12+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=CLI logging/privacy: gate debug output, redact templates/defaults, expandEnvVars tests; keep in CLI reliability PR.

- Subject: fix(logging): gate debug output and redact templates
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `6 files changed, 170 insertions(+), 17 deletions(-)`
- What changed:
  - `cli/src/ui/logger.ts`:
    - Makes `logger.debugLargeJson(...)` a true DEBUG-only path (returns immediately unless `process.env.DEBUG` is set).
    - Adds `cli/src/ui/logger.test.ts` to prove it does not write to disk when DEBUG is unset.
  - `cli/src/ui/doctor.ts`:
    - Adds `maskValue(...)` that treats `${VAR}` templates as safe but masks `${VAR:-fallback}` / `${VAR:=fallback}` fallbacks (prevents leaking embedded secrets).
    - Adds `redactSettingsForDisplay(...)` so doctor output prints redacted settings (and deletes legacy `localEnvironmentVariables` if present).
    - Adds `cli/src/ui/doctor.test.ts` for the masking behavior.
  - `cli/src/utils/expandEnvVars.ts` + tests:
    - Treats empty strings as “missing” when using defaults (`:-` / `:=`), reduces value-logging, and adds tests for `:-` and `:=` expansion.
- Coherence:
  - Strong match to subject; this is a meaningful logging/privacy hardening series with good tests.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: consistent use of `maskValue` in doctor output paths (the commit wires it).

### Rewrite Commit Message

- Keep subject: `fix(logging): gate debug output and redact templates`

---

## 089 2026-01-13 ef418bc4dda3 `pr107`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ef418bc4dda3 patchFile=/tmp/leeroy-wip-pass2/089.patch patchSha256=bf155c744980506ceceb29720199a8f0ddcc98737887771636a03accfe55b292 patchBytes=2577 patchLines=50 notesReviewed=yes reviewedAt=2026-01-24T20:42:51+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=CLI: doctor redaction coverage + preserve TMUX_TMPDIR empty-string semantics; keep with CLI reliability/logging hardening.

- Subject: fix(pr107): redact profile secrets in doctor + align tmux tmpDir
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `2 files changed, 12 insertions(+), 8 deletions(-)`
- What changed:
  - `cli/src/persistence.ts`: adjusts `getProfileEnvironmentVariables` tmux tmpDir export:
    - before: only set `TMUX_TMPDIR` when `tmpDir` was truthy (so `''` was dropped).
    - after: sets `TMUX_TMPDIR` when `tmpDir !== undefined`, preserving the “empty string is meaningful” contract (use tmux defaults / “explicitly provided” semantics).
  - `cli/src/ui/doctor.test.ts`: hardens redaction tests:
    - Imports `maskValue` directly.
    - Asserts templates like `${LEAK:-sk-live-secret}` and `${LEAK:=sk-live-secret}` are masked (do not leak embedded defaults).
    - Adds explicit cases for empty string, undefined, and plain secret-ish strings.
- Coherence:
  - Cohesive security + correctness fix set: doctor redaction coverage + tmux env var semantics.

### Verdict

- Keep/squash/split/drop/reorder: keep (small, targeted).
- Depends on: `maskValue` being exported/used by doctor output generation; tmux behavior expecting empty string to be meaningful.

### Rewrite Commit Message

- Rewrite subject to a stable scope (avoid “pr107”): `fix(cli): redact profile secrets in doctor and preserve TMUX_TMPDIR`

---

## 090 2026-01-13 1f90659694be `profiles`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1f90659694be patchFile=/tmp/leeroy-wip-pass2/090.patch patchSha256=11d16d45035d579a8eb333ac99ff09a2f7a3225147dd595843238eb3d8130ffe patchBytes=4699 patchLines=137 notesReviewed=yes reviewedAt=2026-01-24T20:43:32+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T12:56:53+0100 manualReviewedForPrPlan=yes notes=Profiles/daemon schema cleanup: remove unwired startupBashScript + local env cache; squash with 085 to present final stable schema (no oscillation).

- Subject: refactor(profiles): remove unwired startup script and local env cache
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 9 insertions(+), 74 deletions(-)`
- What changed:
  - `cli/src/persistence.ts`:
    - Removes `startupBashScript` from the CLI profile schema (previously persisted but not executed/used).
    - Removes `localEnvironmentVariables` (a CLI-local env var cache) from settings:
      - bumps `SUPPORTED_SCHEMA_VERSION` and adds a migration that deletes the cache.
      - deletes helper APIs that merged local cache values into profile env vars.
- Coherence:
  - Matches subject; this is a cleanup/refactor that reduces hidden state and removes an unused field.
- Notes:
  - This partially conflicts with earlier commits that introduced `startupBashScript` for schema alignment (085). During rewrite we should reconcile to the final desired schema (keep or drop) and ensure the earlier commit doesn’t become a no-op/confusing detour.

### Verdict

- Keep/squash/split/drop/reorder: keep, but squash/reconcile with the earlier schema-alignment steps so history doesn’t oscillate.
- Depends on: settings migration correctness (this commit adds a migration path).

### Rewrite Commit Message

- Keep subject: `refactor(profiles): remove unwired startup script and local env cache`

---

## 091 2026-01-15 7aed8aaee02f `profiles`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7aed8aaee02f patchFile=/tmp/leeroy-wip-pass2/091.patch patchSha256=bd0fd2c41227435a67a7fc7de0f8410764f24a886f6f181cb099cb8f416d7467 patchBytes=11805 patchLines=278 notesReviewed=yes reviewedAt=2026-01-24T20:44:29+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=CLI profile schema alignment: drop provider config objects and migrate to env vars (with tests); keep with daemon/CLI schema-spawn work to present one coherent schema story.

- Subject: refactor(profiles): drop provider config objects
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 124 insertions(+), 75 deletions(-)`
- What changed:
  - `cli/src/persistence.ts`:
    - Removes legacy provider config objects (`openaiConfig`, `anthropicConfig`, `azureOpenAIConfig`, `togetherAIConfig`) from the CLI profile schema.
    - Uses `z.preprocess(...)` to migrate any legacy provider config values into `environmentVariables` during parsing (preserves data and avoids silent drops).
    - Removes now-redundant “provider config → env vars” mapping from `getProfileEnvironmentVariables` (profiles are env-var based only).
  - Adds `cli/src/persistence.profileSchema.test.ts` ensuring:
    - legacy config objects migrate correctly,
    - explicit `environmentVariables` are not overridden by legacy config values.
- Coherence:
  - Strong match to subject; this reduces schema duplication and aligns with the app’s “env vars only” profile model.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the GUI sending env-var based profiles going forward (migration covers older shapes).

### Rewrite Commit Message

- Keep subject: `refactor(profiles): drop provider config objects`

---

## 092 2026-01-15 6b0bfe546d9e `runtime`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=6b0bfe546d9e patchFile=/tmp/leeroy-wip-pass2/092.patch patchSha256=f2911b6ee75672a1cddd503f1e9ab32e95b736288b19eb1218217e972c4aaf14 patchBytes=7229 patchLines=195 notesReviewed=yes reviewedAt=2026-01-24T21:51:01+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=Runtime feature for daemon subprocesses: support bun vs node with tests; keep adjacent to daemon spawn refactors.

- Subject: feat(runtime): support bun for daemon-spawned subprocesses
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `3 files changed, 130 insertions(+), 20 deletions(-)`
- What changed:
  - `cli/src/utils/spawnHappyCLI.ts` introduces a single “runtime invocation builder” for spawning subprocesses with `node` vs `bun`.
  - Allows overriding runtime via `HAPPY_CLI_SUBPROCESS_RUNTIME`.
  - Ensures Node subprocesses include `--no-warnings` / `--no-deprecation` flags (mirrors wrapper behavior); bun subprocesses do not.
  - Adds a helpful “entrypoint missing” error if `dist/index.mjs` is missing.
  - Adds solid unit tests for invocation building and spawn behavior (`spawnHappyCLI.invocation.test.ts`, `spawnHappyCLI.test.ts`).
- Coherence:
  - Matches subject; this is a scoped runtime feature with good tests.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `bun` being installed/available when selected; tests validate selection logic, not installation.

### Rewrite Commit Message

- Keep subject: `feat(runtime): support bun for daemon-spawned subprocesses`

---

## 093 2026-01-15 4071466545ab `rpc`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4071466545ab patchFile=/tmp/leeroy-wip-pass2/093.patch patchSha256=901ae5fe7520f97b4eeb1e33bd3982c80b2bc3488c641697e4653c4290b734a0 patchBytes=2411 patchLines=52 notesReviewed=yes reviewedAt=2026-01-24T21:51:52+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=RPC spawn shape: accept arbitrary env var maps for spawn; keep with daemon spawn schema work.

- Subject: refactor(rpc): accept arbitrary env var maps for spawn
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 12 insertions(+), 14 deletions(-)`
- What changed:
  - `cli/src/modules/common/registerCommonHandlers.ts` loosens `SpawnSessionOptions.environmentVariables` typing from a fixed whitelist to `Record<string, string>`.
  - Documents expected keys (provider keys + tmux knobs) in a comment.
- Coherence:
  - Matches subject; necessary for GUI/CLI interoperability as the GUI can emit provider-specific keys.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: downstream spawn paths accepting and forwarding arbitrary env vars (they do).

### Rewrite Commit Message

- Keep subject: `refactor(rpc): accept arbitrary env var maps for spawn`

---

## 094 2026-01-17 809be82b53e7 `env`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=809be82b53e7 patchFile=/tmp/leeroy-wip-pass2/094.patch patchSha256=21ee926dbf4f14df07d8bb300909dca832ee0c6fb0f7e26b9d52148bd3344522 patchBytes=4976 patchLines=112 notesReviewed=yes reviewedAt=2026-01-24T21:52:35+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=CLI env expansion correctness: implement := assignment semantics; keep adjacent to other expandEnvVars/logging hardening commits.

- Subject: fix(env): implement default assignment semantics
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 36 insertions(+), 5 deletions(-)`
- What changed:
  - `cli/src/utils/expandEnvVars.ts` implements bash-like semantics for `${VAR:=default}`:
    - When missing/empty, uses the default and also assigns it for subsequent `${VAR}` references within the same expansion call (without mutating the real process env).
    - Adds `options.warnOnUndefined` to optionally silence warnings about unresolved vars.
  - Adds a focused unit test proving `${VAR:=default}` is reused within a single value (`'${MISSING:=x}-${MISSING}'`).
- Coherence:
  - Matches subject; builds on earlier “support :=” parsing by adding the crucial assignment behavior.

### Verdict

- Keep/squash/split/drop/reorder: keep; should live adjacent to the earlier `expandEnvVars` changes (085/088).
- Depends on: callers expecting bash-like expansion semantics.

### Rewrite Commit Message

- Keep subject: `fix(env): implement default assignment semantics`

---

## 095 2026-01-17 4c2a671dc3f4 `rpc`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4c2a671dc3f4 patchFile=/tmp/leeroy-wip-pass2/095.patch patchSha256=6aa8fddd2772556524f0568d5a2bccfccdaab0fb4a1df234ae6f8bd33867c558 patchBytes=17576 patchLines=426 notesReviewed=yes reviewedAt=2026-01-24T21:53:26+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR15 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=preview-env daemon handler: security-sensitive env preview with policies/heuristics and thorough tests; keep in preview-env end-to-end PR.

- Subject: feat(rpc): add preview-env handler
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 380 insertions(+)`
- What changed:
  - Adds a new `preview-env` RPC handler in `cli/src/modules/common/registerCommonHandlers.ts`:
    - Accepts keys + optional `extraEnv` and returns “effective env values” after applying `${VAR}` expansion (via `expandEnvironmentVariables`).
    - Includes strong secret-handling controls:
      - policy determined by `HAPPY_ENV_PREVIEW_SECRETS` (`none` | `redacted` | `full`)
      - supports caller-supplied `sensitiveKeys` hints, plus daemon-side forced sensitivity heuristics (regex overridable via `HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX` with safe fallback on invalid regex).
    - Validates key names (`^[A-Z_][A-Z0-9_]*$`) and caps requests to 200 keys.
    - Returns per-key metadata (`isSet`, `isSensitive`, `isForcedSensitive`, `sensitivitySource`, `display`) for UI correctness.
  - Adds `registerCommonHandlers.previewEnv.test.ts` with thorough behavioral coverage (policy modes, expansion correctness, regex override/fallback).
- Coherence:
  - Strong match to subject, and the tests are high value for a security-sensitive feature.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: correct `expandEnvironmentVariables` semantics and a sensible default “secret name” heuristic.

### Rewrite Commit Message

- Keep subject: `feat(rpc): add preview-env handler`

---

## 096 2026-01-15 2973f7fe6861 `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2973f7fe6861 patchFile=/tmp/leeroy-wip-pass2/096.patch patchSha256=3e1f48b2afe0e7844cc032ad624ff3bc7f63a1950862666d53aaccd9557490c6 patchBytes=9238 patchLines=242 notesReviewed=yes reviewedAt=2026-01-24T21:54:17+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR21 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=Codex MCP safety: harden codex/mcp-server detection and avoid stdout payload leaks; keep with codex tool-call/permission correctness PR.

- Subject: fix(codex): harden MCP command detection
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `2 files changed, 148 insertions(+), 51 deletions(-)`
- What changed:
  - `cli/src/codex/codexMcpClient.ts`:
    - Hardens Codex version detection:
      - Before: only matched `codex-cli <semver>`.
      - After: matches `codex-cli` **or** `codex`, optional `v` prefix, case-insensitive.
      - If version output is unexpected but `codex --version` succeeds, it no longer treats Codex as “not installed”; it defaults to the modern subcommand (`mcp-server`) to avoid false negatives.
    - Removes stdout logging of full elicitation request payloads (`console.log(...)`), replacing it with a request handler that only logs via `logger.debug` (and only logs the permission result/error).
    - Refactors elicitation handling into `createCodexElicitationRequestHandler(...)`, taking a handler provider (or function) so it can be unit-tested cleanly and used with `this.permissionHandler` without capturing stale references.
  - `cli/src/codex/codexMcpClient.test.ts`:
    - Adds tests ensuring:
      - elicitation requests do not print payloads to stdout,
      - `codex --version` output like `codex 0.43.0-alpha.5` is accepted and results in spawning the MCP server transport with `command='codex'` and `args=['mcp-server']`.
- Coherence:
  - Cohesive reliability + security fix: correct detection and avoid leaking potentially sensitive tool-call data to stdout.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: Codex CLI supporting `--version` and the appropriate MCP subcommand (`mcp` vs `mcp-server`) for the detected version; permission handler plumbing in the CLI daemon.

### Rewrite Commit Message

- Keep subject: `fix(codex): harden MCP command detection`

---

## 097 2026-01-15 c863db3d5bbc `offline`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c863db3d5bbc patchFile=/tmp/leeroy-wip-pass2/097.patch patchSha256=32bf80f2b0853267c5ae8073e09dc4052cf05cc241006c168614da2cffca5439 patchBytes=4754 patchLines=130 notesReviewed=yes reviewedAt=2026-01-24T21:55:09+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=Resume/offline reliability: safer offline session stub (EventEmitter + aligned surface) with test; keep with resume/offline tooling PR.

- Subject: refactor(offline): make offline session stub safer
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 78 insertions(+), 20 deletions(-)`
- What changed:
  - `cli/src/utils/offlineSessionStub.ts` replaces a plain-object “fake ApiSessionClient” with an `EventEmitter`-based stub class:
    - Implements an explicit `Pick<ApiSessionClient, ...>` contract so the stub stays aligned with the real client surface.
    - Adds a real `RpcHandlerManager` instance (with no-op logger) for callers that expect an RPC manager to exist.
  - Adds `cli/src/utils/offlineSessionStub.test.ts` asserting it supports `on(...)` + `emit(...)` without crashing.
- Coherence:
  - Matches subject: offline mode should not crash due to missing EventEmitter semantics or missing stub fields.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none (purely local stub behavior).

### Rewrite Commit Message

- Keep subject: `refactor(offline): make offline session stub safer`

---

## 098 2026-01-15 0a1a91a631e7 `tmux`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=0a1a91a631e7 patchFile=/tmp/leeroy-wip-pass2/098.patch patchSha256=a82d831612efb8e049226844eb4012087d4937724779ec76fa3c899be2a45af2 patchBytes=20638 patchLines=479 notesReviewed=yes reviewedAt=2026-01-24T21:56:06+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=TMUX feature: per-instance socket path support + quoting/validation hardening + tests; keep in terminal/tmux end-to-end PR.

- Subject: feat(tmux): support per-instance socket path
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 198 insertions(+), 84 deletions(-)`
- What changed (high-level):
  - `cli/src/utils/tmux.ts` expands tmux utilities to support:
    - a per-instance tmux socket path (`-S <socket>`) via `TmuxUtilities` constructor config (and override param).
    - a per-instance tmux command environment (merged into spawn env for `tmux` client invocations).
  - Introduces `normalizeExitCode(code)` so signal-terminated tmux commands (`code === null`) are treated as failure.
  - Hardens parsing/escaping:
    - Adds POSIX-safe quoting helpers for building shell commands.
    - Adjusts session/window identifier validation to allow spaces (while keeping separators unambiguous).
    - Improves parsing helpers to allow spaces in `session:window` output while still enforcing end-of-line matches + name constraints.
  - Refines tmux session selection behavior in `spawnInTmux(...)`:
    - Differentiates `undefined` (use default session name) vs `''` (pick current/most-recent deterministically).
    - Extends return type to include `sessionName`/`windowName` for better caller UX messaging.
    - Switches `new-window` invocation to:
      - explicitly target the session via `-t <sessionName>` (correct option ordering),
      - capture pane PID via `-P -F '#{pane_pid}'`,
      - and avoid quoting/escaping env values passed via `-e KEY=VALUE` (tmux does not do shell parsing for these).
    - Changes global caching from a single `_tmuxUtils` to a keyed cache (`sessionName` + socketPath + tmuxCommandEnv) to support multiple isolated tmux instances.
    - Simplifies `listWindows` to use `-F '#W'` rather than parsing human output.
  - Adds `cli/src/utils/tmux.socketPath.test.ts` asserting `-S <socket>` is used when configured.
- Coherence:
  - Matches subject; this is a cohesive tmux capability upgrade + test.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: tmux supporting `-S` socket isolation and consistent `tmux` client semantics on the target platform.

### Rewrite Commit Message

- Keep subject: `feat(tmux): support per-instance socket path`

---

## 099 2026-01-15 c52227082c2b `tmux`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c52227082c2b patchFile=/tmp/leeroy-wip-pass2/099.patch patchSha256=8fcb1e38cd9e571e2e71d652bef92d1ff58ecb5c94531bca1ea5c12fcf58de10 patchBytes=68578 patchLines=1678 notesReviewed=yes reviewedAt=2026-01-24T21:59:16+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T12:59:52+0100 manualReviewedForPrPlan=yes notes=Large mixed tmux+daemon spawn reliability: TMUX_TMPDIR/env correctness + deterministic session selection + tests; split into tmux vs daemon vs codex/logging pieces during rewrite; keep core tmux/daemon parts under PR18/PR16 as appropriate.

- Subject: fix(tmux): correct env, tmpdir, and session selection
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `21 files changed, 915 insertions(+), 206 deletions(-)`
- Commit message/body vs diff: broadly matches the core tmux intent, but the commit also includes adjacent refactors/tests (Codex MCP params validation, logger test changes, minor utility cleanups). For rewrite, this should be split into a few coherent commits.
- Major changes (tmux + daemon spawn reliability):
  - `cli/src/daemon/run.ts`:
    - Adds `buildTmuxWindowEnv(...)` and `buildTmuxSpawnConfig(...)` to centralize tmux spawn construction.
    - Uses `buildHappyCliSubprocessInvocation(...)` (from `spawnHappyCLI`) to build the agent command tokens rather than constructing a hardcoded `node dist/index.mjs ...` string.
    - Passes tmux subprocess env separately from per-window env:
      - window env: merged daemon env + `extraEnv` (matches non-tmux spawn behavior).
      - tmux command env: includes `TMUX_TMPDIR` when provided so it’s treated as a directory (tmux client env), not conflated with the `-S` socket path.
    - Makes tmux session resolution deterministic when `sessionName` is empty/undefined by using `tmuxResult.sessionName` (and only falling back when needed) when constructing user-facing messages.
    - Restores safe sequencing for Codex auth token persistence:
      - `await fs.writeFile(..., auth.json, token)` (this reverts the non-awaited write introduced in commit 086, avoiding a race).
      - Adds cleanup hooks for the temporary `CODEX_HOME` directory keyed by PID so it can be removed when spawning fails or after session tracking is established.
    - Improves safety of log redaction by truncating the list of env var keys logged in `apiMachine` (logs a sample of up to 20 keys + a “truncated” flag).
  - `cli/src/utils/tmux.*.test.ts` and new `cli/src/utils/tmux.commandEnv.test.ts`:
    - Asserts `TMUX_TMPDIR` is passed via the **tmux subprocess environment** when provided.
    - Tightens spawn mocks/types so tests more accurately reflect node child-process behavior.
    - Preserves failure semantics by treating signal-terminated tmux commands (`exitCode === null`) as non-zero (via `normalizeExitCode`).
    - Updates identifier parsing rules (notably: allows spaces in session/window names but rejects special characters like `@` that would break identifier safety).
  - Adds opt-in integration tests `cli/src/utils/tmux.real.integration.test.ts`:
    - Guarded by `HAPPY_CLI_TMUX_INTEGRATION=1` and designed to use isolated tmux servers so it shouldn’t touch user sessions.
  - Adds targeted daemon tests:
    - `cli/src/daemon/run.tmuxEnv.test.ts` and `cli/src/daemon/run.tmuxSpawn.test.ts` validate env merging + runtime selection (e.g. bun) for tmux spawn config.
- Additional changes mixed into this commit (recommend splitting during rewrite):
  - `cli/src/codex/codexMcpClient.ts`: adds Zod validation for elicitation params before calling the permission handler (deny-by-default on invalid payloads).
  - `cli/src/ui/logger.test.ts`: changes assertions to validate on-disk log output rather than checking append-file calls.
  - Small correctness tweaks (e.g. `createSessionMetadata` env trimming, offline stub test import path).
  - `cli/src/api/apiSession.ts`: reintroduces “emit while disconnected” behavior safely by adding `logSendWhileDisconnected(...)` (logs once per connection) and resetting the flag on reconnect (fixes gaps from commit 087).
  - `cli/src/persistence.ts`: drops `tmuxConfig.updateEnvironment` from the schema and stops exporting `TMUX_UPDATE_ENVIRONMENT` (schema alignment cleanup).

### Verdict

- Keep/squash/split/drop/reorder: keep, but **split during rewrite** (too many concerns bundled).
  - Recommended split:
    1) `fix(tmux): pass TMUX_TMPDIR via tmux env and preserve signal failure semantics`
    2) `refactor(daemon): centralize tmux spawn config and ensure deterministic session selection`
    3) `test(tmux): add command-env and opt-in integration coverage`
    4) `fix(codex): validate elicitation params before permission handling` (if not grouped with other codex work)
    5) `test(logger): assert debugLargeJson writes to log file` (optional)
- Depends on:
  - tmux semantics (client env vs `-S` socket isolation) and the existing `TmuxUtilities` implementation.
  - `buildHappyCliSubprocessInvocation` behavior staying stable across runtimes (node/bun).
  - The earlier Codex MCP changes (096) if we keep the codex portion together.

### Rewrite Commit Message

- Keep bucket subject: `fix(tmux): correct env, tmpdir, and session selection`
- If split, use the subjects suggested above.

---

## 100 2026-01-16 7a01a8e42629 `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7a01a8e42629 patchFile=/tmp/leeroy-wip-pass2/100.patch patchSha256=3a5b6360339afeb6ed3363fb66a332db4d008bee9b3f1abeb2b0ab6bd473924f patchBytes=4948 patchLines=117 notesReviewed=yes reviewedAt=2026-01-24T22:00:22+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T13:02:08+0100 manualReviewedForPrPlan=yes notes=Claude scanner test reliability: align transcript path mapping and reduce flakiness (waitFor); keep with Claude transcript/session scanning PR.

- Subject: test(claude): align sessionScanner path mapping
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 34 insertions(+), 7 deletions(-)`
- What changed:
  - `cli/src/claude/utils/sessionScanner.test.ts`:
    - Sets `process.env.CLAUDE_CONFIG_DIR` to a test-controlled directory so the test and the scanner agree on transcript file locations (via `getProjectPath`).
    - Replaces fixed `setTimeout(...)` sleeps with a small `waitFor(...)` polling helper to reduce flakiness.
    - Restores `CLAUDE_CONFIG_DIR` after the test to avoid leaking env state.
- Coherence:
  - Matches subject; improves reliability of an async scanner test.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `getProjectPath` honoring `CLAUDE_CONFIG_DIR`.

### Rewrite Commit Message

- Keep subject: `test(claude): align sessionScanner path mapping`

---

## 101 2026-01-17 1075a768e551 `rpc`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1075a768e551 patchFile=/tmp/leeroy-wip-pass2/101.patch patchSha256=d999914eba7a69d93fcfd4671ba844a8b853acf80605549454970438f9ba4f41 patchBytes=7789 patchLines=208 notesReviewed=yes reviewedAt=2026-01-24T22:01:26+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T13:02:08+0100 manualReviewedForPrPlan=yes notes=detect-cli daemon handler (PATH scan + tests): foundational for capabilities/diagnostics; keep with capabilities end-to-end PR.

- Subject: feat(rpc): add detect-cli handler
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 159 insertions(+), 2 deletions(-)`
- What changed:
  - Adds a `detect-cli` RPC handler in `cli/src/modules/common/registerCommonHandlers.ts` that:
    - checks whether `claude`, `codex`, and `gemini` are resolvable on the daemon’s `PATH` (without relying on a login shell).
    - returns `path` + a per-cli `{ available, resolvedPath? }` map.
  - Adds `registerCommonHandlers.detectCli.test.ts` using a temporary fake executable to validate detection.
- Notes:
  - Implementation uses a manual PATH scan (`resolveCommandOnPath`) with `fs.access` and PATHEXT handling on Windows (rather than `which`/shell), which matches the “daemon spawn resolution” goal.
- Coherence:
  - Strong match to subject and a good foundation for UI gating and diagnostics.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: daemon `PATH` being representative of how spawns will resolve binaries (this is the whole point of the handler).

### Rewrite Commit Message

- Keep subject: `feat(rpc): add detect-cli handler`

---

## 102 2026-01-21 c988f68ca4d0 `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c988f68ca4d0 patchFile=/tmp/leeroy-wip-pass2/102.patch patchSha256=11ecb4a4e4ea947f09aa214e51377caec83ba779a548578217f2a7ee5cec5ec7 patchBytes=24188 patchLines=527 notesReviewed=yes reviewedAt=2026-01-24T22:11:23+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR30 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=Agent error surfaces: surface Codex/Gemini errors in session UI + add shared CLI formatting helpers/tests; keep separate from modal/overlay primitives PR.

- Subject: feat(ui): surface Codex/Gemini errors in session UI
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `9 files changed, 306 insertions(+), 71 deletions(-)`
- What changed (high-level):
  - Adds `cli/src/utils/formatErrorForUi.ts` (+ tests) to convert unknown throws into a user-visible string with truncation (prefers `Error.stack`).
  - Codex:
    - Adds `formatCodexEventForUi(...)` (+ tests) to turn certain MCP events into status messages.
    - Adds `extractCodexToolErrorText(...)` (+ tests) to display tool response errors when `isError=true`.
    - `cli/src/codex/runCodex.ts` forwards MCP startup/stream/tool errors to both the Ink UI buffer and `session.sendSessionEvent({ type:'message', ... })` so the app UI doesn’t silently hang.
  - Gemini:
    - Extracts and centralizes error formatting into `formatGeminiErrorForUi(...)` (+ tests) so users get actionable messages instead of silent failure.
- Notes / risks:
  - `formatErrorForUi` has a minimum `maxChars` of 1000 even if a smaller value is requested (good for preserving meaningful info; slightly surprising API).
  - `runCodex.ts` now treats `startSession`/`continueSession` tool responses with `isError=true` as “hard failures” and clears the session state immediately; this is likely correct UX-wise, but it is a behavior change worth keeping adjacent to any Codex resume/retry logic during rewrite.
- Coherence:
  - Matches subject; a high-value UX reliability improvement for non-Claude agents.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the session UI rendering `session.sendSessionEvent({ type:'message', ... })` messages as user-visible status lines.

### Rewrite Commit Message

- Keep subject: `feat(ui): surface Codex/Gemini errors in session UI`

---

## 103 2026-01-21 b2269ee11252 `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=b2269ee11252 patchFile=/tmp/leeroy-wip-pass2/103.patch patchSha256=cf9d1c3d1c11b8f947a6829d1151207b278740561e7e9c0c62812f9979e641b2 patchBytes=62570 patchLines=1600 notesReviewed=yes reviewedAt=2026-01-24T22:15:19+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T13:02:08+0100 manualReviewedForPrPlan=yes notes=Claude hook switching/transcript correctness: prefer hook transcript_path, idempotent switch, waitForSessionFound, and tests; large but cohesive, may split into hook-path vs switch-RPC vs tests during rewrite.

- Subject: fix(claude): use hook transcript path across switching
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `18 files changed, 1025 insertions(+), 93 deletions(-)`
- Commit message body is detailed and matches the diff’s major intent: make hook-mode Claude switching + transcript scanning reliable.
- What changed (high-level):
  - Hook transcript path:
    - Prefers hook `transcript_path` for session scanning and `claudeCheckSession` (keeps heuristics as fallback).
    - Emits a UI message when transcript file isn’t available yet (prevents silent/blank state).
  - Hook-mode flag preservation:
    - When hooks are enabled, avoids rewriting/deriving `--continue/--resume/--session-id` via local heuristics; lets Claude/hook server control the session identity.
    - Adds tests guarding these behaviors.
  - Switch RPC hardening:
    - Makes the `switch` RPC idempotent by accepting `{ to: 'local'|'remote' }` and returning `boolean`; retains legacy “toggle” semantics when params are missing.
    - Waits briefly for hook session info before switching to avoid null sessionId/transcript races.
- Misc reliability:
  - KeepAlive reports correct mode.
  - Remote init no longer blocks on a long file wait.
- Additional (diff-confirmed) details worth preserving in rewrite:
  - `Session` now tracks `transcriptPath` derived from hook data (`transcript_path`/`transcriptPath`) and notifies sessionFound callbacks with `{ sessionId, transcriptPath }`.
  - `Session.waitForSessionFound({ timeoutMs, requireTranscriptPath })` is introduced so launchers can avoid switching before session/transcript info exists.
  - `sessionScanner` supports per-session transcript path overrides:
    - initial `transcriptPath` is applied before “mark existing messages as processed” to avoid replaying history from the wrong file.
    - watcher restarts when a session’s resolved file path changes (e.g., hook provides a new transcript path later).
    - optional `onTranscriptMissing` warning fires after `transcriptMissingWarningMs` (default `5000`), and is guarded to avoid spamming.
  - Hook server data shape is widened to accept camelCase keys (`transcriptPath`, `hookEventName`) in addition to snake_case.
- Coherence:
  - Large, but cohesive: all changes serve “reliable switching + transcript discovery in hook mode”.

### Verdict

- Keep/squash/split/drop/reorder: keep, but it’s big; consider splitting during rewrite into:
  - `fix(claude): preserve hook-mode flags and prefer hook transcript_path`
  - `fix(rpc): make claude switch handler idempotent`
  - `test(claude): cover hook transcript and switch races`
- Depends on: hook server/session-start events providing `transcript_path` in time; consistent RPC contract between UI and CLI.

### Rewrite Commit Message

- Keep subject: `fix(claude): use hook transcript path across switching` (or split as suggested above).

---

## 104 2026-01-21 f53cf71d416c `persistence`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=f53cf71d416c patchFile=/tmp/leeroy-wip-pass2/104.patch patchSha256=c6a15de58398ba05150e0c76876ff207f0cb46324c1e763401111140eaf9158d patchBytes=2622 patchLines=63 notesReviewed=yes reviewedAt=2026-01-24T22:16:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T13:02:08+0100 manualReviewedForPrPlan=yes notes=CLI schema cleanup: remove deprecated tmuxConfig + env injection; keep with daemon/CLI spawn/schema alignment PR.

- Subject: refactor(persistence): remove deprecated tmuxConfig
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 18 deletions(-)`
- What changed:
  - `cli/src/persistence.ts` removes:
    - `tmuxConfig` from `AIBackendProfileSchema`
    - the `TMUX_SESSION_NAME` / `TMUX_TMPDIR` injection path in `getProfileEnvironmentVariables`.
- Coherence:
  - Matches subject; removes a legacy schema path in favor of explicit terminal/runtime flags (avoids drift).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: terminal/tmux behavior being driven by the newer explicit terminal spawn options (added in later commits).

### Rewrite Commit Message

- Keep subject: `refactor(persistence): remove deprecated tmuxConfig`

---

## 105 2026-01-21 8dae65f6c585 `rpc`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8dae65f6c585 patchFile=/tmp/leeroy-wip-pass2/105.patch patchSha256=2c7d6936b9a9bb12b20a6419899b34039e91bfcb58076ee36ea8189da7804a87 patchBytes=17844 patchLines=416 notesReviewed=yes reviewedAt=2026-01-24T22:16:51+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T13:02:08+0100 manualReviewedForPrPlan=yes notes=detect-cli enrichment: include tmux + version parsing + optional login status + tests; keep with capabilities/diagnostics PR and order after 101.

- Subject: feat(rpc): include tmux in detect-cli
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 302 insertions(+), 6 deletions(-)`
- What changed:
  - Extends `detect-cli` RPC response to include:
    - per-CLI `version?: string` (best-effort semver extraction)
    - optional `isLoggedIn?: boolean | null` when `includeLoginStatus` is requested (best-effort, command-specific)
    - a new `tmux` entry with `{ available, resolvedPath?, version? }`.
  - Adds helper utilities:
    - safe “first line” extraction and version parsing (`extractSemver`, `extractTmuxVersion`)
    - best-effort `execFile` wrappers with timeouts and non-throwing behavior.
    - Windows nuance: `.cmd/.bat` are invoked via `cmd.exe /c` for probes (`--version` / `-V` / login status) instead of direct `execFile`.
  - Tests:
    - expands `registerCommonHandlers.detectCli.test.ts` to validate version parsing and optional login status behavior using fake scripts.
  - Also updates `SpawnSessionOptions` to prefer `terminal?: TerminalSpawnOptions` over legacy `TMUX_*` env vars (doc-level shape change, consumed in later tmux/terminal commits).
- Coherence:
  - Matches subject; this turns detect-cli into a more complete diagnostics primitive for UI gating.
- Security/reliability note:
  - This handler executes resolved binaries (`--version` / `login status`) with a timeout. That’s acceptable for local diagnostics but should remain strictly best-effort and never block/throw in production control paths.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: CLI `--version` output containing recognizable versions; tmux `-V` output format.

### Rewrite Commit Message

- Keep subject: `feat(rpc): include tmux in detect-cli`

---

## 106 2026-01-21 ad66c6aaa3ae `terminal`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ad66c6aaa3ae patchFile=/tmp/leeroy-wip-pass2/106.patch patchSha256=3d6e395250cebf51ffe4f82a279b4407665c893d55d42dee5bd7a69c541c82a0 patchBytes=47830 patchLines=1371 notesReviewed=yes reviewedAt=2026-01-24T22:18:04+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T13:02:08+0100 manualReviewedForPrPlan=yes notes=Terminal feature: add metadata + attach tooling + headless tmux launcher + runtime flags and strong tests; large, consider splitting into metadata vs attach command vs runtime flags during rewrite.

- Subject: feat(terminal): add tmux metadata + attach tooling
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `23 files changed, 1036 insertions(+), 11 deletions(-)`
- What changed (high-level):
  - Adds session terminal metadata:
    - `cli/src/api/types.ts` adds `Metadata.terminal` describing runtime mode (`plain|tmux`), requested mode, fallback reasons, and tmux target/tmpDir (non-secret).
    - `cli/src/claude/runClaude.ts` builds `terminal` metadata from internal terminal runtime flags and persists best-effort local attachment info once a session ID exists.
    - When tmux was requested but unavailable, sends a UI-facing status message into the session transcript (fallback reason).
    - `cli/src/codex/runCodex.ts` and `cli/src/gemini/runGemini.ts` mirror the same “persist attachment info + fallback message” behavior once a real session ID exists.
  - Adds attach workflows + utilities:
    - New `happy attach` command (planning + attach selection behavior).
    - Adds terminal planning modules (`terminalAttachPlan`, `tmuxSessionSelector`, `terminalAttachmentInfo`, etc.) with a strong test suite.
  - Adds headless tmux launcher support (`--tmux`) and internal terminal runtime flags plumbing for spawners (daemon/tmux wrapper).
    - Internal runtime flags are explicitly parsed + stripped before CLI subcommand parsing (`--happy-terminal-*`, `--happy-tmux-*`) to avoid leaking internal implementation details to users.
    - Attachment info is persisted per-session under `happyHomeDir/terminal/sessions/<sessionId>.json` using a `*.tmp` write + `rename` (atomic on POSIX).
- Coherence:
  - Large but coherent: all changes support “spawn + attach” workflows and consistent UI/metadata reporting.

### Verdict

- Keep/squash/split/drop/reorder: keep, but it’s big; during rewrite consider splitting into:
  - metadata + persistence (session terminal info)
  - attach command + planning utils
  - tmux headless launcher + runtime flags
- Depends on: downstream server/UI consuming `metadata.terminal` for “Session Details” and attach UX; tmux availability for tmux mode.

### Rewrite Commit Message

- Keep subject: `feat(terminal): add tmux metadata + attach tooling`

---

## 107 2026-01-21 c13d51ee4638 `daemon`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c13d51ee4638 patchFile=/tmp/leeroy-wip-pass2/107.patch patchSha256=f1b4d85a17e6b6eb2e3b3623b47a9ce05e4634756a9f584ed2873eb0693ed53a patchBytes=28540 patchLines=665 notesReviewed=yes reviewedAt=2026-01-24T22:18:59+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T13:02:08+0100 manualReviewedForPrPlan=yes notes=Daemon: tmux spawn support + runtime/backoff hardening; keep adjacent to terminal/tmux series and ensure terminal options forwarding stays consistent.

- Subject: feat(daemon): support tmux spawn + harden runtime
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `8 files changed, 324 insertions(+), 74 deletions(-)`
- What changed (high-level):
  - Extends spawn plumbing to accept terminal options:
    - `cli/src/api/apiMachine.ts` threads `terminal` through the `spawn-happy-session` handler into `spawnSession(...)` (and logs it).
    - Adds `cli/src/api/apiMachine.spawnSession.test.ts` ensuring terminal spawn options are forwarded.
  - Adds daemon tmux spawn support:
    - Updates `cli/src/daemon/run.ts` to spawn sessions in tmux when requested (including isolated tmux server option) and to record tmux target/fallback metadata.
    - Uses `resolveTerminalRequestFromSpawnOptions` (typed `terminal` config preferred; legacy `TMUX_*` respected as fallback) and strips `TMUX_SESSION_NAME` / `TMUX_TMPDIR` from the child env (daemon/runtime concerns, not agent-runtime env).
    - Resolves empty tmux session names (“current/most recent”) deterministically via `tmux list-sessions` + `selectPreferredTmuxSessionName`; builds a per-window target (`<session>:happy-<timestamp>-<agent>`).
    - On fallback to plain spawn when tmux was requested, injects internal runtime flags (`--happy-terminal-mode plain --happy-terminal-requested tmux --happy-terminal-fallback-reason ...`) so the started CLI can surface the reason + metadata.
  - Runtime hardening:
    - Adds a no-op short-circuit when machine metadata updates do not change (avoids unnecessary writes); note this uses `JSON.stringify` equality which assumes stable key ordering.
    - Makes `ApiMachineClient.connect` accept an optional best-effort `onConnect` hook (errors ignored) and uses it in the daemon to refresh machine metadata once after connect.
    - On macOS, prefers a more user-friendly/stable host name via `scutil --get HostName|LocalHostName|ComputerName` with short timeouts.
    - Logging is made strictly non-fatal (even with `DEBUG=1`): first file write failure is surfaced once, then silenced.
    - Backoff utilities are hardened: delay clamps/jitter avoids <minDelay, default `maxFailureCount` is reduced to `8`, adds `shouldRetry` gates (`retryable:false` / `canTryAgain:false`) and introduces `backoffForever` for “keep retrying” flows (`InvalidateSync` now uses `backoffForever`).
- Coherence:
  - Matches subject; this is the daemon-side counterpart to the terminal/attach tooling series.

### Verdict

- Keep/squash/split/drop/reorder: keep; should stay adjacent to 106 and the tmux spawn series (099/098).
- Depends on: tmux spawn helpers and terminal runtime flags added in the surrounding commits; server/GUI passing `terminal` options.

### Rewrite Commit Message

- Keep subject: `feat(daemon): support tmux spawn + harden runtime`

---

## 108 2026-01-21 f7db44dea458 `docs`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=f7db44dea458 patchFile=/tmp/leeroy-wip-pass2/108.patch patchSha256=f5fd09d2bb72379dfd58d163dae1326f5eb30d40f128b29dee41121a0b595514 patchBytes=524 patchLines=18 notesReviewed=yes reviewedAt=2026-01-24T22:20:24+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T13:02:08+0100 manualReviewedForPrPlan=yes notes=Docs-only: add cli AGENTS.md symlink; keep in Foundations/DevX.

- Subject: docs: add AGENTS.md symlink
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+)`
- What changed:
  - Adds `cli/AGENTS.md` as a symlink to `cli/CLAUDE.md` to keep agent guidance single-sourced.
- Coherence:
  - Matches subject; documentation plumbing only.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: symlink behavior on the target platforms and repo policies (some tooling/windows checkouts may not preserve symlinks).

### Rewrite Commit Message

- Keep subject: `docs: add AGENTS.md symlink`

---

## 109 2026-01-21 af5000ea6318 `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=af5000ea6318 patchFile=/tmp/leeroy-wip-pass2/109.patch patchSha256=60b9f67574d7599d92a182dfa55f618b98bd11e5ce8fee5403d42b1f5de1d361 patchBytes=3587 patchLines=75 notesReviewed=yes reviewedAt=2026-01-24T22:20:50+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR30 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=Agent error surfaces follow-up: polish Codex/Gemini error message formatting + tests; keep adjacent to 102.

- Subject: fix(ui): polish Codex and Gemini error messages
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `4 files changed, 17 insertions(+), 5 deletions(-)`
- What changed:
  - Codex: improves `formatCodexEventForUi` fallback text for MCP startup failures when `error` is missing, and adds a regression test.
  - Gemini: avoids printing an empty “Quota resets in .” suffix when reset duration parsing captures no tokens, and adds a regression test.
- Coherence:
  - Matches subject; small UX correctness tweaks with tests.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(ui): polish Codex and Gemini error messages`

---

## 110 2026-01-21 f68552d03ab9 `tmux`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=f68552d03ab9 patchFile=/tmp/leeroy-wip-pass2/110.patch patchSha256=1a6a3abf018cc15fd95308d7fcabfefbf83bbb49904de901318da0a125378761 patchBytes=3791 patchLines=109 notesReviewed=yes reviewedAt=2026-01-24T22:21:22+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=TMUX UX: print attach instructions in correct order (inside vs outside tmux) + tests; keep with terminal/tmux end-to-end PR.

- Subject: fix(tmux): print attach instructions in correct order
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `2 files changed, 82 insertions(+), 3 deletions(-)`
- What changed:
  - `cli/src/terminal/startHappyHeadlessInTmux.ts` prints attach instructions differently depending on whether we are already inside tmux:
    - inside tmux: `tmux select-window -t <target>`
    - outside tmux: first `tmux attach -t <session>`, then `tmux select-window -t <target>`
  - Adds `startHappyHeadlessInTmux.test.ts` covering both cases.
- Coherence:
  - Matches subject; improves UX and includes good tests.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `TMUX` env var being the correct “inside tmux” indicator.

### Rewrite Commit Message

- Keep subject: `fix(tmux): print attach instructions in correct order`

---

## 111 2026-01-21 4e4bb65ff3f0 `tmux`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4e4bb65ff3f0 patchFile=/tmp/leeroy-wip-pass2/111.patch patchSha256=71887a7db86be20d1e41c337bb2a58f0130acdb056c1ebd9a0f8786c5e15403e patchBytes=3004 patchLines=68 notesReviewed=yes reviewedAt=2026-01-24T22:21:47+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=TMUX test robustness: avoid brittle assertions for attach instructions; keep adjacent to 110.

- Subject: test(tmux): avoid brittle assertions
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 15 insertions(+), 14 deletions(-)`
- What changed:
  - `cli/src/terminal/startHappyHeadlessInTmux.test.ts`:
    - Stops reassigning `process.env` wholesale (restores only `TMUX` key).
    - Replaces exact `console.log` call equality assertions with “contains substring” checks and ordering checks (attach appears before select-window).
- Coherence:
  - Matches subject; makes tests less brittle to formatting/emoji/spacing.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `test(tmux): avoid brittle assertions`

---

## 112 2026-01-21 45f4eb5d145c `detect-cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=45f4eb5d145c patchFile=/tmp/leeroy-wip-pass2/112.patch patchSha256=978de92bacd197cdf02e40b59b067b1eaf67fc17a05b1f04dd6eb9bb41578777 patchBytes=1773 patchLines=49 notesReviewed=yes reviewedAt=2026-01-24T22:22:13+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=detect-cli test hygiene: avoid process.env reassignment; keep with capabilities/detect-cli PR.

- Subject: test(detect-cli): avoid process.env reassignment
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 22 insertions(+), 3 deletions(-)`
- What changed:
  - `cli/src/modules/common/registerCommonHandlers.detectCli.test.ts`:
    - Avoids copying/overwriting the entire `process.env` object.
    - Restores only `PATH` and `PATHEXT` across tests (more compatible with node runtime and avoids breaking other tests).
- Coherence:
  - Matches subject; test hygiene improvement.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `test(detect-cli): avoid process.env reassignment`

---

## 113 2026-01-21 51782fdbfbcd `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=51782fdbfbcd patchFile=/tmp/leeroy-wip-pass2/113.patch patchSha256=16f8931d350cde0a26d3b46e49c920e9b4d58f19ba99020a3604f9e7a19d9253 patchBytes=889 patchLines=21 notesReviewed=yes reviewedAt=2026-01-24T22:22:40+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR21 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=Codex MCP client test isolation: reset shared transport instances deterministically; keep with Codex MCP tool-call correctness PR.

- Subject: test(codex): reset transport instances between tests
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `cli/src/codex/codexMcpClient.test.ts` resets the shared `__transportInstances` array via `length = 0` instead of `splice(0)`.
- Why it matters:
  - Ensures deterministic test isolation without allocating/returning spliced contents; simplest way to clear an array in-place.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the test’s module-level singleton `__transportInstances`.

### Rewrite Commit Message

- Keep subject: `test(codex): reset transport instances between tests`

---

## 114 2026-01-21 0ad76f4877ae `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=0ad76f4877ae patchFile=/tmp/leeroy-wip-pass2/114.patch patchSha256=1bf8701713c523749d310b281e5d6d661dd0a1ac56387921bd72f69da58384b6 patchBytes=796 patchLines=23 notesReviewed=yes reviewedAt=2026-01-24T22:23:08+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=CLI code style: whitespace/indentation fix in Gemini daemon start block (no behavior); squash into nearby CLI changes if desired.

- Subject: chore(cli): fix gemini daemon block indentation
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 2 insertions(+), 2 deletions(-)`
- What changed:
  - `cli/src/index.ts` fixes indentation/whitespace in the Gemini daemon start block (no behavior change).
- Coherence:
  - Matches subject.

### Verdict

- Keep/squash/split/drop/reorder: keep; can be squashed into a nearby `cli/src/index.ts` change if desired.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `chore(cli): fix gemini daemon block indentation`

---

## 115 2026-01-21 a8093e3e4ddb `persistence`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a8093e3e4ddb patchFile=/tmp/leeroy-wip-pass2/115.patch patchSha256=3420822641f5afa625dc3e0ba74db3ba10a8626334544a4792bde9eb4374a63c patchBytes=926 patchLines=21 notesReviewed=yes reviewedAt=2026-01-24T22:23:33+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR05 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=Docs-only: update profile schema version comment in persistence module; keep with persistence PR (or squash into adjacent persistence changes).

- Subject: docs(persistence): update profile schema version comment
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `cli/src/persistence.ts` updates a comment to remove outdated references (purely documentation).
- Coherence:
  - Matches subject.

### Verdict

- Keep/squash/split/drop/reorder: keep; low-risk docs-only change.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `docs(persistence): update profile schema version comment`

---

## 116 2026-01-21 b16c3658b944 `gemini`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=b16c3658b944 patchFile=/tmp/leeroy-wip-pass2/116.patch patchSha256=10f3b6671931c4fea61fa9a5457164e7631cb88d0742e69d1f5b95a79aa0874c patchBytes=1280 patchLines=24 notesReviewed=yes reviewedAt=2026-01-24T22:24:11+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR30 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=Agent error formatting robustness: normalize Gemini errorDetails shape for consistent UI checks; keep with Codex/Gemini error surface PR.

- Subject: fix(gemini): normalize error details for UI
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 4 insertions(+), 1 deletion(-)`
- What changed:
  - `cli/src/gemini/utils/formatGeminiErrorForUi.ts`:
    - Normalizes `errorDetails` when it comes as an array (joins lines, stringifies non-string entries) instead of assuming a string.
    - Ensures downstream string `.includes(...)` checks behave consistently.
- Coherence:
  - Matches subject; small robustness fix for UI-facing error formatting.

### Verdict

- Keep/squash/split/drop/reorder: keep; belongs with the Gemini error-formatting series (102/109).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(gemini): normalize error details for UI`

---

## 117 2026-01-21 d9591372655f `common`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d9591372655f patchFile=/tmp/leeroy-wip-pass2/117.patch patchSha256=ace20816f9d2755efdc5a488bee70435c87d582e160182818d5bcbc48a24bcf0 patchBytes=1230 patchLines=22 notesReviewed=yes reviewedAt=2026-01-24T22:24:48+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=detect-cli robustness: handle execFileAsync non-zero exit codes across node/platform shapes; keep adjacent to detect-cli handler series.

- Subject: fix(common): handle execFileAsync exit codes
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 2 insertions(+), 1 deletion(-)`
- What changed:
  - `cli/src/modules/common/registerCommonHandlers.ts` improves `detectCliLoginStatus`:
    - `execFileAsync` throws on non-zero exit; the code now checks exit code via `error.status` / `error.exitCode` / `error.code` to be resilient across node/platform variations.
- Coherence:
  - Matches subject; small correctness hardening.

### Verdict

- Keep/squash/split/drop/reorder: keep; should stay adjacent to the detect-cli series (101/105/112).
- Depends on: node’s error shape for child-process failures.

### Rewrite Commit Message

- Keep subject: `fix(common): handle execFileAsync exit codes`

---

## 118 2026-01-22 ab95e8cf920c `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ab95e8cf920c patchFile=/tmp/leeroy-wip-pass2/118.patch patchSha256=66ac52b7e573865bf218d966b913633fb1087b1c31792efcb0273a2094486141 patchBytes=5426 patchLines=160 notesReviewed=yes reviewedAt=2026-01-24T22:25:33+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR27 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=Sync reliability: ensure invalidateAndAwait resolves on command errors (no hangs) + add tests; keep in sync robustness PR.

- Subject: fix(sync): avoid hanging invalidateAndAwait on errors
- Reasons: safety/reliability-sensitive area (prevents deadlocks/hangs)

### Manual Review Notes

- Diffstat: `3 files changed, 84 insertions(+), 11 deletions(-)`
- What changed:
  - `cli/src/utils/sync.ts` refactors `InvalidateSync`:
    - Replaces `backoffForever` with an injectable backoff (`BackoffFunc`) defaulting to `createBackoff(...)`.
    - Adds `onError` callback support and tracks failure count.
    - Critical fix: catches failures so `invalidateAndAwait()` always resolves pending awaiters even when the command throws; previously it could hang forever.
    - Behavior change (important): retries are now bounded by `createBackoff`’s default `maxFailureCount` (currently 8); on terminal failure it invokes `onError` in the catch and resolves awaiters instead of retrying forever.
    - Avoids unhandled promise chains by `void`-ing `_doSync()` calls.
  - Adds `cli/src/utils/sync.test.ts` validating:
    - `invalidateAndAwait` resolves even when the command throws.
    - A second invalidate while the first run is in-flight triggers a second run.
  - `cli/src/claude/utils/sessionScanner.ts` passes an `onError` handler to avoid log spam and capture the first failure.
- Coherence:
  - Matches subject; this is a high-value reliability fix with strong unit tests.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `createBackoff` semantics; callers expecting “best effort” sync behavior rather than permanent hang.

### Rewrite Commit Message

- Keep subject: `fix(sync): avoid hanging invalidateAndAwait on errors`

---

## 119 2026-01-22 6f98c95fb5d8 `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=6f98c95fb5d8 patchFile=/tmp/leeroy-wip-pass2/119.patch patchSha256=838e1cf143030a5f2593857ae404888f883c6e6b4b7ebd8fb80d3201c9632390 patchBytes=35603 patchLines=855 notesReviewed=yes reviewedAt=2026-01-24T22:27:03+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=Claude session continuity: support -c/-r flags, CLAUDE_CONFIG_DIR consistency, and avoid session loss on remote/local switch with strong tests; keep with Claude transcript/session handling PR (after 103).

- Subject: fix(claude): support -c/-r and avoid session loss on switch
- Reasons: safety/reliability-sensitive area (session continuity)

### Manual Review Notes

- Diffstat: `13 files changed, 496 insertions(+), 60 deletions(-)`
- What changed (high-level):
  - Claude flag + path handling:
    - Treats `-c` as `--continue` and `-r` as `--resume` for session selection logic.
    - Threads a `CLAUDE_CONFIG_DIR` override through the path helpers so local session discovery and transcript scanning are consistent with the actual Claude config dir used at runtime.
  - Switch/session continuity:
    - Clears `session.sessionId` and `session.transcriptPath` before spawning a local resume session (tests assert this), avoiding stale IDs/transcripts causing session-loss when hooks “fork” a resume into a new session ID.
    - If local spawn fails before Claude reports the forked session, restores the previous `sessionId/transcriptPath` so remote mode can still resume the last-known session (prevents “session lost” dead-ends on fast switch).
    - `Session.clearSessionId()` now clears `transcriptPath` as well (test added), so “new session boundary” is consistent.
    - Remote launcher correctness: tracks `previousSessionId` across iterations and uses a `forceNewSession` flag on reset to avoid double parent-chain resets when the session ID changes mid-run (system init / resume / fork / compact).
  - Remote `--continue`/`--resume` semantics:
    - Avoids discarding a resume session just because `claudeCheckSession` fails transcript validation early (important for fast switch when transcript may exist but not contain UUID/message lines yet).
    - Honors `--continue/-c` by passing `continue: true` to the Claude SDK.
    - Treats `--resume/-r` without an ID as “resume last session” by using `claudeFindLastSession(...)` (non-interactive remote mode).
  - Adds/extends many launcher/session tests around switch idempotency and transcript/sessionId races.
  - Refactors:
    - `getProjectPath(...)` and `claudeFindLastSession(...)` accept an explicit claude config dir override.
- Coherence:
  - Matches subject; this is a cohesive follow-up to 103 that tightens correctness around continue/resume and switching behavior.

### Verdict

- Keep/squash/split/drop/reorder: keep; belongs immediately after 103 in a “Claude switching + transcript” topic cluster.
- Depends on: hook-mode behavior that can change sessionId on resume; correctness of CLAUDE_CONFIG_DIR overrides.

### Rewrite Commit Message

- Keep subject: `fix(claude): support -c/-r and avoid session loss on switch`

---

## 120 2026-01-16 9489d70848f3 `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9489d70848f3 patchFile=/tmp/leeroy-wip-pass2/120.patch patchSha256=410c21de11c9acb2535f2acab3c016ae5690d5213def0191c98b328880d7e429 patchBytes=826 patchLines=20 notesReviewed=yes reviewedAt=2026-01-24T22:30:30+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T13:40:00+0100 manualReviewedForPrPlan=yes notes=Trivial test cleanup (blank line) with misleading subject; squash into nearby Claude tests during rewrite.

- Subject: test(claude): align sessionScanner path mapping
- Reasons: full manual review coverage (was not in the manual-review queue)

### Manual Review Notes

- Diffstat: `1 file changed, 1 deletion(-)`
- What changed:
  - Removes a stray blank line in `cli/src/claude/utils/sessionScanner.test.ts`.
- Coherence:
  - The diff is fine, but the subject is misleading (it duplicates the earlier “align path mapping” subject). During rewrite, rename or squash this into the surrounding test cleanup.

### Verdict

- Keep/squash/split/drop/reorder: squash into nearby test commits (very small).
- Depends on: none.

### Rewrite Commit Message

- Rewrite subject to match diff (if not squashed): `chore(test): remove stray blank line`

---

## 121 2026-01-12 9c4cb44b7d86 `daemon`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9c4cb44b7d86 patchFile=/tmp/leeroy-wip-pass2/121.patch patchSha256=99c4d3f4aa1de8302a73d5e1141f76222598f8dd28aed42743a3a27c069b8b26 patchBytes=19771 patchLines=495 notesReviewed=yes reviewedAt=2026-01-24T22:31:11+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Daemon reliability: session marker registry + restart reattach semantics + atomic daemon state persistence; keep with ownership-gated process safety.

- Subject: fix(daemon): reattach sessions after daemon restart
- Reasons: safety/reliability-sensitive area (process ownership + restart continuity)

### Manual Review Notes

- Diffstat: `7 files changed, 295 insertions(+), 17 deletions(-)`
- What changed (high-level):
  - Introduces “session markers” persisted under `HAPPY_HOME_DIR` so a restarted daemon can reattach to still-running session processes:
    - Adds `cli/src/daemon/sessionRegistry.ts` to write/list/remove per-PID marker JSON files.
    - On daemon startup, reads markers and adopts sessions whose PID is still alive and still looks like a Happy session process.
  - PID reuse safety:
    - Uses `ps-list`-based classification (`findAllHappyProcesses`) to only adopt PIDs that look like Happy sessions (fail-closed vs false positives).
    - When stopping a reattached session by PID, verifies the PID still appears to be a Happy session process before sending SIGTERM.
  - Cross-stack safety:
    - Ignores webhooks for sessions whose `metadata.happyHomeDir` doesn’t match the daemon’s configured `happyHomeDir`.
  - Lifecycle:
    - Writes/updates markers on session webhook reports (best-effort).
    - Removes markers when sessions exit/are cleaned up.
  - API surface:
    - Makes `stopSession` async (`Promise<boolean>`) across the daemon control server and apiMachine handler.
  - Persistence hardening (also in this commit; important for restart/reattach correctness):
    - `persistence.ts` adds a zod `DaemonLocallyPersistedStateSchema` and validates reads.
    - `readDaemonState()` now retries up to 3 times on transient read errors (but fails closed on `SyntaxError` / invalid schema).
    - `writeDaemonState()` becomes atomic via `*.tmp` + `renameSync` with Windows `EEXIST/EPERM` fallback; also ensures parent dir exists and cleans up orphan `*.tmp` files.
- Coherence:
  - Matches subject and is strongly aligned with reliability + “ownership-gated kills” principles.

### Verdict

- Keep/squash/split/drop/reorder: keep (important).
- Depends on: `ps-list` command visibility/accuracy on the target OS and stable `HAPPY_HOME_DIR` scoping.

### Rewrite Commit Message

- Keep subject: `fix(daemon): reattach sessions after daemon restart`

---

## 122 2026-01-14 dbde6d871670 `daemon`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=dbde6d871670 patchFile=/tmp/leeroy-wip-pass2/122.patch patchSha256=425e25bd818a27228a3718449865e31d36f01c62caacd74073750d79d93a7280 patchBytes=10606 patchLines=278 notesReviewed=yes reviewedAt=2026-01-24T22:31:56+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Daemon reliability tests: PID classification strictness + session marker semantics; keep adjacent to 121.

- Subject: test(daemon): add PID classification + session marker tests
- Reasons: safety/reliability-sensitive area (tests for ownership gating)

### Manual Review Notes

- Diffstat: `3 files changed, 199 insertions(+), 34 deletions(-)`
- What changed:
  - Adds `classifyHappyProcess(...)` in `cli/src/daemon/doctor.ts` and makes classification intentionally strict (to avoid adopting/killing unrelated processes).
  - Adds `cli/src/daemon/doctor.test.ts` verifying classification behavior on representative command lines.
  - Adds `cli/src/daemon/sessionRegistry.test.ts`:
    - validates marker write/update semantics (createdAt preserved, updatedAt bumps),
    - ignores markers for the wrong `happyHomeDir`,
    - tolerates invalid JSON.
- Coherence:
  - Matches subject and provides meaningful coverage for the reattach feature introduced in 121.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: marker layout conventions and the classification heuristics remaining strict.

### Rewrite Commit Message

- Keep subject: `test(daemon): add PID classification + session marker tests`

---

## 123 2026-01-14 7cffdd5ce614 `daemon`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7cffdd5ce614 patchFile=/tmp/leeroy-wip-pass2/123.patch patchSha256=89b569fa130b6df175f9de9ac140dd213b835490ff97a5aa37487b6cc4d82c3f patchBytes=9585 patchLines=207 notesReviewed=yes reviewedAt=2026-01-24T22:32:42+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Daemon safety hardening: PID reuse mitigation via process command hashing for reattach/stop; keep adjacent to 121/122.

- Subject: fix(daemon): verify PID via command hash for reattach/stop
- Reasons: safety/security-sensitive area (PID reuse mitigation)

### Manual Review Notes

- Diffstat: `4 files changed, 74 insertions(+), 10 deletions(-)`
- What changed:
  - Strengthens PID reuse safety by adding process identity hashing:
    - `cli/src/daemon/sessionRegistry.ts` adds `hashProcessCommand(command)` and stores `processCommandHash` (+ optional `processCommand`) in markers.
    - On daemon startup reattach, refuses to adopt markers missing a hash (fail-closed) and requires the current process command hash to match the marker hash.
    - On stopSession, requires hash match (when known) before sending SIGTERM, even for non-“reattached” sessions.
  - Tightens allowed process types for session PIDs (keeps it strict).
  - Extends sessionRegistry tests to cover new marker fields.
- Coherence:
  - Matches subject; this is the natural hardening follow-up to 121/122.
- Reliability tradeoff:
  - Reattach becomes “safer but stricter”: if command hashing can’t be computed (e.g., ps truncation or missing cmdline), reattach will now skip the session. That may be acceptable but should be validated on target platforms.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be adjacent to 121/122.
- Depends on: `ps-list` returning a stable-enough command string to hash and compare.

### Rewrite Commit Message

- Keep subject: `fix(daemon): verify PID via command hash for reattach/stop`

---

## 124 2026-01-13 8b88dcd73d40 `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8b88dcd73d40 patchFile=/tmp/leeroy-wip-pass2/124.patch patchSha256=39d4e468290fc1869c70575d79c787da0b81f17506ce0fc4b8e7403e0e3b0f57 patchBytes=6503 patchLines=149 notesReviewed=yes reviewedAt=2026-01-24T22:33:10+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Permission framework (Claude baseline): carry selected permission mode across remote/local switches by persisting lastPermissionMode and updating Claude args before spawn; safety-critical.

- Subject: fix(claude): carry permission mode across remote/local switches
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `5 files changed, 51 insertions(+)`
- What changed:
  - Introduces `Session.lastPermissionMode` as a first-class session field (defaults to `'default'`), and ensures it is updated whenever:
    - the remote launcher observes a mode change from message metadata, or
    - the permission handler updates its mode from permission responses, or
    - the local launcher receives a queued message (captures the mode used for that message).
  - Ensures local Claude Code is spawned with flags matching the current session permission mode:
    - Adds `upsertClaudePermissionModeArgs(...)` which removes existing permission flags (`--permission-mode`, `--dangerously-skip-permissions`) from `session.claudeArgs` and re-inserts the correct flags based on `session.lastPermissionMode` (via `mapToClaudeMode`).
    - In `claudeLocalLauncher`, applies this right before spawning Claude, so remote → local switches carry over the app-selected mode.
  - Seeds `lastPermissionMode` on session creation (`loop.ts`) from `opts.permissionMode` to avoid “unknown until first message” behavior.
- Why it matters:
  - Permission mode is a safety-critical control. Without this, switching between remote and local modes can silently drop the user’s intended permission policy.

### Verdict

- Keep/squash/split/drop/reorder: keep (safety fix).
- Depends on: `mapToClaudeMode` correctly mapping app permission modes to Claude CLI flags; correctness of any existing `session.claudeArgs` construction upstream.

### Rewrite Commit Message

- Keep subject: `fix(claude): carry permission mode across remote/local switches`

---

## 125 2026-01-13 8f0e10c9428b `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8f0e10c9428b patchFile=/tmp/leeroy-wip-pass2/125.patch patchSha256=f63543b0a8ff41e6064cf9680650c5aa0f4b0d4711cf3eb4b04015e200d34474 patchBytes=8317 patchLines=188 notesReviewed=yes reviewedAt=2026-01-24T22:33:49+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Permission framework (metadata publish): publish permissionMode(+updatedAt) into session metadata for Claude so app can seed UI state before first app-originated message; keep adjacent to 124 and app-side consumption.

- Subject: fix(claude): publish permission mode in session metadata
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `7 files changed, 58 insertions(+), 10 deletions(-)`
- What changed:
  - Publishes the session’s current permission mode into server-visible session metadata so the app can seed UI state even before any app-originated user message exists (i.e. before `message.meta.permissionMode` exists).
  - `cli/src/api/types.ts` extends `Metadata` with:
    - `permissionMode?: PermissionMode`
    - `permissionModeUpdatedAt?: number`
  - `cli/src/claude/session.ts` adds `setLastPermissionMode(mode, updatedAt?)`:
    - updates `session.lastPermissionMode` + `lastPermissionModeUpdatedAt`
    - calls `client.updateMetadata(...)` to publish `{ permissionMode, permissionModeUpdatedAt }`
  - `cli/src/claude/runClaude.ts`:
    - infers an initial permission mode for terminal/local-started sessions from `options.permissionMode` or `options.claudeArgs` (via `inferPermissionModeFromClaudeArgs`), defaulting to `default`.
    - includes `permissionMode` + `permissionModeUpdatedAt` in the initial session metadata at creation time.
  - Updates launchers/handlers to use `session.setLastPermissionMode(...)` consistently (removes duplicate direct assignments in remote launcher).
- Risk / ordering note:
  - The timestamp uses `Date.now()` on the CLI side; the app later tries to arbitrate “latest wins” across devices. If device clocks differ materially, this can still be imperfect. It’s an improvement over “no signal at all”, but during rewrite we may want to align this with a server-derived timestamp strategy if feasible.

### Verdict

- Keep/squash/split/drop/reorder: keep, but it should be grouped/squashed with:
  - 124 (carry mode across remote/local) and
  - the app-side metadata consumption (067).
- Depends on: session metadata being persisted/synced and visible to the app; correctness of permission-mode-to-CLI-args mapping for local starts.

### Rewrite Commit Message

- Keep subject: `fix(claude): publish permission mode in session metadata`

---

## 126 2026-01-13 ffad20faa406 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ffad20faa406 patchFile=/tmp/leeroy-wip-pass2/126.patch patchSha256=6b3d0db00b8297708e4c126cda7c32cc1757917052997e3e89d2afb7b0f932bc patchBytes=16431 patchLines=352 notesReviewed=yes reviewedAt=2026-01-24T22:34:28+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR11 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Permission modes (Codex/Gemini): seed/update permissionMode in metadata + CLI arg parsing + canonical permission constants; keep with agent-specific permission-mode PR (adjacent to 125 for shared metadata shape).

- Subject: fix(cli): publish permission mode for codex/gemini sessions
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `5 files changed, 129 insertions(+), 30 deletions(-)`
- What changed:
  - Generalizes “session permission mode is metadata” beyond Claude, covering Codex and Gemini:
    - `cli/src/api/types.ts`:
      - Replaces the raw string-literal union with canonical `PERMISSION_MODES` constants and helpers:
        - `isPermissionMode`, `CODEX_GEMINI_PERMISSION_MODES`, `CODEX_PERMISSION_MODES`, and related type guards.
      - Updates `MessageMetaSchema.permissionMode` to `z.enum(PERMISSION_MODES)` to keep Zod validation aligned with the canonical list.
    - `cli/src/index.ts`:
      - Adds CLI arg parsing for:
        - `--started-by daemon|terminal`
        - `--permission-mode <value>` (validated)
        - `--yolo` (alias sets permissionMode to `yolo`)
      - Intended to pass these session-start options down to the agent runners.
    - `cli/src/codex/runCodex.ts`:
      - Accepts `permissionMode` in opts, seeds it into initial session metadata (`permissionMode`, `permissionModeUpdatedAt`).
      - Initializes `currentPermissionMode` to the initial value (instead of `undefined`) and updates session metadata when user messages include a permission override.
    - `cli/src/gemini/runGemini.ts`:
      - Accepts `permissionMode` in opts but clamps it to the Codex/Gemini subset (defaults to `default` if invalid).
      - Seeds and updates session metadata similarly to Codex.
    - `cli/src/utils/createSessionMetadata.ts`:
      - Updated to handle `permissionMode`/`permissionModeUpdatedAt` when constructing session metadata (used by multiple agents).
- Issues / follow-ups to address during rewrite:
  - In the diff as captured, `createSessionMetadata({ ... })` calls for Codex/Gemini show `terminalRuntime` duplicated in the object literal. If that duplication is real in the final tree, it would be a TS error and should be fixed (keep only one key).
  - `permissionModeUpdatedAt` uses `Date.now()` on the CLI side; the app later tries to arbitrate across devices using server-aligned time. If we want strong cross-device ordering, we may need a consistent timestamp strategy across CLI/app.

### Verdict

- Keep/squash/split/drop/reorder: keep, but group with 125 (Claude metadata publish) and app-side metadata consumption (067) so the end-to-end feature lands coherently.
- Depends on: session metadata being visible/synced to the app; message meta permissionMode being populated consistently by the app/daemon; correct permission-mode subsets per agent.

### Rewrite Commit Message

- Keep subject: `fix(cli): publish permission mode for codex/gemini sessions`

---

## 127 2026-01-13 eb72c6e69434 `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=eb72c6e69434 patchFile=/tmp/leeroy-wip-pass2/127.patch patchSha256=37144c1d8f0462514b28be4ce13ecc782ca18e4b8e5fd3296be01c38f77113dc patchBytes=8988 patchLines=173 notesReviewed=yes reviewedAt=2026-01-24T22:37:39+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Codex session lifecycle: remove brittle experimental resume + disable model overrides (restart drops context explicitly); squash into the earlier Codex “model override/experimental resume” intro commits during rewrite.

- Subject: fix(codex): disable model override and remove experimental resume
- Reasons: reliability; removes a brittle/unsupported resume path and prevents misleading UI behavior (model switching that can’t be honored)

### Manual Review Notes

- Diffstat: `1 file changed, 13 insertions(+), 96 deletions(-)`
- What changed:
  - `cli/src/codex/runCodex.ts`:
    - Removes per-message model override support:
      - Drops `currentModel` tracking and ignores `message.meta.model` (model overrides are now intentionally unsupported for Codex).
      - Updates the per-message mode hash to ignore `model` (comment explains Codex can’t reliably switch models mid-session without losing in-memory context).
    - Removes the “experimental resume” mechanism:
      - Deletes `findCodexResumeFile(...)` which scanned Codex’s local session transcript directory to find a `*-<sessionId>.jsonl` file.
      - Removes passing `experimental_resume` into the Codex start config (previously used to resume after mode changes / aborted sessions).
    - Clarifies restart semantics:
      - Permission-mode changes still restart the Codex session, but now explicitly drop context with no resume attempt.
- Coherence / grouping notes:
  - This commit is effectively an unwind of earlier “model override” and “resume by transcript scanning” experiments. During history cleanup, this should be squashed with the commits that introduced those behaviors so intermediate states aren’t preserved as “real” milestones.

### Verdict

- Keep/squash/split/drop/reorder: keep, but squash into the earlier Codex “model override / experimental resume” series.
- Depends on: none (behavior change is self-contained in Codex runner).

### Rewrite Commit Message

- Keep subject: `fix(codex): disable model override and remove experimental resume`
- Optional body notes (recommended): mention removal of transcript scanning and `experimental_resume` injection; mention mode-hash no longer includes `model`.

---

## 128 2026-01-21 d317f6bfa282 `typecheck`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d317f6bfa282 patchFile=/tmp/leeroy-wip-pass2/128.patch patchSha256=7d619c16cb24462638734c1315b9ea8009a3985d40fceec02197075d008387bd patchBytes=1915 patchLines=50 notesReviewed=yes reviewedAt=2026-01-24T22:38:30+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR11 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Follow-up compile fix: remove duplicated terminalRuntime keys introduced in Codex/Gemini metadata plumbing + align async stopSession test stub; squash into 126 during rewrite.

- Subject: fix(typecheck): resolve duplicate terminalRuntime and RPC handler types
- Reasons: fixes real TypeScript errors / type drift introduced by nearby commits

### Manual Review Notes

- Diffstat: `3 files changed, 1 insertion(+), 4 deletions(-)`
- What changed:
  - Fixes duplicate `terminalRuntime` object keys:
    - `cli/src/codex/runCodex.ts`: removes a duplicated `terminalRuntime: ...` entry in `createSessionMetadata(...)`.
    - `cli/src/gemini/runGemini.ts`: removes the same duplication.
  - Fixes test handler type mismatch:
    - `cli/src/api/apiMachine.spawnSession.test.ts`: makes `stopSession` `async () => true` (aligns with an async RPC handler signature).
- Coherence / grouping notes:
  - This is a “make the tree compile” follow-up. It should almost certainly be squashed into the commits that introduced the duplicates / handler signature changes (not kept as a standalone conceptual change).

### Verdict

- Keep/squash/split/drop/reorder: keep, but squash into the parent commits that introduced the duplicates / handler type changes.
- Depends on: commit(s) that introduced duplicated `terminalRuntime` keys and async handler typing.

### Rewrite Commit Message

- Suggested subject: `fix(cli): remove duplicated terminalRuntime fields`
- Optional split (if you want cleaner history): separate the test stub fix into `test(api): align spawn handler mock types`.

---

## 129 2026-01-21 a8682d427794 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a8682d427794 patchFile=/tmp/leeroy-wip-pass2/129.patch patchSha256=947a93b2c50fa20c7d24f8d7fe673af70e00f9609eb66a2a5f62eca5c6f9123b patchBytes=995 patchLines=21 notesReviewed=yes reviewedAt=2026-01-24T22:39:02+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=detect-cli reliability: relax tmux version probe timeout to reduce false negatives on slow machines; keep with capabilities/detect-cli PR.

- Subject: fix(detect-cli): relax tmux version probe timeout
- Reasons: reliability; avoids false negatives on slow machines / cold disks / heavy load

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `cli/src/modules/common/registerCommonHandlers.ts`:
    - In `detectTmuxVersion(...)`, increases the subprocess timeout from `600ms` → `1500ms`.

### Verdict

- Keep/squash/split/drop/reorder: keep (small, focused reliability fix).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(detect-cli): relax tmux version probe timeout`

---

## 130 2026-01-07 e2824dbc858b `queue`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e2824dbc858b patchFile=/tmp/leeroy-wip-pass2/130.patch patchSha256=93510969262820b68b311d7d7edeffd55470d857bccda37b4d1e1ef256e906c6 patchBytes=7458 patchLines=155 notesReviewed=yes reviewedAt=2026-01-24T22:39:35+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T13:17:11+0100 manualReviewedForPrPlan=yes notes=Mixed: (a) queue feature: add pending-pop/pull API + agent-side drain semantics; (b) resume: add codexSessionId metadata. Keep PR13 but needsSplit=yes: move codexSessionId metadata to PR19 during rewrite.

- Subject: feat(queue): add server-side pending message pull support
- Reasons: enables “defer committing UI messages to transcript” by letting the agent pull server-staged messages when ready

### Manual Review Notes

- Diffstat: `5 files changed, 58 insertions(+), 8 deletions(-)`
- What changed:
  - Adds a client API for “pending messages” staged on the server:
    - `cli/src/api/types.ts`: adds socket events:
      - `'pending-enqueue'`, `'pending-list'`, `'pending-update'`, `'pending-delete'`, `'pending-pop'`.
    - `cli/src/api/apiSession.ts`: adds `ApiSessionClient.popPendingMessage()` which calls `'pending-pop'` with `{ sid }` and returns a boolean (fail-closed on errors/disconnect).
  - Hooks pending-pop into agent runners to keep sessions responsive:
    - `cli/src/claude/claudeRemoteLauncher.ts`: when the local queue is empty, tries `session.client.popPendingMessage()` before blocking on `waitForMessages...`.
    - `cli/src/codex/runCodex.ts` and `cli/src/gemini/runGemini.ts`:
      - calls `session.popPendingMessage()` before waiting for queue messages, so server-side staged messages get materialized into the transcript.
      - after a turn, tries `popPendingMessage()` and only emits “ready” if nothing was popped (so “ready” isn’t emitted while there’s work staged server-side).
  - Also adds a resume-related metadata field:
    - `cli/src/api/types.ts`: adds `codexSessionId?: string` to `Metadata` (this is logically “resume” / vendor-id plumbing rather than “queue”).
- Coherence / grouping notes:
  - This commit mixes two concepts:
    - “pending message pull/pop” (queue mechanics),
    - “codexSessionId metadata” (resume/vendor id plumbing).
  - During rewrite, consider moving the `codexSessionId` metadata addition into the resume series (or at least keep those commits adjacent).
- Dependencies / ordering constraints:
  - Requires server-side implementation of the `pending-*` socket events; without it, `popPendingMessage()` simply returns `false` (safe, but feature is inert).
  - If the UI introduces “pending enqueue”, this commit should land no later than those UI changes (otherwise the agent won’t drain staged messages).

### Verdict

- Keep/squash/split/drop/reorder: keep, but consider splitting `codexSessionId` metadata into the resume topic.
- Depends on: server support for pending queue operations; UI changes that enqueue pending items.

### Rewrite Commit Message

- Suggested subject: `feat(queue): allow agents to pop server-staged messages`
- Optional follow-up body: note that pop is best-effort and only triggers when idle.

---

## 131 2026-01-12 37a2c55a6c2f `session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=37a2c55a6c2f patchFile=/tmp/leeroy-wip-pass2/131.patch patchSha256=68b33689da303dc91fe0a0d90841f26f061bfa13b0b2779156640251e74cefbf patchBytes=2238 patchLines=50 notesReviewed=yes reviewedAt=2026-01-24T22:40:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Agent lifecycle correctness: prime Codex/Gemini agentStateVersion so UI readiness flips true; keep with resume/session lifecycle PR since resume/attach relies on readiness.

- Subject: fix(session): prime agent state for readiness
- Reasons: correctness; ensures the UI’s “agent ready” signal becomes true for Codex/Gemini on session start

### Manual Review Notes

- Diffstat: `2 files changed, 20 insertions(+)`
- What changed:
  - `cli/src/codex/runCodex.ts` and `cli/src/gemini/runGemini.ts`:
    - Calls `session.updateAgentState((current) => ({ ...current }))` right after session creation to bump `agentStateVersion`.
    - Comment documents a current server behavior: agentState starts at version 0 and only changes via `update-state`; UI uses `agentStateVersion > 0` as readiness (Claude already bumped it).
    - Errors are swallowed (non-fatal) and only logged in debug.
- Coherence / grouping notes:
  - This is a targeted workaround for a cross-component contract (“UI readiness is agentStateVersion > 0”). If we ever change that contract or persist agent state server-side at creation time, this can be revisited.

### Verdict

- Keep/squash/split/drop/reorder: keep (small and behaviorally meaningful), but group with other readiness / resume-attach changes that rely on readiness.
- Depends on: current UI readiness semantics (agentStateVersion-based).

### Rewrite Commit Message

- Keep subject: `fix(session): prime agent state for readiness`

---

## 132 2026-01-21 88494b122194 `tools`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=88494b122194 patchFile=/tmp/leeroy-wip-pass2/132.patch patchSha256=1bb827b658bf676d4cafa97e0a1fbb677ce20e89541ca55c496c15be19982c80 patchBytes=6195 patchLines=173 notesReviewed=yes reviewedAt=2026-01-24T22:40:48+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Mixed + later reverted by 140: Windows arm64 tools mapping + rg.exe launcher + test-runner env fallbacks. Keep in tools PR as a pair with its revert; likely drop 132+140 in rewrite unless we intentionally reintroduce parts. needsSplit=yes

- Subject: fix(tools): support Windows arm64 tool unpacking
- Reasons: platform compatibility (Windows ARM running x64 tools under emulation), plus test harness reliability changes

### Manual Review Notes

- Diffstat: `6 files changed, 70 insertions(+), 6 deletions(-)`
- What changed (as authored, this is a mixed-concern commit):
  - Tools/platform mapping and ripgrep execution:
    - `cli/scripts/unpack-tools.cjs`: maps `win32/arm64` → `x64-win32` (comment notes Windows-on-ARM x64 emulation).
    - `cli/scripts/ripgrep_launcher.cjs`: uses `rg.exe` on Windows for the “local binary fallback” path.
    - Adds tests:
      - `cli/scripts/__tests__/unpack-tools.test.ts`: asserts the platform mapping.
      - `cli/scripts/__tests__/ripgrep_launcher.test.ts`: asserts the launcher references `rg.exe`.
  - Test runner environment changes (not directly related to Windows tools):
    - `cli/src/test-setup.ts`: forces writable `HAPPY_HOME_DIR` and `CLAUDE_CONFIG_DIR` under a temp base dir when configured homes aren’t writable.
    - `cli/vitest.config.ts`:
      - includes `scripts/**/*.test.ts`,
      - makes dotenv parse default to `{}` instead of `undefined`,
      - changes env merge order to prefer `.env.integration-test` over `process.env`.
- Coherence / grouping notes:
  - This commit is reverted later (see commit 140 in this audit), which strongly suggests the scope/mix or the approach was deemed incorrect.
  - For history rewrite, the clean options are:
    - squash `132` + `140` away entirely (no net change),
    - or keep only the parts that we actually want long-term, but split them into focused commits:
      - `fix(tools): map win32/arm64 to x64-win32`
      - `fix(tools): use rg.exe on win32 fallback`
      - `test(cli): run scripts tests in vitest`
      - `test(cli): make HAPPY_HOME_DIR/CLAUDE_CONFIG_DIR writable in tests`

### Verdict

- Keep/squash/split/drop/reorder: likely squash with its revert (140) unless pieces are reintroduced later in a cleaner form.
- Depends on: how `tools/` are packaged and whether `rg.exe` exists in the unpacked bundle for Windows.

### Rewrite Commit Message

- If kept at all, split (recommended). The current subject is misleading for the non-tools changes.

---

## 133 2026-01-09 d71587eda2db `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d71587eda2db patchFile=/tmp/leeroy-wip-pass2/133.patch patchSha256=c971266bb10bc1e5b1d08786d71c52848de972e1bd0e9983a05f7269b38ba902 patchBytes=10151 patchLines=218 notesReviewed=yes reviewedAt=2026-01-24T22:41:30+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Resume plumbing (Claude): plumb vendor resume id through daemon spawn with strict allowlist; keep adjacent to resume UI/RPC callers so it lands end-to-end.

- Subject: feat: resume Claude sessions from UI
- Reasons: introduces a controlled “vendor resume id” plumbing path; safety-gated to Claude in upstream

### Manual Review Notes

- Diffstat: `4 files changed, 83 insertions(+), 41 deletions(-)`
- What changed:
  - Plumbs `resume` through the daemon’s “spawn session” RPC:
    - `cli/src/api/apiMachine.ts`: includes `resume` in the RPC params, logs `hasResume`, and forwards it to `spawnSession(...)`.
    - `cli/src/modules/common/registerCommonHandlers.ts`: extends `SpawnSessionOptions` with `resume?: string` (documented as vendor resume for Claude).
  - Adds safety gating for vendor resume:
    - `cli/src/utils/agentCapabilities.ts` (new):
      - introduces `supportsVendorResume(agent)` with an allowlist (`['claude']` upstream policy).
  - Actually passes `--resume` to the agent process when spawning:
    - `cli/src/daemon/run.ts`:
      - rejects resume requests for unsupported agents (fail-closed).
      - appends `--resume <id>` in both tmux spawn and direct `spawnHappyCLI(...)` paths when `resume` is present.
- Coherence / grouping notes:
  - The subject says “from UI”, but this commit is purely CLI/daemon plumbing; the UI is expected to call the RPC later. During rewrite, keep it adjacent to the UI changes that start issuing the resume RPC so the feature is reviewable end-to-end.
  - There is a lot of indentation churn in `cli/src/daemon/run.ts` (tabs/spaces). It’s mostly mechanical noise, but it makes review harder; consider isolating formatting-only changes during history rewrite (or squashing them into one “format” commit).

### Verdict

- Keep/squash/split/drop/reorder: keep, but group with the rest of the resume/inactive-session series (134–136, 141, 137 fork-only Codex resume, etc.).
- Depends on: the agent runner supporting `--resume` (Claude) and the UI/daemon actually sending resume requests.

### Rewrite Commit Message

- Suggested subject: `feat(resume): plumb vendor resume id through daemon spawn (claude)`

---

## 134 2026-01-11 a80868aff50d `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a80868aff50d patchFile=/tmp/leeroy-wip-pass2/134.patch patchSha256=51800354db7127967253bb7b54d93e74ad4b0f9cc0bce291490413faffc92b3c patchBytes=24151 patchLines=538 notesReviewed=yes reviewedAt=2026-01-24T22:42:27+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Inactive resume core (Claude): resume-session RPC + persisted session file + --existing-session + initial message injection. Large mixed commit; keep in resume PR but split during rewrite into persistence format + RPC plumbing + Claude attach + types (needsSplit=yes).

- Subject: feat: resume inactive Claude sessions from UI
- Reasons: implements “inactive session resume” by attaching a new CLI worker to an existing Happy session (and optionally vendor-resuming Claude)

### Manual Review Notes

- Diffstat: `7 files changed, 253 insertions(+), 69 deletions(-)`
- What changed (high-level):
  - Adds an RPC-level “resume inactive session” flow and a local persistence layer that makes “attach to existing Happy session” possible.
- Key changes:
  - RPC / daemon spawn plumbing:
    - `cli/src/api/apiMachine.ts`:
      - Adds a new `params.type === 'resume-session'` branch that calls `spawnSession(...)` with:
        - `existingSessionId` (Happy session id),
        - `resume` (vendor resume id; Claude session id),
        - `initialMessage` (a message to send immediately after reattach).
      - Returns `{ type: 'success' }` without a new `sessionId`.
    - `cli/src/modules/common/registerCommonHandlers.ts`:
      - Extends `SpawnSessionOptions` with `existingSessionId?: string` and `initialMessage?: string`.
      - Makes `SpawnSessionResult`’s success `sessionId` optional to support “resume” returning no new id.
    - `cli/src/daemon/run.ts`:
      - Plumbs `--existing-session <id>` into the spawned child args (tmux + non-tmux).
      - Passes `initialMessage` via `HAPPY_INITIAL_MESSAGE` env var (so the child can enqueue it on startup).
      - Extends the “vendor resume supported agents” gating to cover `existingSessionId` too (fail-closed for unsupported agents upstream).
  - CLI option parsing:
    - `cli/src/index.ts`: parses `--existing-session` and stores it in options (daemon passes this flag).
  - CLI “attach to existing session” behavior (Claude):
    - `cli/src/claude/runClaude.ts`:
      - Adds `existingSessionId?: string` to options.
      - If set, reads local persisted session state and uses it as the base session instead of calling `api.getOrCreateSession(...)`.
      - Always writes the base session state to disk so future attaches are possible.
      - Updates session metadata on startup (`lifecycleState: 'running'`, `lifecycleStateSince: Date.now()`).
      - If `HAPPY_INITIAL_MESSAGE` is present, pushes an initial message into the message queue immediately.
  - Local persistence layer:
    - `cli/src/daemon/persistedHappySession.ts` (new):
      - Writes `$HAPPY_HOME_DIR/sessions/<sessionId>.json` with encryption key, metadata, agentState, versions.
      - Atomic write via `*.tmp` then rename; reads validate via Zod and return a minimal `Session` object (with `seq: 0`).
- Issues / inaccuracies in commit message:
  - The commit message claims “Add RESUMABLE_AGENTS config for dynamic agent resume capability”, but the diff does not implement that (capability remains a static allowlist in `agentCapabilities.ts`).
- Coherence / split candidates:
  - This commit bundles multiple large concerns:
    - new persisted session storage format,
    - RPC/daemon plumbing for resume-session,
    - CLI attach behavior + initial message queueing,
    - metadata lifecycle updates.
  - For a clean history rewrite, splitting this into smaller commits will materially improve reviewability and bisectability.

### Verdict

- Keep/squash/split/drop/reorder: keep, but split during history rewrite (recommended).
- Depends on:
  - 133 (introduces `supportsVendorResume` gating),
  - local file persistence being stable across versions,
  - UI sending correct `resume-session` RPC payload.

### Rewrite Commit Message

- Suggested split (recommended):
  - `feat(daemon): persist session state for reattach`
  - `feat(resume): support resume-session RPC via --existing-session`
  - `feat(claude): attach to existing Happy session and queue initial message`
  - `chore(types): allow spawn success without sessionId`

---

## 135 2026-01-12 bcaa530b12d7 `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=bcaa530b12d7 patchFile=/tmp/leeroy-wip-pass2/135.patch patchSha256=7561d4a5e463553cc4bf588412dc7cece7ec1033d235b320aa959724d340a78d patchBytes=9675 patchLines=229 notesReviewed=yes reviewedAt=2026-01-24T22:47:01+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Resume UX/tooling: add `happy resume` command and persist vendorResumeId into local session file; keep with inactive resume end-to-end PR.

- Subject: feat(resume): add happy resume and persist vendor resume id
- Reasons: adds a user-facing recovery command (`happy resume`) and ensures the vendor resume id is persisted once discovered

### Manual Review Notes

- Diffstat: `3 files changed, 98 insertions(+), 27 deletions(-)`
- What changed:
  - Persist vendor resume id into the local session file:
    - `cli/src/claude/session.ts`:
      - When the Claude session id is discovered and written to metadata, also updates the local persisted session file with `vendorResumeId` (best-effort).
    - `cli/src/daemon/persistedHappySession.ts`:
      - Adds `vendorResumeId?: string` to the persisted schema and writes it from metadata when present.
      - Splits file reading into `readPersistedHappySessionFile(...)` (raw JSON) and `readPersistedHappySession(...)` (converted `Session`).
      - Adds `updatePersistedHappySessionVendorResumeId(sessionId, vendorResumeId)` which updates the JSON atomically.
  - Adds a new CLI subcommand:
    - `cli/src/index.ts`:
      - Adds `happy resume <happySessionId>`:
        - reads the persisted session file,
        - infers `agent` from `(metadata as any).flavor` (defaults to `claude`),
        - verifies `supportsVendorResume(agent)`,
        - finds the vendor resume id (`vendorResumeId` or `metadata.claudeSessionId`),
        - spawns a detached child with `--existing-session <id>` and `--resume <vendorId>`.
      - Updates help text to mention `happy resume`.
- Coherence / grouping notes:
  - This commit is a natural follow-on to 134 (persisted session state + `--existing-session`) and 133 (vendor resume gating). During rewrite, keep these adjacent.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on:
  - 134’s persisted session format,
  - 133’s `supportsVendorResume` allowlist (upstream: Claude only),
  - Claude session id being discoverable via the SessionStart hook.

### Rewrite Commit Message

- Keep subject: `feat(resume): add happy resume and persist vendor resume id`

---

## 136 2026-01-12 e085e75c5d36 `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e085e75c5d36 patchFile=/tmp/leeroy-wip-pass2/136.patch patchSha256=452991bc1eca3a28189df45150e8cb2677f7ee444af83678c2d164d8a0ef3fd8 patchBytes=11945 patchLines=254 notesReviewed=yes reviewedAt=2026-01-24T22:47:53+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Resume robustness: allow resume-session without agentSessionId and derive effective vendor resume id from persisted local session state; must squash/follow immediately with compile-fix for duplicate import.

- Subject: feat(resume): allow resume-session without agentSessionId
- Reasons: makes “inactive session resume” more robust by allowing the daemon to derive the vendor resume id from local state

### Manual Review Notes

- Diffstat: `3 files changed, 83 insertions(+), 64 deletions(-)`
- What changed:
  - Allows resume-session RPC to omit `agentSessionId`:
    - `cli/src/api/apiMachine.ts`: no longer throws when `agentSessionId` is missing; passes `resume` only if non-empty.
  - Derives an effective vendor resume id when missing:
    - `cli/src/daemon/run.ts`:
      - If `existingSessionId` is provided but `resume` is not, reads the local persisted session file and derives `effectiveResume` from:
        - `persisted.vendorResumeId`, else
        - `persisted.metadata.claudeSessionId`.
      - Uses `effectiveResume` to decide whether to pass `--resume`.
  - Extends CLI recovery to support bulk resume:
    - `cli/src/index.ts`: changes `happy resume <happySessionId>` to `happy resume <happySessionId...>` and attempts to resume each; exits non-zero if any fail.
- Issues / correctness concerns:
  - `cli/src/daemon/run.ts` has a duplicated import of `readPersistedHappySessionFile` from two paths (`./persistedHappySession` and `@/daemon/persistedHappySession`), which is a TypeScript compile error unless later commits fix it. This should be corrected (or squashed into the fix) during history rewrite so the series is bisectable.
  - There is substantial indentation churn in `cli/src/index.ts` and `cli/src/daemon/run.ts` mixed into the functional change; during rewrite, try to avoid mixing formatting-only diffs into behavior commits.

### Verdict

- Keep/squash/split/drop/reorder: keep, but must be squashed with (or followed immediately by) a compile-fix commit for the duplicate import.
- Depends on: 134–135 (persisted session file and vendorResumeId).

### Rewrite Commit Message

- Suggested subject: `feat(resume): derive vendor resume id from local session state`

---

## 137 2026-01-12 68a6ba4bc244 `fork`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=68a6ba4bc244 patchFile=/tmp/leeroy-wip-pass2/137.patch patchSha256=09b9dd4d2c2f8544ed66f7bc3c8667e4bba75d5cd6fa965616b8b0b929112434 patchBytes=23853 patchLines=497 notesReviewed=yes reviewedAt=2026-01-24T22:48:49+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Codex inactive resume: attach to existing session + vendor resume id via codex-reply MCP tool. Treat as upstream-targeted per decision: remove “fork-only” framing/gating during rewrite. Big mixed; split into MCP resume seeding + attach plumbing + persistence updates (needsSplit=yes).

- Subject: feat(fork): enable Codex inactive-session resume via codex-reply
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Diffstat: `6 files changed, 224 insertions(+), 73 deletions(-)`
- What changed (feature overview):
  - Adds a fork-only “inactive session resume” path for Codex by persisting a vendor resume id (`codexSessionId`) and resuming via a Codex MCP tool (`codex-reply`) rather than relying on transcript scanning.
- Key changes:
  - `cli/src/codex/codexMcpClient.ts`:
    - Adds `setSessionIdForResume(sessionId)` to seed the MCP client with a prior Codex session id and reset conversationId so `codex-reply` can resume without local transcript files.
  - `cli/src/codex/runCodex.ts`:
    - Adds support for:
      - `opts.existingSessionId`: attach to an existing Happy session (by reading persisted local session state) instead of creating a new server session.
      - `opts.resume`: provide a vendor resume id to resume Codex via MCP `codex-reply` on the first message.
    - On “attach”:
      - Requires local persisted session state; errors if missing (fails closed).
      - Updates metadata to mark the session running and primes agent state so the UI considers the agent “ready”.
      - Writes/ensures persisted session state exists for future resumes.
    - On “resume”:
      - Stores the resume id and consumes it once on the next message, using `client.setSessionIdForResume(resumeId)` then `client.continueSession(...)`.
      - Falls back to normal `startSession(...)` when no resume id is present.
    - Persists the Codex session id whenever the MCP client reports a new session id:
      - writes it to session metadata (`codexSessionId`),
      - and updates the local persisted Happy session vendorResumeId via `updatePersistedHappySessionVendorResumeId(...)`.
    - Adds a small “initial message” queue path via `HAPPY_INITIAL_MESSAGE` to support resume flows that want to inject an initial prompt automatically.
  - `cli/src/daemon/persistedHappySession.ts`:
    - Generalizes `vendorResumeId` selection:
      - for `flavor === 'codex'`, uses `metadata.codexSessionId`;
      - otherwise uses `metadata.claudeSessionId`.
  - `cli/src/index.ts`:
    - Parses `--existing-session` and `--resume` flags for Codex and passes them to `runCodex(...)`.
  - `cli/src/utils/agentCapabilities.ts`:
    - Enables Codex in “vendor resume capability” gating (fork-only).
- Risk notes:
  - This is intentionally fork-only and depends on custom Codex MCP behavior (`codex-reply`) and the presence of a persisted local session file. If either is missing, resume should fail closed rather than silently starting a new unrelated Codex conversation.
  - The feature spans CLI, daemon persistence, and app-side consumption (later commits); during rewrite, these should land as a coherent stack.

### Verdict

- Keep/squash/split/drop/reorder: keep (fork-only), but split during rewrite so each responsibility is reviewable:
  - `feat(codex): seed MCP resume via codex-reply`
  - `feat(resume): attach to existing Happy session and persist vendor resume id`
  - `feat(daemon): persist vendor resume id for codex sessions`
  - `feat(cli): plumb --existing-session/--resume flags`
- Depends on:
  - Codex MCP support for `codex-reply` and stable session ids.
  - `persistedHappySession` storage being present and readable for the target session id.
  - Follow-on app changes to trigger/consume inactive-session resume correctly.

### Rewrite Commit Message

- Keep subject: `feat(fork): enable Codex inactive-session resume via codex-reply`

---

## 138 2026-01-22 d3aefaa9b598 `daemon`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d3aefaa9b598 patchFile=/tmp/leeroy-wip-pass2/138.patch patchSha256=84e78084d52f7e771deaf52f514ec75cc0376492c391fbca3fe5dc52fdb49c7d patchBytes=9489 patchLines=210 notesReviewed=yes reviewedAt=2026-01-24T22:49:26+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Daemon refactor: extract pidSafety + marker adoption helpers to keep reattach/kill ownership gates strict and testable; keep with daemon reattach series.

- Subject: refactor(daemon): extract reattach and pid safety helpers
- Reasons: reliability/safety; isolates PID-reuse protection logic so it stays strict and testable

### Manual Review Notes

- Diffstat: `3 files changed, 86 insertions(+), 64 deletions(-)`
- What changed:
  - Extracts PID safety logic:
    - `cli/src/daemon/pidSafety.ts` (new):
      - Defines `ALLOWED_HAPPY_SESSION_PROCESS_TYPES` (strict allowlist).
      - Adds `isPidSafeHappySessionProcess({ pid, expectedProcessCommandHash? })`:
        - checks the PID is a currently running process that “looks like” a Happy session (via `findHappyProcessByPid` classification),
        - if a hash is provided, requires it to match the current command hash (PID reuse safety).
  - Extracts session adoption/reattach from markers:
    - `cli/src/daemon/reattach.ts` (new):
      - Adds `adoptSessionsFromMarkers({ markers, happyProcesses, pidToTrackedSession })` which:
        - only adopts markers when the PID’s process type is in the allowlist,
        - requires `processCommandHash` to be present and match the current running process hash,
        - sets `reattachedFromDiskMarker: true` on adopted tracked sessions.
  - Updates daemon runtime to use helpers:
    - `cli/src/daemon/run.ts`:
      - filters out dead markers (removes marker when PID is no longer alive),
      - uses `adoptSessionsFromMarkers(...)` instead of inline logic,
      - uses `isPidSafeHappySessionProcess(...)` before SIGTERM for reattached/external sessions.
- Coherence / grouping notes:
  - This is a clean refactor commit (mostly extraction + small behavioral improvements like “aliveMarkers”). It should live adjacent to the daemon session reattach feature and its tests (139).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: existing daemon “session markers” machinery (`sessionRegistry`, `doctor`).

### Rewrite Commit Message

- Keep subject: `refactor(daemon): extract reattach and pid safety helpers`

---

## 139 2026-01-22 72397b7e8c94 `test`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=72397b7e8c94 patchFile=/tmp/leeroy-wip-pass2/139.patch patchSha256=df9fca792af161b02458bb3a2f4dc937a154a83b01de8ba580e235e947b5bb8b patchBytes=9080 patchLines=258 notesReviewed=yes reviewedAt=2026-01-24T22:50:06+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Daemon opt-in real integration tests for reattach/command hashing; keep with daemon reliability PR (gated by env var, safe for CI).

- Subject: test(daemon): add opt-in reattach integration tests
- Reasons: validates the strict PID-safety / marker adoption logic against real OS process listing and command hashing

### Manual Review Notes

- Diffstat: `2 files changed, 238 insertions(+)`
- What changed:
  - Adds opt-in real integration tests (skipped by default) gated by `HAPPY_CLI_DAEMON_REATTACH_INTEGRATION=1`:
    - `cli/src/daemon/pidSafety.real.integration.test.ts`:
      - spawns a “happy-looking” Node process whose argv contains `happy-cli` and `--started-by daemon` so `ps-list` classification treats it as a Happy daemon-spawned session.
      - verifies `isPidSafeHappySessionProcess(...)` returns `true` when the expected command hash matches, and `false` when it mismatches.
    - `cli/src/daemon/reattach.real.integration.test.ts`:
      - uses a temporary `HAPPY_HOME_DIR`,
      - writes a session marker for the spawned PID (including command hash),
      - verifies `adoptSessionsFromMarkers(...)` adopts only when hash matches (fail-closed otherwise).
- Test quality notes:
  - These tests are inherently platform/environment sensitive because they rely on `ps-list` observing the spawned process in time. Gating them behind an explicit env var is appropriate (keeps normal CI deterministic while still allowing “real system” validation when needed).

### Verdict

- Keep/squash/split/drop/reorder: keep (opt-in integration suite).
- Depends on: `ps-list` behavior and `doctor.classifyHappyProcess` heuristics remaining stable.

### Rewrite Commit Message

- Keep subject: `test(daemon): add opt-in reattach integration tests`

---

## 140 2026-01-22 9c40c54018c2 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9c40c54018c2 patchFile=/tmp/leeroy-wip-pass2/140.patch patchSha256=eb909f9775dd5ada4bdbe85356bb0966679858bb5986524d600f7dd471607c53 patchBytes=6275 patchLines=175 notesReviewed=yes reviewedAt=2026-01-24T22:52:35+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T13:23:41+0100 manualReviewedForPrPlan=yes notes=Revert of 132 (Windows arm64 tools + scripts tests + test env fallback). Keep paired with 132 in tools PR; likely drop both (net no-op) unless reintroducing pieces intentionally.

- Subject: Revert "fix(tools): support Windows arm64 tool unpacking"
- Reasons: non-Conventional-Commits subject; revert commit

### Manual Review Notes

- Diffstat: `6 files changed, 6 insertions(+), 70 deletions(-)`
- What it does:
  - Reverts commit `88494b122194e4482ea817fad9c655c83b61a9e7` (Windows arm64 tool unpacking support).
- Observable changes from the revert:
  - Tooling:
    - `cli/scripts/unpack-tools.cjs`: removes `win32/arm64 → x64-win32` mapping.
    - `cli/scripts/ripgrep_launcher.cjs`: removes the `rg.exe` fallback path on Windows and always uses `rg`.
  - Tests/config:
    - Deletes `cli/scripts/__tests__/unpack-tools.test.ts`.
    - Removes a Windows-specific assertion from `cli/scripts/__tests__/ripgrep_launcher.test.ts`.
    - `cli/vitest.config.ts`: stops running `scripts/**/*.test.ts` entirely (only `src/**/*.test.ts` remain).
  - Test env setup:
    - `cli/src/test-setup.ts`: removes the “ensure writable HAPPY_HOME_DIR / CLAUDE_CONFIG_DIR fallback under tmpdir” logic.
- Risk notes:
  - Removing the writable-dir fallback can reintroduce flaky test behavior in sandboxed runners where `$HOME` is not writable.
  - Dropping `scripts/**/*.test.ts` from vitest means script/tooling behavior is no longer covered by tests; if we still care about those scripts, they should be covered elsewhere or re-enabled selectively.

### Verdict

- Keep/squash/split/drop/reorder: keep as a revert **only if** we truly want to drop Windows arm64 support and accept the test coverage/env fallback removal. Otherwise, during rewrite we should:
  - either drop both the original change and this revert (net no-op),
  - or re-apply a corrected version of the Windows arm64 support with a safer approach.
- Depends on: the upstream state we want for Windows tool handling and test runner constraints.

### Rewrite Commit Message

- Keep as a `revert:` commit during rewrite *only if* we keep the revert conceptually.
- If the end state should simply omit the original change, prefer squashing away the original + revert pair instead of carrying a revert commit forward.

---

## 141 2026-01-22 ab35b47ff699 `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ab35b47ff699 patchFile=/tmp/leeroy-wip-pass2/141.patch patchSha256=d4e54e806288f0c6edb2ab4d786de5f4684a7e297032bb3494763ae893eebf17 patchBytes=53960 patchLines=1080 notesReviewed=yes reviewedAt=2026-01-24T22:54:06+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Inactive resume finalization: remove HAPPY_INITIAL_MESSAGE flow in favor of pending-queue + spawn-only; add experimental Codex resume gating (app+daemon) + resume-server install/status surfaces. Large mixed; keep in resume PR but split during rewrite (needsSplit=yes).

- Subject: fix(resume): make inactive resume reliable; gate Codex resume
- Reasons: multiple major areas: cli, expo-app; safety/security-sensitive area

### Manual Review Notes

- Diffstat: `17 files changed, 483 insertions(+), 89 deletions(-)`
- Commit message/body vs diff: matches the intent. This is an end-to-end reliability + safety hardening pass for inactive-session resume, especially around Codex being experimental.
- Major changes (by subsystem):
  - **Resume flow correctness (stop sending message payload via spawn)**
    - Removes the `HAPPY_INITIAL_MESSAGE` mechanism from both Claude and Codex runners.
      - Previously, inactive-resume could pass a message via env var to the spawned agent process.
      - Now, the doc (`expo-app/INACTIVE_SESSION_RESUME.md`) and code align on a safer flow:
        - enqueue the user message as “server-pending” first (preserves history),
        - then send a `resume-session` RPC that only spawns the agent (no message payload),
        - the agent then pops pending messages and delivers them.
  - **Codex resume gating + tool selection**
    - App-side gating:
      - Adds `expCodexResume` setting and uses it (behind `experiments`) to compute `allowCodexResume`.
      - `SessionView.tsx` switches from `canResumeSession(...)` to `canResumeSessionWithOptions(..., { allowCodexResume })`.
      - `utils/agentCapabilities.ts` changes Codex resume from “always allowed” to “allowed only when explicitly enabled”.
    - CLI/daemon-side gating:
      - `cli/src/utils/agentCapabilities.ts` changes upstream default to Claude-only vendor resume.
      - Adds `isExperimentalCodexVendorResumeEnabled()` via `HAPPY_EXPERIMENTAL_CODEX_RESUME` env var and a per-spawn override `allowExperimentalCodex`.
      - `daemon/run.ts` now calls `supportsVendorResume(agent, { allowExperimentalCodex })` (fail-closed by default for Codex).
      - For experimental Codex spawns, daemon sets `HAPPY_EXPERIMENTAL_CODEX_RESUME=1` in the child env.
    - Codex MCP client hardening for “resume binary” selection:
      - `cli/src/codex/codexMcpClient.ts` now:
        - accepts `{ command?: string }` and uses `execFileSync(command, ['--version'])` (safer than shelling `execSync('codex --version')`),
        - connects to MCP using the chosen command (system `codex` for normal, alternate “resume build” when resuming).
      - `cli/src/codex/runCodex.ts` selects the Codex command:
        - normal: `'codex'`
        - resume requested (`--resume`): requires experimental enablement and a resume binary:
          - either `HAPPY_CODEX_RESUME_BIN`, or
          - default under `~/.happy/.../tools/codex-resume/node_modules/.bin/(codex|codex.cmd)`
        - errors with an actionable message if missing (fail-closed).
  - **Plumbing: new spawn params**
    - Adds/threads `experimentalCodexResume?: boolean` through:
      - `expo-app` spawn payload (`sync/spawnSessionPayload.ts`) and ops (`sync/ops.ts`),
      - machine RPC handling (`cli/src/api/apiMachine.ts`),
      - daemon spawn (`cli/src/daemon/run.ts`),
      - common handler types (`cli/src/modules/common/registerCommonHandlers.ts`).
  - **Codex resume installer (experimental)**
    - `cli/src/modules/common/registerCommonHandlers.ts` adds:
      - `codex-resume-status`: checks if the resume binary exists and reports version (via execFile).
      - `codex-resume-install`: installs a specified npm package into a Happy-owned prefix (`~/.happy/.../tools/codex-resume`) using `npm install --prefix ... <installSpec>`, writes logs to `configuration.logsDir`, and persists `install-state.json` with the last log path.
    - This is intentionally “user initiated” and uses `execFile` (no shell) but still carries typical “install arbitrary package” risk; the install spec should be controlled by trusted UX.
  - **Docs/tests**
    - Adds tests for `supportsVendorResume` gating on the CLI side.
    - Updates inactive-resume doc to match the new pending-message + spawn-only flow.

### Verdict

- Keep/squash/split/drop/reorder: keep, but split during rewrite (large cross-cutting change).
  - Suggested split:
    1) `refactor(resume): enqueue pending message before resume-session spawn` (drop HAPPY_INITIAL_MESSAGE)
    2) `feat(resume): add experimental codex resume gating (app + daemon)` (expCodexResume + allow flags)
    3) `feat(codex): support alternate codex resume binary and execFile version detection`
    4) `feat(tools): add codex-resume install/status RPCs` (machine UI can call these)
- Depends on:
  - pending message infrastructure on the server/agent side behaving as documented,
  - consistent gating between app settings, daemon env, and codex runner behavior.

### Rewrite Commit Message

- Keep subject (bucket): `fix(resume): make inactive resume reliable; gate Codex resume`
- If split, use the subjects suggested above.

---

## 142 2026-01-22 aa9b11684389 `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=aa9b11684389 patchFile=/tmp/leeroy-wip-pass2/142.patch patchSha256=669155234b852409456b69b513c95fa35fef784e6c456c26ff694a323d494590 patchBytes=1017 patchLines=21 notesReviewed=yes reviewedAt=2026-01-24T22:54:32+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Resume-server installer reliability: increase npm install execFile buffer to avoid maxBuffer failures; keep adjacent to 141/144/146.

- Subject: fix(codex): increase installer output buffer
- Reasons: reliability; avoids `execFile` buffer overflow when `npm install` emits large output (common in verbose installs)

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `cli/src/modules/common/registerCommonHandlers.ts`:
    - Adds `maxBuffer: 50 * 1024 * 1024` to the `execFileAsync('npm', ['install', ...])` call used by the Codex resume installer RPC.
    - Keeps the existing 15-minute timeout.
- Coherence / grouping notes:
  - This is a small “make the feature usable” follow-up for the Codex installer surfaced in the surrounding resume commits (141/144+). It should be grouped with those commits, not separated far away.

### Verdict

- Keep/squash/split/drop/reorder: keep (small reliability fix).
- Depends on: the Codex resume installer machinery added in the surrounding commits.

### Rewrite Commit Message

- Keep subject: `fix(codex): increase installer output buffer`

---

## 143 2026-01-22 c4beebbfa375 `app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c4beebbfa375 patchFile=/tmp/leeroy-wip-pass2/143.patch patchSha256=818cb06f839d355eb6f859700abc520dde89360b5e2bfcc9c531c81ec30d30a2 patchBytes=9499 patchLines=151 notesReviewed=yes reviewedAt=2026-01-24T22:54:52+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Resume UX gating: add expCodexResume experiment toggle + translations; keep with inactive resume/Codex resume gating PR.

- Subject: feat(ui): add Codex resume experiment toggle
- Reasons: exposes Codex resume gating as an explicit experiment flag in the UI

### Manual Review Notes

- Diffstat: `10 files changed, 26 insertions(+)`
- What changed:
  - `expo-app/sources/app/(app)/settings/features.tsx`:
    - Adds `expCodexResume` setting and renders a new toggle row under experiments.
  - Adds localized strings for the toggle label/subtitle across translations (`en`, `ca`, `es`, `it`, `ja`, `pl`, `pt`, `ru`, `zh-Hans`).
- Coherence / grouping notes:
  - This should land adjacent to the commits that actually honor `expCodexResume` (gating Codex resume behavior). By itself it is harmless UI plumbing.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none (UI-only; behavior is controlled by downstream consumers of the setting).

### Rewrite Commit Message

- Keep subject: `feat(ui): add Codex resume experiment toggle`

---

## 144 2026-01-22 0140ae276869 `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=0140ae276869 patchFile=/tmp/leeroy-wip-pass2/144.patch patchSha256=da1d68ddbd6e718dcee9bde45d79db2fdb031e3e40cdee7c3204d35123ac5c84 patchBytes=20922 patchLines=449 notesReviewed=yes reviewedAt=2026-01-24T22:55:39+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Resume-server installer refactor: switch to install-dep + mcp-server wrapper binary (codex-mcp-resume) and update app ops/settings accordingly; keep with 141/146.

- Subject: refactor(codex): install mcp resume server via install-dep
- Reasons: multiple major areas: cli, expo-app; safety/security-sensitive area

### Manual Review Notes

- Diffstat: `5 files changed, 211 insertions(+), 89 deletions(-)`
- What changed (high-level):
  - Refactors the experimental Codex resume tooling from bespoke “codex-resume install” RPCs into a more generic “install dependency into Happy-owned prefix” pattern, and switches the resume binary to a dedicated MCP server wrapper (`codex-mcp-resume`) instead of a replacement `codex` CLI.
- Key changes:
  - `cli/src/codex/codexMcpClient.ts`:
    - Adds a spawn mode:
      - `mode: 'codex-cli'` (default): run system Codex CLI and pass the appropriate subcommand (`mcp` / `mcp-server`) based on detected version.
      - `mode: 'mcp-server'`: run a dedicated MCP server binary directly (args are provided as-is).
    - This allows resume to use a separate MCP server wrapper binary without impersonating the user’s `codex` CLI.
  - `cli/src/codex/runCodex.ts`:
    - For normal sessions: uses `{ mode: 'codex-cli', command: 'codex' }`.
    - For resume sessions: selects a `codex-mcp-resume` binary:
      - supports env overrides: `HAPPY_CODEX_RESUME_MCP_SERVER_BIN` (preferred) and legacy `HAPPY_CODEX_RESUME_BIN`,
      - searches in both new and legacy install prefixes:
        - `~/.happy/.../tools/codex-mcp-resume/node_modules/.bin/(codex-mcp-resume|.cmd)`
        - `~/.happy/.../tools/codex-resume/...` (legacy fallback)
      - then instantiates `CodexMcpClient({ mode: 'mcp-server', command })` for resume.
  - `cli/src/modules/common/registerCommonHandlers.ts`:
    - Introduces a generic `install-dep` concept (initially only `dep='codex-mcp-resume'`):
      - installs a package into a Happy-owned prefix via `npm install --prefix <dir> <installSpec>`,
      - writes install logs to `configuration.logsDir`,
      - persists install state and supports legacy fallback state paths.
    - Adds a default install spec for the resume MCP server wrapper (fork-specific): `@leeroy/codex-mcp-resume@happy-codex-resume`.
    - Increases exec buffer (`maxBuffer`) to tolerate large npm output.
  - `expo-app/sources/sync/ops.ts` + `expo-app/sources/sync/settings.ts`:
    - Adjusts the app-side RPC calls/IDs and settings wiring to match the new installer approach (details continue in subsequent commits 146+).
- Coherence:
  - This is a sensible direction: separate resume tooling from the user’s system codex, and move toward a generic install/status surface.
- Risk notes:
  - Installing via npm remains inherently “runs arbitrary package install scripts”; this should be controlled behind explicit user intent and trusted install specs.
  - The fallback to legacy paths is helpful for upgrades but adds branching complexity; ensure later commits keep status reporting unambiguous.

### Verdict

- Keep/squash/split/drop/reorder: keep, but group/squash with the surrounding resume tooling commits (141, 146, 148+) so the installer + status UX is coherent end-to-end.
- Depends on:
  - npm being available on the machine,
  - the `codex-mcp-resume` package producing the expected executable in `.bin`,
  - consistent gating (experimental flags) from app → daemon → codex runner.

### Rewrite Commit Message

- Keep subject: `refactor(codex): install mcp resume server via install-dep`

---

## 145 2026-01-22 2ec41b621b54 `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2ec41b621b54 patchFile=/tmp/leeroy-wip-pass2/145.patch patchSha256=efd08aabefa137e9e983d4c5efcbe2d8e04c093973a6a79dd95ff1ed93e6a9cd patchBytes=1848 patchLines=39 notesReviewed=yes reviewedAt=2026-01-24T22:56:00+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Resume-server Windows fix: detect installed binary with F_OK on win32 (X_OK unreliable); keep adjacent to 144/146.

- Subject: fix(codex): detect resume binary on Windows
- Reasons: correctness on Windows; `fs.access(..., X_OK)` is not reliable/meaningful for Windows executables

### Manual Review Notes

- Diffstat: `1 file changed, 4 insertions(+), 3 deletions(-)`
- What changed:
  - `cli/src/modules/common/registerCommonHandlers.ts`:
    - Introduces `accessMode`:
      - on Windows: `fsConstants.F_OK` (exists),
      - elsewhere: `fsConstants.X_OK` (executable).
    - Uses that mode when checking if the Codex resume binary exists (primary vs legacy paths) and when selecting the actual `binPath`.
- Coherence / grouping notes:
  - This is a small platform fix that should stay adjacent to the “dep-status / installer” Codex resume tooling commits (141/144/146+).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the Codex resume binary search paths and installer logic in surrounding commits.

### Rewrite Commit Message

- Keep subject: `fix(codex): detect resume binary on Windows`

---

## 146 2026-01-22 e1deb6db8ddb `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e1deb6db8ddb patchFile=/tmp/leeroy-wip-pass2/146.patch patchSha256=9b831cce3cd6420cf89ba5f4b8c0e3c726826fc682812c897227b487c25ba544 patchBytes=17863 patchLines=364 notesReviewed=yes reviewedAt=2026-01-24T22:56:28+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Resume-server API consolidation: add dep-status + install-dep and drop codex-resume-specific RPCs; update machine UI + ops to use generic dep APIs.

- Subject: refactor(codex): add dep-status and drop codex-resume RPCs
- Reasons: multiple major areas: cli, expo-app

### Manual Review Notes

- Diffstat: `3 files changed, 125 insertions(+), 83 deletions(-)`
- What changed:
  - Replaces bespoke “codex resume status/install” RPCs with generic dependency status/install RPCs.
  - `cli/src/modules/common/registerCommonHandlers.ts`:
    - Removes `codex-resume-status` response types and deprecates the old RPC handler.
    - Adds `dep-status`:
      - currently supports only `dep='codex-mcp-resume'` (fails on unknown deps).
      - Reports:
        - `installed` + `binPath` (checks both new and legacy install prefixes),
        - `installedVersion` (reads from installed package.json under the prefix),
        - `latestVersion` for a configured npm dist-tag (via `npm view <pkg>@<dist-tag> version`),
        - `distTag` + `lastInstallLogPath`.
    - Improves install error messaging when the default dist-tag isn’t available yet (“No matching version found” hint).
    - Deletes the deprecated `codex-resume-install` handler (now: use `install-dep`).
  - `expo-app/sources/app/(app)/machine/[id].tsx`:
    - Switches from `machineCodexResumeStatus/machineCodexResumeInstall` to `machineDepStatus/machineInstallDep`.
    - Updates types accordingly (`CodexResumeStatus` → `DepStatus`).
  - `expo-app/sources/sync/ops.ts`:
    - Renames/plumbs the underlying RPC calls for `dep-status` and `install-dep` (and removes the codex-resume-specific wrappers).
- Coherence:
  - Good consolidation step: the UI (machine screen) shouldn’t have to know about special-case RPC names per dependency.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be grouped with the surrounding installer changes (144/141) so the machine UI and daemon handlers agree on one stable API.
- Depends on: npm being available, and the install prefixes/legacy fallback paths remaining stable.

### Rewrite Commit Message

- Keep subject: `refactor(codex): add dep-status and drop codex-resume RPCs`

---

## 147 2026-01-22 231d7ad9bdd9 `app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=231d7ad9bdd9 patchFile=/tmp/leeroy-wip-pass2/147.patch patchSha256=61ec89be876220236ee267e4c6c6ea5a0c0f252db616b2b47740e96118c944a4 patchBytes=959 patchLines=21 notesReviewed=yes reviewedAt=2026-01-24T22:57:00+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Resume-server UX copy: clarify machine screen section label (“Codex resume server”); keep adjacent to 146.

- Subject: fix(ui): clarify Codex resume server label
- Reasons: UX clarity; prevents “Resume Codex” from sounding like an action on a session rather than a machine-level installer/status section

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `expo-app/sources/app/(app)/machine/[id].tsx`:
    - Renames the machine screen section title from `"Resume Codex"` → `"Codex resume server"`.
- Coherence / grouping notes:
  - This is a tiny UI copy tweak and should stay near the machine screen “Codex resume server” UX commits (146/148 and the related capabilities/installer work).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(ui): clarify Codex resume server label`

---

## 148 2026-01-22 8bebf04aca92 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8bebf04aca92 patchFile=/tmp/leeroy-wip-pass2/148.patch patchSha256=5afb051e441a3e49c0f1bc5110d2a3f3a150b01eae0b0ac4afe08bb7898ab763 patchBytes=95216 patchLines=2353 notesReviewed=yes reviewedAt=2026-01-24T22:58:28+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Capabilities protocol final feature: add checklist-based capabilities.* RPC (detect/describe/invoke) and migrate detect-cli/dep-status/install-dep; also extracts preview-env handler module. Keep under capabilities PR but needsSplit=yes: move preview-env extraction to PR15 during rewrite.

- Subject: refactor(cli): modularize capabilities and env preview
- Reasons: large change (+1296/-908); safety/security-sensitive area

### Manual Review Notes

- Introduces a new **checklist-based “capabilities” RPC surface** and moves several “machine introspection” handlers out of `registerCommonHandlers.ts`:
  - Adds `cli/src/modules/common/capabilities/service.ts`:
    - `createCapabilitiesService({ capabilities, checklists, buildContext })` with:
      - `capabilities.describe` → lists descriptors + checklists (`protocolVersion: 1`)
      - `capabilities.detect` → runs a checklist or ad-hoc requests and returns `{ ok, checkedAt, data|error }` per capability
      - `capabilities.invoke` → calls capability methods (install/upgrade style), wrapped with consistent `{ ok, error.code, error.message }`
    - Supports request param overrides via `CapabilitiesDetectRequest.overrides` (merged on top of checklist request params).
  - Adds `cli/src/modules/common/capabilities/types.ts`:
    - Capability IDs are a closed union (`cli.*`, `tool.tmux`, `dep.codex-mcp-resume`).
    - `ChecklistId` union: `new-session`, `machine-details`, `resume.codex`.
  - Adds `cli/src/modules/common/capabilities/checklists.ts` defining the three checklists and (for `resume.codex`) pins the expected dist-tag via `CODEX_MCP_RESUME_DIST_TAG`.
  - Wires handlers in `cli/src/modules/common/capabilities/registerCapabilitiesHandlers.ts`:
    - Registers `capabilities.describe`, `capabilities.detect`, `capabilities.invoke`.
    - Installs a fixed set of capabilities (codex/claude/gemini/tmux + codex-mcp-resume dep).
- Replaces the legacy `detect-cli` RPC with a new **CLI/tmux snapshot detector**:
  - Adds `cli/src/modules/common/capabilities/snapshots/cliSnapshot.ts`:
    - Resolves binaries on the *daemon* PATH (no login shell).
    - Best-effort version probes with timeouts, non-throwing by design, and Windows PATHEXT support.
    - Optional login-status probing:
      - `codex`: `codex login status`
      - `gemini`: `gemini auth status`
      - `claude`: returns `null` (no stable non-interactive status command)
  - Adds `cli/src/modules/common/capabilities/context/buildDetectContext.ts`:
    - Only builds `cliSnapshot` when the requested capabilities require it.
    - Turns on login probing only when any request asks for `includeLoginStatus`.
  - Adds thin per-CLI capability wrappers under `cli/src/modules/common/capabilities/caps/cli*.ts` using a shared helper `caps/cliBase.ts` that conditionally includes `isLoggedIn` only when requested.
- Moves Codex MCP-resume dependency management under capabilities:
  - Adds `cli/src/modules/common/capabilities/deps/codexMcpResume.ts`:
    - Install: `npm install --prefix <happyHomeDir>/tools/codex-mcp-resume <spec>` (writes a timestamped log under `configuration.logsDir` and persists `install-state.json` with the last log path).
    - Status: checks for the bin under the new install dir and a legacy fallback dir (`tools/codex-resume`), reads installed version from `node_modules/<pkg>/package.json`, and optionally queries registry dist-tag via `npm view`.
    - Supports legacy env var `HAPPY_CODEX_RESUME_INSTALL_SPEC` and new `HAPPY_CODEX_MCP_RESUME_INSTALL_SPEC`.
  - Exposes it as capability `dep.codex-mcp-resume` in `caps/depCodexMcpResume.ts`, with `install` + `upgrade` methods via `capabilities.invoke`.
- Extracts `preview-env` into its own module (still registered from `registerCommonHandlers`):
  - Adds `cli/src/modules/common/previewEnv/registerPreviewEnvHandler.ts` which:
    - Validates env keys (caps/underscore convention; max 200 keys)
    - Builds an “effective env” by applying `extraEnv` with the same `${VAR}` expansion semantics used for spawns
    - Applies a daemon-controlled secret policy (`HAPPY_ENV_PREVIEW_SECRETS` = `none|redacted|full`)
    - Determines sensitivity via:
      - forced regex on key name (default `/TOKEN|KEY|SECRET|AUTH|PASS|PASSWORD|COOKIE/i`, overridable via `HAPPY_ENV_PREVIEW_SECRET_NAME_REGEX`)
      - plus UI-provided hints (`sensitiveKeys`)
    - Returns both the value (possibly null/redacted) and metadata about sensitivity + display mode.
- Removes multiple “legacy” RPC handlers from `cli/src/modules/common/registerCommonHandlers.ts` and replaces them with:
  - `registerCapabilitiesHandlers(rpcHandlerManager)`
  - `registerPreviewEnvHandler(rpcHandlerManager)`
  - Deleted handlers include at least: `detect-cli`, plus the codex-mcp-resume install/status plumbing that previously lived inline (and the inline `preview-env` handler).
- Tests:
  - Deletes `cli/src/modules/common/registerCommonHandlers.detectCli.test.ts`.
  - Adds `cli/src/modules/common/registerCommonHandlers.capabilities.test.ts`:
    - Covers `capabilities.describe` + `capabilities.detect` determinism via a temp PATH directory with fake `codex/claude/gemini/tmux` scripts.
    - Validates `includeLoginStatus` behavior and that registry lookups are skipped when `onlyIfInstalled=true` and the dep is not installed.

### Verdict

- Keep/squash/split/drop/reorder: keep, but this is a **major protocol refactor** and is a prime candidate to split during history rewrite:
  - Split A (pure extraction): `refactor(cli): extract preview-env handler module`
  - Split B (new API): `feat(cli): add checklist-based capabilities RPC`
  - Split C (migrations): `refactor(cli): migrate legacy detect-cli/dep-status/install-dep to capabilities` (or keep compatibility aliases here, then remove them later once the app is migrated)
- Coherence: mostly coherent (all in `cli/src/modules/common/*`), but it mixes two large concerns (capabilities registry + env preview extraction) and also includes a protocol-breaking removal of legacy RPC methods.
- Risk / reliability notes:
  - Breaking-change surface: clients must migrate from `detect-cli` / `dep-status` / `install-dep` (and any “detect-capabilities” legacy) to `capabilities.*` and `preview-env`.
  - Ordering constraint: must be adjacent to/precede the expo-app commits that switch the UI to `capabilities.*` (likely 151 and friends), otherwise app↔daemon mismatches will be hard failures.
  - If we want a safer rewrite, consider keeping legacy RPC aliases for one release window (or at least one topic-group) before removing.
- Depends on:
  - Earlier codex resume dependency work (141/144/146) for the conceptual contract (install prefix, logs dir, legacy fallback), though the implementation is now relocated.
  - `RpcHandlerManager` encryption framing used by the tests, and `expandEnvironmentVariables` + `configuration` utilities for runtime behavior.

### Rewrite Commit Message

- Keep subject (acceptable): `refactor(cli): modularize capabilities and env preview`
- Suggested rewrite subjects if split:
  - `feat(cli): add checklist-based capabilities RPC`
  - `refactor(cli): extract preview-env handler`
  - `refactor(cli): move codex-mcp-resume dep install/status under capabilities`

---

## 149 2026-01-22 bcf10af94626 `test`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=bcf10af94626 patchFile=/tmp/leeroy-wip-pass2/149.patch patchSha256=3680c05a9986bb7a88b6cf09ffedd3ca44f0f1c07c8fecde4031e88d02da5828 patchBytes=3896 patchLines=93 notesReviewed=yes reviewedAt=2026-01-24T22:59:07+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=Capabilities UI test alignment: update useCLIDetection hook tests for capabilities snapshot model; keep adjacent to app-side capabilities migration commits.

- Subject: test(ui): update useCLIDetection hook tests for capabilities
- Reasons: keeps tests aligned with the new “capabilities snapshot” data model used by the UI

### Manual Review Notes

- Diffstat: `1 file changed, 23 insertions(+), 27 deletions(-)`
- What changed:
  - `expo-app/sources/hooks/useCLIDetection.hook.test.ts`:
    - Switches mocking from `useMachineDetectCliCache` to `useMachineCapabilitiesCache`.
    - Updates the mocked “loaded” shape from:
      - `state.response.{ clis, tmux }`
      - to `state.snapshot.response.{ protocolVersion, results }` where results are keyed capabilities (e.g. `cli.codex`, `tool.tmux`).
    - Removes the `machineBash` mock (no longer required by the hook under test).
    - Keeps the intent of both tests:
      - tmux availability is present → hook exposes it,
      - tmux missing → treated as unknown/null (older daemon compatibility).
- Test quality notes:
  - These tests assert on stable, user-observable behavior (the hook’s output shape) rather than fragile implementation details, which is appropriate for this kind of refactor.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the capabilities snapshot contract (`protocolVersion: 1`, `results` key naming) remaining stable.

### Rewrite Commit Message

- Keep subject: `test(ui): update useCLIDetection hook tests for capabilities`

---

## 150 2026-01-22 a5cac698f15b `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a5cac698f15b patchFile=/tmp/leeroy-wip-pass2/150.patch patchSha256=3694dd443122580af8b8145d55135c8a8a096631b741e919f13c0b817962250e patchBytes=81312 patchLines=1988 notesReviewed=yes reviewedAt=2026-01-24T23:10:58+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T13:27:40+0100 manualReviewedForPrPlan=yes notes=MessageQueueV1 core (CLI): metadata-backed pending queue w/ inFlight acking via localId, plus discard-on-switch policy + ACP lifecycle edges. Very mixed; keep in queue PR but needsSplit=yes (split queue semantics vs switching prompt vs ACP lifecycle vs idle-wake util).

- Subject: feat(cli): harden session queue, switching, and lifecycle
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Commit message body is informative but was authored with literal `\n` escapes (so the log shows `...\n- ...` on one line). During rewrite, preserve the content but reformat the body as real bullet lines.
- Introduces a **metadata-backed pending message queue** (v1) stored in encrypted session metadata, and changes how remote agents materialize pending UI messages:
  - Adds `cli/src/api/messageQueueV1.ts` + `messageQueueV1.test.ts`:
    - `messageQueueV1: { v: 1, queue: [...], inFlight?: ... }` lives under session metadata.
    - Claim semantics:
      - `claimMessageQueueV1Next(metadata, now)` moves the oldest queued item into `inFlight` (or returns existing `inFlight`).
      - If `inFlight` is older than `60s`, it is re-claimed (crash recovery).
    - Acknowledgement semantics:
      - `clearMessageQueueV1InFlight(metadata, localId)` clears `inFlight` only when the localId matches.
    - Discard semantics:
      - `discardMessageQueueV1All(metadata, { now, reason })` clears queue + inFlight and appends to `messageQueueV1Discarded` (capped; includes `discardedAt` and `discardedReason`).
  - Updates `cli/src/api/types.ts`:
    - Removes client→server `pending-*` socket events and switches to `message({ sid, message, localId? })`.
    - Adds `localId` on update `new-message` payloads and on decrypted `UserMessageSchema`.
    - Adds `metadata.messageQueueV1?: { v: 1, queue: ..., inFlight?: ... }` type (note: `messageQueueV1Discarded` is used but not typed here).
  - Updates `cli/src/api/apiSession.ts`:
    - `popPendingMessage()` no longer calls server `pending-pop`; it now:
      1) Claims the oldest `messageQueueV1` item via `update-metadata` (with version-mismatch retry/backoff).
      2) Emits it into the transcript via `socket.emit('message', { sid, message, localId })`.
      3) Defers clearing `inFlight` until the materialized user message is observed back from the server.
    - On incoming encrypted `new-message` updates:
      - attaches server `localId` onto the decrypted payload
      - emits a dedicated `user-message` event
      - calls `maybeClearPendingInFlight(localId)` to clear `messageQueueV1.inFlight` only after the server has accepted/materialized it.
    - Adds `waitForMetadataUpdate()` (event-based) so idle loops can wake when metadata changes (e.g., new pending enqueue).
    - Adds helpers used by switching flows:
      - `peekPendingMessageQueueV1Preview()` decrypts a small preview of queued items for user prompts.
      - `discardPendingMessageQueueV1All()` updates metadata to discard all pending items.
      - `discardCommittedMessageLocalIds()` persists stable localIds of messages we want the UI to treat as discarded.
    - Adds `cli/src/api/discardedCommittedMessageLocalIds.ts` + tests: capped append-only list in metadata (`discardedCommittedMessageLocalIds`, default cap 500).
- Adds a **safer remote→local switching policy** (Claude local launcher):
  - `cli/src/claude/claudeLocalLauncher.ts` now:
    - checks both local queue (`session.queue`) and server pending queue (`peekPendingMessageQueueV1Preview`)
    - prompts the user via `node:readline` to confirm discarding before switching to local mode
    - discards server pending items first (to avoid replay when returning to remote)
    - records discard markers for committed queued messages via `discardCommittedMessageLocalIds`
    - clears the local queue (`session.queue.reset()`) when confirmed
  - `cli/src/claude/claudeLocalLauncher.test.ts` adds coverage for confirm/decline paths (TTY + mocked readline).
- Propagates stable `localId` through Claude send pipeline:
  - Adds `EnhancedMode.localId` in `cli/src/claude/loop.ts`.
  - Plumbs `message.localId` into `EnhancedMode` in `cli/src/claude/runClaude.ts` (including `/compact` and `/clear`).
- Emits **durable ACP task lifecycle edges**:
  - Claude:
    - `cli/src/claude/session.ts` sends `{ type: 'task_started' }` when thinking flips false→true and `{ type: 'task_complete' }` when it flips true→false (with a stable per-task id).
    - `cli/src/claude/session.test.ts` adds a focused test for the id + edge emission behavior.
  - Codex:
    - Adds `cli/src/codex/utils/codexAcpLifecycle.ts` + tests.
    - `cli/src/codex/runCodex.ts` parses Codex events and forwards `task_started/task_complete/turn_aborted` as ACP messages with a stable task id.
- Makes remote loops more responsive to pending-queue updates:
  - Adds `cli/src/utils/waitForMessagesOrPending.ts` + tests:
    - When the normal queue is empty, it calls `popPendingMessage()`, then waits for either:
      - a queue message, or
      - a metadata update signal (wakes the loop so it can attempt `popPendingMessage()` again).
  - Integrates it into `cli/src/codex/runCodex.ts` and `cli/src/gemini/runGemini.ts`.
- Session metadata now seeds queue support:
  - `cli/src/utils/createSessionMetadata.ts` initializes `metadata.messageQueueV1 = { v: 1, queue: [] }` so the app can detect queue support without capabilities RPC.
  - Adds `cli/src/utils/createSessionMetadata.test.ts` to lock this contract.
- Test harness tweak:
  - `cli/src/test-setup.ts` adds an opt-out env (`HAPPY_CLI_TEST_SKIP_BUILD`) to skip `yarn build` in tests.

### Verdict

- Keep/squash/split/drop/reorder: keep, but this commit is **very mixed** and should almost certainly be split during history rewrite into coherent layers:
  - `feat(cli): add metadata-backed pending queue (messageQueueV1)`
  - `feat(cli): materialize pending queue items into transcript with inFlight acking`
  - `feat(cli): prompt/discard policy for remote→local switching`
  - `feat(cli): emit ACP lifecycle events for claude/codex`
  - `refactor(cli): wait for queue or metadata updates when idle`
  - `test(cli): add coverage for queue + lifecycle`
- Coherence: the overall theme is “reliable delivery + lifecycle”, but it spans API protocol, queue semantics, UX prompts, lifecycle telemetry, and test harness behavior.
- Risk / reliability notes:
  - Protocol-breaking: removes `pending-*` socket events and requires server support for:
    - `update-metadata` CAS semantics (version matching)
    - `message` accepting `localId`
    - updates including `message.localId`
    - unique `(sessionId, localId)` semantics on stored messages (explicitly relied upon for idempotency)
  - Strong ordering constraints: must be paired with the expo-app changes that enqueue into `messageQueueV1` and the server changes that understand/emit `localId`.
  - The inFlight clear-on-observe strategy is an intentional reliability tradeoff (avoid message loss on crash between emit and persist); keep this behavior intact through refactors.
- Depends on:
  - `ApiSessionClient` metadata update delivery (`update-session` events) working reliably.
  - Server/database constraint: unique `(sessionId, localId)` for transcript messages (or equivalent idempotency mechanism).
  - App-side enqueuer writing `metadata.messageQueueV1.queue[*].message` as base64 encrypted payload compatible with this decrypt logic.

### Rewrite Commit Message

- Keep subject (good): `feat(cli): harden session queue, switching, and lifecycle`
- Suggested rewrite subjects (if split):
  - `feat(cli): add metadata-backed pending message queue (messageQueueV1)`
  - `feat(cli): materialize pending messages from metadata queue with inFlight acking`
  - `feat(cli): confirm + discard queued messages when switching to local mode`
  - `feat(cli): emit ACP task lifecycle events for claude and codex`
  - `refactor(cli): wake idle loops on metadata updates`

---

## 151 2026-01-22 3fc704424e8d `app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=3fc704424e8d patchFile=/tmp/leeroy-wip-pass2/151.patch patchSha256=7ed45b3314f7105169d3ac395b48d833d5bac29717fc5b3a7f05ea532840cba0 patchBytes=196858 patchLines=4390 notesReviewed=yes reviewedAt=2026-01-24T23:14:06+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T13:31:51+0100 manualReviewedForPrPlan=yes notes=Mixed mega-commit spanning MessageQueueV1 (app metadata-backed queue + CAS updater + submit-mode policy + discard markers) and Capabilities protocol migration + NewSession wizard extraction. Keep finalPr=PR13 but needsSplit=yes: move capabilities migration + new-session wizard pieces to PR14/PR26 during rewrite; keep queue+submit-mode+discard-markers in PR13.

- Subject: feat(app): add pending queue, discard markers, and capabilities
- Reasons: too many files (44)

### Manual Review Notes

- Commit message body is informative but was authored with literal `\n` escapes (same issue as 150). During rewrite, keep the same bullet content but reformat it as real lines.
- Replaces the fragile server-side `pending-*` socket events with an **encrypted metadata-backed queue** aligned with the new CLI implementation (150):
  - Adds `expo-app/sources/sync/messageQueueV1.ts` + tests:
    - `metadata.messageQueueV1: { v: 1, queue: [...], inFlight?: ... }`
    - Helpers to enqueue/update/delete queued items by `localId` (stable id).
    - Discard flow: `discardMessageQueueV1All` moves queued+inFlight into `metadata.messageQueueV1Discarded` (with `discardedAt` + `discardedReason`), clears queue, and caps discarded history.
    - Restore/delete helpers for discarded items (`restoreMessageQueueV1DiscardedItem`, `deleteMessageQueueV1DiscardedItem`).
  - Updates `expo-app/sources/sync/storageTypes.ts`:
    - Zod schema now supports `messageQueueV1`, `messageQueueV1Discarded`, and `discardedCommittedMessageLocalIds`.
    - Adds `optimisticThinkingAt` on `Session` and a typed `DiscardedPendingMessage` shape.
    - Adds a focused schema test: `storageTypes.discardedCommitted.test.ts`.
  - Updates `expo-app/sources/sync/apiTypes.ts`:
    - Removes the ephemeral `pending-queue` update type (no more server-driven pending count).
- Adds a **metadata CAS updater** in the app sync layer and rewires pending message CRUD around metadata:
  - `expo-app/sources/sync/sync.ts`:
    - Adds `updateSessionMetadataWithRetry(sessionId, updater)` which uses `update-metadata` with `expectedVersion` and retries once after `refreshSessions()` on version mismatch.
    - `fetchPendingMessages()` now reads from `session.metadata.messageQueueV1.queue` and `session.metadata.messageQueueV1Discarded`, decrypts each payload, and fills local pending+discarded state (even if the server provides no separate pending list).
    - `enqueuePendingMessage()` now:
      - builds an encrypted user payload (same meta as direct send), assigns a stable `localId`, writes it to `messageQueueV1` via `updateSessionMetadataWithRetry`, and mirrors it in local storage for immediate UI.
      - on metadata update failure, removes the optimistic pending entry.
    - `updatePendingMessage()` / `deletePendingMessage()` now mutate `messageQueueV1` (and local state), not server queues.
    - Adds `restoreDiscardedPendingMessage()` / `deleteDiscardedPendingMessage()` that operate on `messageQueueV1Discarded`.
    - Adjusts control handoff logic:
      - introduces `didControlReturnToMobile` helper and uses it when deciding to refetch messages after a controlledByUser transition.
    - Removes debug `console.log` tracing for lifecycle events.
  - `expo-app/sources/sync/storage.ts`:
    - Extends pending storage to keep both `messages` and `discarded`.
    - Adds optimistic “processing” feedback:
      - `markSessionOptimisticThinking()` sets `optimisticThinkingAt` and auto-clears it via timeout.
      - `applySessions()` clears `optimisticThinkingAt` once the server reports `thinking: true`.
      - `sendMessage()` / `enqueuePendingMessage()` call `markSessionOptimisticThinking` immediately and clear it on hard errors.
- Adds a **submit-mode policy** that prefers the queue when direct sends are unreliable:
  - Adds `expo-app/sources/sync/submitMode.ts` + tests:
    - When configured mode is `agent_queue`, switches to `server_pending` if the session supports the queue and:
      - terminal has control (`controlledByUser`)
      - agent is busy (`thinking`)
      - agent is not ready (`agentStateVersion` not set)
      - machine is offline (presence not `"online"`)
    - Preserves explicit `interrupt` and explicit `server_pending`.
  - `sync.submitMessage()` now uses `chooseSubmitMode` and routes `server_pending` to `enqueuePendingMessage(...)`.
- Adds **discard markers for committed transcript messages** (to align with CLI’s “discard committed localIds” write-path):
  - Adds `expo-app/sources/utils/discardedCommittedMessages.ts` + tests:
    - `isCommittedMessageDiscarded(metadata, localId)` checks `metadata.discardedCommittedMessageLocalIds`.
  - Updates `expo-app/sources/components/MessageView.tsx`:
    - dims discarded user bubbles and renders a localized “Discarded” label (`message.discarded`) when the committed message localId is marked discarded.
  - Adds translation key `message.discarded` across locales.
- Refactors CLI detection and machine introspection to the **capabilities protocol** introduced in 148:
  - Adds `expo-app/sources/sync/capabilitiesProtocol.ts` with parsing helpers:
    - `parseCapabilitiesDescribeResponse`, `parseCapabilitiesDetectResponse`, `parseCapabilitiesInvokeResponse`.
    - Defines `CliCapabilityData`, `TmuxCapabilityData`, and `CodexMcpResumeDepData` shapes used by the UI.
  - Updates `expo-app/sources/sync/ops.ts`:
    - Removes `machineDetectCli` and replaces with:
      - `machineCapabilitiesDescribe`
      - `machineCapabilitiesDetect` (with timeouts; longer for registry-heavy checks)
      - `machineCapabilitiesInvoke` (long timeout; install/upgrade operations)
    - Removes `dep-status` / `install-dep` usage in favor of `capabilities.*`.
  - Adds `expo-app/sources/hooks/useMachineCapabilitiesCache.ts`:
    - In-memory per-machine cache with stale-while-revalidate behavior.
    - Merges dep results across calls (preserves registry snapshots) but replaces CLI/tool results to avoid stale resolved paths/versions.
  - Removes `expo-app/sources/hooks/useMachineDetectCliCache.ts` and reworks `useCLIDetection.ts` to read from `useMachineCapabilitiesCache` (drops bash fallback for older daemons; not-supported → unknown).
  - Updates machine/new-session UI components to use the new cache/hook and `capabilities.detect` checklists (`new-session`, `machine-details`, `resume.codex`).
- UX updates around pending queue visibility + management:
  - `expo-app/sources/-session/SessionView.tsx`:
    - Always refetches pending messages when `session.metadataVersion` changes (metadata is now the source of truth for queue/discarded).
    - Updates pending indicator count to fall back to metadata queue length when pending list is not loaded yet.
  - `expo-app/sources/components/PendingMessagesModal.tsx`:
    - Renders a “Discarded messages” section with actions:
      - Re-queue
      - Remove (confirm destructive)
      - Send now (abort current turn, delete discarded entry, then send immediately)
- Updates inactive resume doc + flow:
  - `expo-app/INACTIVE_SESSION_RESUME.md` now describes:
    - enqueue into `session.metadata.messageQueueV1`
    - resume session via machine RPC `spawn-happy-session` (no server change required)
- New-session flow refactor:
  - Adds `expo-app/sources/app/(app)/new/NewSessionWizard.tsx` and updates `new` screens to prefetch capabilities instead of detect-cli.

### Verdict

- Keep/squash/split/drop/reorder: keep, but this commit is very large and spans multiple topics. For history rewrite, strongly consider splitting into coherent groups:
  - `feat(app): add metadata-backed pending queue (messageQueueV1) + metadata CAS updates`
  - `feat(app): add discarded queue + committed discard markers`
  - `feat(app): prefer queue send mode when agent not ready/busy/offline/controlledByUser`
  - `feat(app): migrate machine introspection to capabilities.* (replace detect-cli/dep-status/install-dep)`
  - `refactor(app): extract new session wizard + update machine picker prefetch`
- Coherence: the “pending queue + capabilities” theme is consistent, but the New Session wizard extraction and some machine UI work could stand alone as separate commits in a rewrite.
- Risk / reliability notes:
  - Requires CLI+daemon support for:
    - `update-metadata` CAS semantics (expectedVersion) and update broadcasts (metadataVersion changes drive UI refresh).
    - remote agent consumption of `messageQueueV1` (150’s pop/materialize path).
  - Removing the old server pending queue means older backends/daemons won’t provide pending counts; this commit intentionally shifts the source of truth to metadata (with graceful fallback to “unknown” for unsupported capabilities).
  - Submit-mode policy is safety-oriented: it biases toward queueing when direct send would likely be lost/missed or cause mode bounce.
- Depends on:
  - CLI changes in 150 (messageQueueV1 consumption + discard markers publishing) and 148 (capabilities.* RPCs).
  - Storage schema compatibility across app/cli (metadata fields must line up: `messageQueueV1`, `messageQueueV1Discarded`, `discardedCommittedMessageLocalIds`).

### Rewrite Commit Message

- Keep subject (good): `feat(app): add pending queue, discard markers, and capabilities`
- Suggested rewrite subjects (if split):
  - `feat(app): replace pending-* socket queue with metadata-backed messageQueueV1`
  - `feat(app): add discarded pending queue + committed discard markers`
  - `feat(app): prefer queueing when agent is busy/offline/controlledByUser`
  - `feat(app): migrate machine CLI/dep detection to capabilities.*`
  - `refactor(app): extract new session wizard and capabilities prefetch`

---

## 152 2026-01-22 49893dd7d16a `terminal`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=49893dd7d16a patchFile=/tmp/leeroy-wip-pass2/152.patch patchSha256=6b2581e37dc06e54a24ff31b994563bd41887a3e228e1a0788c0861dc67b508f patchBytes=12596 patchLines=348 notesReviewed=yes reviewedAt=2026-01-24T23:15:23+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR31 plannedAt=2026-01-25T13:40:30+0100 manualReviewedForPrPlan=yes notes=Terminal switching reliability: deterministic remote→local input handling + stdin drain helper with tests; not tmux-specific, keep in terminal-switching PR.

- Subject: fix(terminal): harden remote→local switching input
- Reasons: user-reported terminal corruption/sluggish input after switching remote→local (especially after “space spam”); this aims to make switching deterministic and prevent buffered input leakage

### Manual Review Notes

- Diffstat: `4 files changed, 218 insertions(+), 42 deletions(-)`
- What changed:
  - Makes Ink remote-mode switching input deterministic and adds a safer shortcut:
    - `cli/src/ui/ink/RemoteModeDisplay.tsx`:
      - Extracts a pure `interpretRemoteModeKeypress(...)` function that maps `(state, input, key)` → action.
      - Adds `Ctrl+T` as an immediate “switch to terminal” action (keeps the existing double-space confirmation).
      - Refactors `useInput` handling to use the interpreter (reduces ad-hoc branching).
      - Updates the help text to mention `Ctrl-T`.
    - `cli/src/ui/ink/RemoteModeDisplay.test.ts` (new): unit tests for Ctrl+T and double-space behavior.
  - Adds a best-effort stdin cleanup helper (with tests):
    - `cli/src/utils/terminalStdinCleanup.ts` (new): `cleanupStdinAfterInk({ stdin, drainMs })` which:
      - disables raw mode (best-effort),
      - optionally drains input for `drainMs` by temporarily attaching a `data` listener and calling `resume()`,
      - then detaches the listener and pauses stdin (best-effort).
    - `cli/src/utils/terminalStdinCleanup.test.ts` (new): verifies draining + pause and the non-TTY no-op behavior.
- Coherence / correctness notes:
  - The commit message describes draining buffered input “after the Ink UI unmounts”; in this commit the cleanup helper is introduced and tested, but it is not wired into the remote→local switch path yet (at least not in the diffs shown here). If a follow-up commit integrates it, these should remain adjacent in history.
  - Ordering constraint: commit `150` uses `cleanupStdinAfterInk(...)`. During rewrite, ensure this commit lands before any commit that imports/uses `cli/src/utils/terminalStdinCleanup.ts`.

### Verdict

- Keep/squash/split/drop/reorder: keep (high-value reliability fix + tests).
- Depends on: none, but should stay close to any follow-up that actually invokes the stdin cleanup helper during switching.

### Rewrite Commit Message

- Keep subject: `fix(terminal): harden remote→local switching input`

---

## 153 2026-01-22 0126667539b6 `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=0126667539b6 patchFile=/tmp/leeroy-wip-pass2/153.patch patchSha256=37b2b9ca38d9a57a8c9e8fc86d0c9eaae64486c46229be56c4639631e51402fd patchBytes=4757 patchLines=128 notesReviewed=yes reviewedAt=2026-01-24T23:15:59+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR31 plannedAt=2026-01-25T13:40:30+0100 manualReviewedForPrPlan=yes notes=Terminal mode-switch reliability: forward SIGINT/SIGTERM(/SIGHUP) to Claude binary child to avoid orphaned stdin owners; not tmux-specific, keep with terminal switching PR.

- Subject: fix(claude): forward signals to binary child process
- Reasons: reliability; prevents orphaned Claude processes (multiple stdin readers) during mode switches when Claude is installed as a native binary

### Manual Review Notes

- Diffstat: `2 files changed, 78 insertions(+), 2 deletions(-)`
- What changed:
  - `cli/scripts/claude_version_utils.cjs`:
    - Adds `attachChildSignalForwarding(child, proc = process)` which forwards `SIGTERM`/`SIGINT` (and `SIGHUP` on non-Windows) to the spawned child.
    - Calls this helper in the “binary spawn” path (`spawn(cliPath, ..., { stdio: 'inherit' })`), ensuring mode-switch termination kills the real stdin owner.
    - Exports the helper for tests.
  - `cli/src/scripts/claude_version_utils.signalForwarding.test.ts` (new):
    - Validates that handlers are registered and forwarded kills reach the child.
    - Validates `SIGHUP` is not registered on Windows.
- Risk notes:
  - The helper uses `process.on(...)` (not `once`) and does not remove listeners. If `runClaudeCli` could be invoked multiple times in one process, this would accumulate listeners. If `runClaudeCli` is effectively “one-shot then exit” (which it appears to be), this is acceptable; still worth noting during rewrite.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(claude): forward signals to binary child process`

---

## 154 2026-01-22 39c7b8a56b32 `app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=39c7b8a56b32 patchFile=/tmp/leeroy-wip-pass2/154.patch patchSha256=b876dd3fe35a303fc2178015b06b7498edd5ec493fd3e6bb98e8785b5f475ab6 patchBytes=33250 patchLines=668 notesReviewed=yes reviewedAt=2026-01-24T23:17:24+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T13:40:30+0100 manualReviewedForPrPlan=yes notes=Mixed settings rename: tmux keys belong in TMUX PR; message-send key rename belongs with queue/submit-mode work. Keep finalPr=PR18 but needsSplit=yes and move message-send rename to PR13 during rewrite.

- Subject: refactor(app): rename tmux/message-send settings under session
- Reasons: makes settings naming coherent (“session” owns both message sending policy and tmux terminal preferences) and reduces settings surface duplication

### Manual Review Notes

- Diffstat: `11 files changed, 235 insertions(+), 231 deletions(-)`
- What changed:
  - Renames “terminal tmux” settings keys to “session tmux” keys across the app:
    - e.g. `terminalUseTmux` → `sessionUseTmux`, and related keys (`sessionTmuxSessionName`, `sessionTmuxIsolated`, etc.).
    - Updates consumers (machine screen, new session screen) to read the new keys.
  - Renames message sending mode setting:
    - `messageSendMode` → `sessionMessageSendMode`.
  - Adds a unified “Session” settings screen:
    - `expo-app/sources/app/(app)/settings/session.tsx` (new): combines message sending options + tmux controls in one place.
    - Keeps old routes as aliases:
      - `settings/message-sending.tsx` re-exports from `./session`.
      - `settings/terminal.tsx` is reduced accordingly (and/or becomes an alias depending on the final tree).
  - Migrates old keys:
    - `expo-app/sources/sync/settings.ts` updates parsing/migration logic so old persisted keys map to the new ones.
    - Tests updated accordingly (`settings.spec.ts`, `terminalSettings.spec.ts`, `terminalSettings.ts`).
- Commit-message formatting note:
  - The body contains literal `\\n` escapes; during rewrite, preserve the content but format as real bullet lines.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: settings migration staying correct; should land before/with any UI that expects the new keys.

### Rewrite Commit Message

- Keep subject: `refactor(app): rename tmux/message-send settings under session`

---

## 155 2026-01-22 fcec6c311b6b `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=fcec6c311b6b patchFile=/tmp/leeroy-wip-pass2/155.patch patchSha256=4cdfa859a03d32adecfcaccd43fed2d1bbba0574c855ead2a96b2c10092baf07 patchBytes=14781 patchLines=338 notesReviewed=yes reviewedAt=2026-01-24T23:21:35+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T13:31:51+0100 manualReviewedForPrPlan=yes notes=Sessions UX (cross-device unread): move unread markers from local MMKV to encrypted metadata readStateV1 + focus/blur marking; keep with sessions/message UX PR (unread consistency story). 

- Subject: feat(sync): add cross-device unread markers
- Reasons: correctness/UX; makes “unread” consistent across devices by storing read markers in encrypted session metadata instead of local-only MMKV state

### Manual Review Notes

- Diffstat: `6 files changed, 187 insertions(+), 41 deletions(-)`
- What changed:
  - Adds a new metadata-backed read marker:
    - `expo-app/sources/sync/storageTypes.ts`:
      - Adds `readStateV1` into the encrypted `Metadata` schema (v1 marker with session/pending activity position).
  - Changes how “unread” is computed:
    - `expo-app/sources/sync/unread.ts`:
      - Replaces message-list scanning with `computeHasUnreadActivity({ sessionSeq, pendingActivityAt, lastViewedSessionSeq, lastViewedPendingActivityAt })`.
      - Introduces `computePendingActivityAt(metadata)` (used to include pending-queue activity).
    - `expo-app/sources/sync/unread.test.ts`: updates tests to validate the new semantics.
    - `expo-app/sources/sync/storage.ts`:
      - `useHasUnreadMessages` now reads from session metadata (`readStateV1`) and session counters instead of local message arrays.
  - Adds a sync operation to write read markers:
    - `expo-app/sources/sync/sync.ts`: adds `markSessionViewed(sessionId)` that updates encrypted metadata with a monotonic marker (uses `Math.max` to avoid regressions).
  - Marks sessions viewed more reliably in the UI:
    - `expo-app/sources/-session/SessionView.tsx`:
      - Uses `useFocusEffect` to mark viewed on focus and on blur.
      - Debounces additional “mark viewed” writes when new activity arrives while focused (only when activity moved beyond last marker).
- Risk notes:
  - This introduces additional metadata writes (focus/blur + debounced updates). The debouncing and “only if activity advanced” guard make this reasonable, but it’s worth validating for backend rate limits and for “metadataVersion churn” side effects.
- Commit-message formatting note:
  - The body contains literal `\\n` escapes; during rewrite, preserve the content but format as real bullet lines.

### Verdict

- Keep/squash/split/drop/reorder: keep (core UX correctness improvement + tests).
- Depends on: encrypted metadata update semantics working reliably across devices.

### Rewrite Commit Message

- Keep subject: `feat(sync): add cross-device unread markers`

---

## 156 2026-01-22 8c16ee8f9cef `auth`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8c16ee8f9cef patchFile=/tmp/leeroy-wip-pass2/156.patch patchSha256=6d9c90b140a896689127d8d93f835d828e3869cfdf4c802f940bd066838ea35f patchBytes=7177 patchLines=175 notesReviewed=yes reviewedAt=2026-01-24T23:22:24+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR04 plannedAt=2026-01-25T13:31:51+0100 manualReviewedForPrPlan=yes notes=Auth hardening: tolerate null service_tier in raw usage payload + add unauth route guard (layout-level) with tests; keep in Auth+storage scoping PR.

- Subject: fix(auth): surface auth failures and gate unauth routes
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Fixes a real reliability/observability issue in message normalization:
  - `expo-app/sources/sync/typesRaw.ts`:
    - Changes `usage.service_tier` from `z.string().optional()` to `z.string().nullish()` because upstream payloads can include `service_tier: null`.
    - Goal: avoid dropping entire messages when schema validation fails, which can hide auth failures (e.g., expired OAuth / 401 surfaced as a message/event payload).
  - Adds a targeted regression test in `expo-app/sources/sync/typesRaw.spec.ts` to ensure `service_tier: null` parses successfully.
- Adds an unauthenticated route guard at the app layout level:
  - `expo-app/sources/app/(app)/_layout.tsx`:
    - Uses `useAuth()` + `useSegments()` to redirect unauthenticated users to `/` unless they are on a public route.
    - Returns `null` while redirecting to avoid flashing protected screens for a frame.
  - Adds `expo-app/sources/auth/authRouting.ts` + `authRouting.test.ts`:
    - `isPublicRouteForUnauthenticated(segments)`:
      - normalizes expo-router route groups like `(app)`
      - allows: `index` (root) and `restore/*`
      - blocks everything else (e.g. `new`, `session/:id`, `settings`)
- Adds root-level convenience scripts to support monorepo test/typecheck execution via `happys`:
  - `package.json` adds:
    - `test`: delegates to `yarn --cwd expo-app test`
    - `typecheck`: delegates to `yarn --cwd expo-app typecheck`

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a cohesive auth hardening commit with focused tests.
- Coherence: good (all changes align with “auth correctness + clearer failure surfacing”).
- Risk / reliability notes:
  - The route guard is intentionally conservative; if additional public routes exist (e.g. dedicated `/login` route), they must be explicitly whitelisted in `isPublicRouteForUnauthenticated`.
  - The schema relaxation is narrowly scoped (string-or-nullish) and prevents silent drops of important system messages.
- Depends on:
  - `useAuth()` implementation semantics for `isAuthenticated` (assumed stable).
  - expo-router segment structure; route-group filtering is the correct approach for `(app)` prefixes.

### Rewrite Commit Message

- Keep as-is: `fix(auth): surface auth failures and gate unauth routes`

---

## 157 2026-01-22 320a89e19ca0 `test`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=320a89e19ca0 patchFile=/tmp/leeroy-wip-pass2/157.patch patchSha256=4b1601ef8c003fc12fc3ab0ad3cadfbe2172c712d0e75bedd4582983ed5cc46a patchBytes=1801 patchLines=46 notesReviewed=yes reviewedAt=2026-01-24T23:22:55+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T13:31:51+0100 manualReviewedForPrPlan=yes notes=DevX/validation: root scripts run expo-app+cli+server test/typecheck/build via happys wrappers; keep in Foundations PR.

- Subject: test(happy): run cli and server checks from root scripts
- Reasons: reliability; makes `happys test/typecheck happy` exercise the full monorepo surface (expo-app + cli + server), not just the app

### Manual Review Notes

- Diffstat: `1 file changed, 9 insertions(+), 9 deletions(-)`
- What changed:
  - `package.json` (repo root):
    - Expands root scripts:
      - `test`: `yarn --cwd expo-app test && yarn --cwd cli test && yarn --cwd server test`
      - `typecheck`: `yarn --cwd expo-app typecheck && yarn --cwd cli typecheck && yarn --cwd server build`
    - Also reformats the `workspaces.nohoist` array indentation (no semantic change).
- Coherence / ordering notes:
  - This is conceptually independent but very useful for preventing regressions: it ensures changes in `cli/` and `server/` are validated when using the standard `happys` wrappers.
  - During rewrite, keep it near other “reliability/testing harness” commits, but it does not need to be tied to any feature work.
- Risk notes:
  - Running more suites increases runtime; if this becomes too heavy for developer workflows, a later commit could introduce `test:fast` or split scripts rather than reducing coverage.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: the subprojects having working `yarn test/typecheck/build` scripts.

### Rewrite Commit Message

- Keep subject: `test(happy): run cli and server checks from root scripts`

---

## 158 2026-01-22 3b3609fed434 `rpc`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=3b3609fed434 patchFile=/tmp/leeroy-wip-pass2/158.patch patchSha256=20570c138e4c5fd64eb5e32ce104b830e042b2e16ec5235e0cc87e7c4bb85c37 patchBytes=12225 patchLines=321 notesReviewed=yes reviewedAt=2026-01-24T23:25:15+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T13:31:51+0100 manualReviewedForPrPlan=yes notes=RPC robustness: add structured errorCode=RPC_METHOD_NOT_AVAILABLE while preserving legacy error string; update app ops fallback logic + add tests server/app side; keep with CLI/app reliability PR.

- Subject: fix(rpc): add structured code for missing RPC methods
- Reasons: multiple major areas: expo-app, server

### Manual Review Notes

- Commit message body contains literal `\n\n` escapes (formatting). During rewrite, keep the content but convert to normal paragraphs/bullets.
- Introduces a **structured error code** for “RPC method not available”, while keeping backward compatibility with legacy string-matching clients:
  - Server:
    - `server/sources/app/api/socket/rpcHandler.ts` now returns:
      - `error: 'RPC method not available'` (legacy)
      - `errorCode: 'RPC_METHOD_NOT_AVAILABLE'` (new structured signal)
    - Adds `server/sources/app/api/socket/rpcHandler.spec.ts` to assert the new `errorCode` is present for missing methods.
  - App client:
    - `expo-app/sources/sync/apiSocket.ts` now throws a richer error on failed RPC calls:
      - Uses new `createRpcCallError({ error, errorCode })` to attach `rpcErrorCode` onto the thrown Error instance.
      - Preserves the legacy `.message` string for human-readable logs/UI.
    - Adds `expo-app/sources/sync/rpcErrors.ts` + tests:
      - `createRpcCallError` attaches `rpcErrorCode` when present.
      - `isRpcMethodNotAvailableError` prefers `rpcErrorCode` and falls back to an exact legacy message match (case-insensitive).
    - Updates `expo-app/sources/sync/ops.ts`:
      - `sessionKill` response now includes `errorCode?: string` extracted from thrown Errors.
      - `sessionArchive` fallback logic now prefers `rpcErrorCode` and falls back to the legacy message (for older servers).
    - Adds `expo-app/sources/sync/ops.sessionArchive.test.ts` to validate:
      - fallback via errorCode
      - fallback via legacy message
      - non-fallback on unrelated errors

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a clean robustness upgrade that reduces brittle string matching and improves client/server compatibility.
- Coherence: good (single theme: structured RPC error semantics + backward compatibility).
- Risk / reliability notes:
  - Attaching `rpcErrorCode` onto an `Error` is a pragmatic approach; ensure downstream code doesn’t rely on it always being present.
  - The fallback matcher intentionally uses an exact normalized string; this is safer than substring matching but assumes the legacy message stays stable.
- Depends on:
  - Server and app versions both benefit, but the design is explicitly backward compatible:
    - newer app can still detect method-not-available on older servers via message match
    - older app keeps working on newer servers via the unchanged error string

### Rewrite Commit Message

- Suggested: `fix(rpc): add structured errorCode for missing RPC methods`

---

## 159 2026-01-22 0e2c7928f24c `scanner`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=0e2c7928f24c patchFile=/tmp/leeroy-wip-pass2/159.patch patchSha256=a7c5dc4351fb8acf7a67277f22fa524fdc85630d99a97ed548f765fc3adf8317 patchBytes=6110 patchLines=149 notesReviewed=yes reviewedAt=2026-01-24T23:25:44+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T13:31:51+0100 manualReviewedForPrPlan=yes notes=Claude scanner robustness: tolerate onMessage callback exceptions + regression test; keep with Claude transcript/scanner correctness PR.

- Subject: fix(scanner): tolerate onMessage exceptions
- Reasons: reliability; prevents session scanner crashes (or cascading retries/backoff) when consumer callbacks throw

### Manual Review Notes

- Diffstat: `3 files changed, 106 insertions(+), 2 deletions(-)`
- What changed:
  - Makes the session scanner resilient to consumer errors:
    - `cli/src/claude/utils/sessionScanner.ts`:
      - Wraps `opts.onMessage(file)` in a try/catch.
      - Logs a debug message when the callback throws and continues scanning.
  - Adds a focused regression test:
    - `cli/src/claude/utils/sessionScanner.onMessageErrors.test.ts` (new):
      - Creates a temp Claude project transcript directory (`CLAUDE_CONFIG_DIR` override),
      - writes a `.jsonl` message,
      - ensures `onMessage` throwing doesn’t crash the scanner (asserts a debug log is emitted).
  - Documents sync invalidation semantics:
    - `cli/src/utils/sync.ts`:
      - Adds explicit docs for `InvalidateSyncOptions.onError` semantics and for the coalescing/failure behavior of `InvalidateSync` (notably: `invalidateAndAwait()` always resolves).
- Coherence / grouping notes:
  - This is mostly scanner robustness. The `InvalidateSync` documentation is related (scanner usage can be wrapped in invalidation flows), but it’s still arguably a second concern. During rewrite, it can either remain here (as explanatory context for the failure mode) or be split into a pure-doc commit.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(scanner): tolerate onMessage exceptions`

---

## 160 2026-01-22 7fbe1f1cc727 `permission`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7fbe1f1cc727 patchFile=/tmp/leeroy-wip-pass2/160.patch patchSha256=70c3ff59d267a94b1219febb476c6aa6147a0fba1cb3b0551f85f80d268c218f patchBytes=10264 patchLines=226 notesReviewed=yes reviewedAt=2026-01-24T23:26:19+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T13:31:51+0100 manualReviewedForPrPlan=yes notes=Permission arbitration correctness: avoid bumping permissionModeUpdatedAt on no-op updates (Claude/Codex/Gemini) via shared helper + tests; keep with permission framework PR.

- Subject: fix(permission): avoid no-op permissionModeUpdatedAt bumps
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Commit message body contains literal `\n\n` escapes; during rewrite, keep content but normalize formatting.
- Fixes a subtle but high-impact reliability issue: **don’t bump `permissionModeUpdatedAt` unless the effective permission mode actually changes**.
  - Why it matters: the app merges/chooses permission mode based on `permissionModeUpdatedAt` (“latest wins”). Repeated no-op bumps can incorrectly override a user’s most recent explicit selection.
- Claude path:
  - `cli/src/claude/session.ts`:
    - `Session.setLastPermissionMode` becomes a no-op when `mode === this.lastPermissionMode`.
  - `cli/src/claude/session.test.ts` adds a regression test that:
    - calls `setLastPermissionMode` repeatedly with the same value
    - asserts only the actual change produces a metadata update (and preserves the first “plan” timestamp).
- Codex + Gemini path:
  - Adds `cli/src/utils/permissionModeMetadata.ts` + tests:
    - `maybeUpdatePermissionModeMetadata({ currentPermissionMode, nextPermissionMode, updateMetadata, nowMs })`
    - returns `{ didChange, currentPermissionMode }`
    - only calls `updateMetadata` (and stamps `permissionModeUpdatedAt`) when the mode changed.
  - `cli/src/codex/runCodex.ts` + `cli/src/gemini/runGemini.ts`:
    - replace unconditional `session.updateMetadata({ permissionMode, permissionModeUpdatedAt: Date.now() })` with `maybeUpdatePermissionModeMetadata`.
    - only logs “permission mode updated …” and updates Gemini’s permission handler when a change actually occurred.

### Verdict

- Keep/squash/split/drop/reorder: keep; important correctness fix for permission-mode arbitration across devices/UI.
- Coherence: good (single behavioral fix + shared helper + tests).
- Depends on:
  - The app-side merge logic that uses `permissionModeUpdatedAt` (earlier permission persistence commits 065–067) — this commit should be kept near those topics in a rewrite.
  - `Session.updateMetadata` CAS semantics remaining stable (this commit reduces churn, so it is strictly safer).

### Rewrite Commit Message

- Keep as-is: `fix(permission): avoid no-op permissionModeUpdatedAt bumps`

---

## 161 2026-01-22 2026fbab5cb3 `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2026fbab5cb3 patchFile=/tmp/leeroy-wip-pass2/161.patch patchSha256=31517c689a995f3333abd75a8fb77a7ec2f4741c754754505d9c5935985df95f patchBytes=1126 patchLines=23 notesReviewed=yes reviewedAt=2026-01-24T23:26:49+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Claude local-runner correctness: treat -c as --continue for session control detection; keep with Claude session handling/transcript correctness PR (adjacent to other -c/-r fixes).

- Subject: fix(claude): accept -c as continue flag
- Reasons: correctness; treats the short `-c` flag as equivalent to `--continue` for “user provided session control” detection

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `cli/src/claude/claudeLocal.ts`:
    - Extends `hasContinueFlag` detection to include `-c` in addition to `--continue`.
    - Keeps existing `--resume` / `-r` detection.
- Coherence / grouping notes:
  - Small, focused bugfix; good candidate to keep as a standalone commit near other Claude local-runner correctness fixes.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(claude): accept -c as continue flag`

---

## 162 2026-01-22 ee0bec2a0b90 `resume`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ee0bec2a0b90 patchFile=/tmp/leeroy-wip-pass2/162.patch patchSha256=40ff48cf0193fef5f9b52f235624fbe1d558451bbc5a579c90dc1594c515a0b5 patchBytes=5105 patchLines=141 notesReviewed=yes reviewedAt=2026-01-24T23:27:08+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Docs cleanup for resume: delete inactive resume design doc; likely squash into broader resume docs reorg or drop only if doc is intentionally superseded elsewhere.

- Subject: Delete INACTIVE_SESSION_RESUME.md
- Reasons: non-Conventional-Commits subject

### Manual Review Notes

- Deletes `expo-app/INACTIVE_SESSION_RESUME.md` (127 lines).
  - This file was a design doc describing the “inactive session resume” flow; it was also updated earlier in this branch (151) to reflect the metadata-backed queue approach.
  - No replacement doc is added in this commit (pure deletion).

### Verdict

- Keep/squash/split/drop/reorder: likely squash into a broader “docs cleanup/reorg” topic, or drop if we still want to keep the design rationale in-repo.
- Coherence: fine (single-file deletion), but it’s potentially information-destructive.
- Depends on: nothing technically, but decision depends on whether this doc was intentionally superseded elsewhere (e.g., moved to a consolidated docs folder).

### Rewrite Commit Message

- Suggested: `docs(resume): remove inactive session resume design doc` (or keep the deletion as part of a “docs reorg” commit)

---

## 163 2026-01-22 84df0d260513 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=84df0d260513 patchFile=/tmp/leeroy-wip-pass2/163.patch patchSha256=647e0bf42ed51f9ea03236552fffbc44c4b31a662983ae7322dde43cfd59088b patchBytes=7646 patchLines=170 notesReviewed=yes reviewedAt=2026-01-24T23:27:39+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Expo install stability: deterministic postinstall (hoisted patch-package support) + libsodium-wrappers ESM patch + related typing fix; keep with Expo web/build stability PR.

- Subject: fix(expo-app): stabilize postinstall and libsodium patches
- Reasons: install reliability across Yarn workspace hoisting layouts; fixes a missing libsodium ESM file and cleans up obsolete patches

### Manual Review Notes

- Diffstat: `6 files changed, 69 insertions(+), 29 deletions(-)`
- What changed:
  - Makes postinstall deterministic under workspace hoisting:
    - `expo-app/package.json`: replaces the inline postinstall with `node ./tools/postinstall.mjs`.
    - `expo-app/tools/postinstall.mjs` (new):
      - Locates `patch-package` either under `expo-app/node_modules` or the repo root `node_modules`.
      - Runs patch-package from the repo root (`cwd: repoRootDir`) with `--patch-dir expo-app/patches` so hoisted packages can still be patched.
      - Runs `npx setup-skia-web public` in `expo-app/` afterwards.
  - Patch maintenance / libsodium correctness:
    - Removes `expo-app/patches/@more-tech+react-native-libsodium+1.5.5.patch` (obsolete given dependency version changes).
    - Adds `expo-app/patches/libsodium-wrappers+0.7.16.patch` to restore a missing `dist/modules-esm/libsodium.mjs` entrypoint.
    - Tightens typing for the web adapter in `expo-app/sources/encryption/libsodium.lib.web.ts` (casts export to `typeof sodiumType`).
  - Updates `yarn.lock`:
    - Bumps `react-test-renderer` to `19.1.0` and adds `@types/react-test-renderer@19.1.0` (plus related dependency graph changes).
- Commit-message formatting note:
  - The body contains literal `\\n` escapes; during rewrite, preserve content but format as real bullet lines.
- Coherence / split notes:
  - The lockfile update might be a byproduct of the Expo-app changes. During rewrite, decide whether to keep it here (if required for tests/build) or split dependency bumps into a dedicated commit.

### Verdict

- Keep/squash/split/drop/reorder: keep (high-value install stability).
- Depends on:
  - `patch-package` being installed somewhere in the workspace (script fails closed if not found),
  - `setup-skia-web` remaining a valid postinstall step.

### Rewrite Commit Message

- Keep subject: `fix(expo-app): stabilize postinstall and libsodium patches`

---

## 164 2026-01-23 544f9711b6cf `app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=544f9711b6cf patchFile=/tmp/leeroy-wip-pass2/164.patch patchSha256=653e794fc7d88d4cb7dcfbb9ce3183711c1676538f59be901a03b7e1dab8eea8 patchBytes=14784 patchLines=402 notesReviewed=yes reviewedAt=2026-01-24T23:28:17+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Tool UX: respond to AskUserQuestion via interaction.respond with back-compat fallback; ExitPlanMode uses allow/deny without extra chat message; keep with AskUserQuestion/ExitPlan native handling PR (requires 158 errorCode support).

- Subject: fix(ui): respond to plan/question tools via session RPC
- Reasons: correctness + UX; avoids extra chat messages and uses the intended session interaction RPCs with backward-compatible fallbacks

### Manual Review Notes

- Diffstat: `5 files changed, 295 insertions(+), 14 deletions(-)`
- What changed:
  - AskUserQuestion tool now responds via a dedicated RPC:
    - `expo-app/sources/components/tools/views/AskUserQuestionView.tsx`:
      - Requires `tool.permission.id` as `toolCallId`.
      - Calls `sessionInteractionRespond(sessionId, { toolCallId, responseText })`.
      - If RPC is unsupported (detected via `isRpcMethodNotAvailableError`), falls back to:
        - `sessionDeny(sessionId, toolCallId)` (cancel tool),
        - `sync.sendMessage(sessionId, responseText)` (send answers as a normal user message).
    - `expo-app/sources/components/tools/views/AskUserQuestionView.test.ts` (new): asserts the RPC path submits without calling deny/sendMessage.
  - ExitPlanMode tool now uses permission RPC only:
    - `expo-app/sources/components/tools/views/ExitPlanToolView.tsx`:
      - Approves/rejects via `sessionAllow`/`sessionDeny` and does not send a follow-up chat message.
    - `expo-app/sources/components/tools/views/ExitPlanToolView.test.ts` (new): asserts approve uses allow and does not call sendMessage.
  - Adds ops wiring for the new RPC:
    - `expo-app/sources/sync/ops.ts`: adds `sessionInteractionRespond(...)`.
- Coherence / ordering notes:
  - This commit relies on the structured RPC error handling introduced earlier (158). During rewrite, keep 158 immediately before this tool-UX layer so the fallback logic is stable and intentional.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: server/daemon implementing `interaction.respond` (fallback keeps older stacks working).

### Rewrite Commit Message

- Suggested subject: `fix(ui): answer AskUserQuestion via interaction.respond with back-compat fallback`

---

## 165 2026-01-23 8b3f39f22664 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8b3f39f22664 patchFile=/tmp/leeroy-wip-pass2/165.patch patchSha256=1161d8d015498f4d4565a2f1ff3869a340af8f3908d3e04f6e9fea4251a63cc6 patchBytes=11312 patchLines=284 notesReviewed=yes reviewedAt=2026-01-24T23:28:57+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Tool plumbing (CLI): add interaction.respond RPC for AskUserQuestion by injecting tool_result into active Claude remote prompt; keep adjacent to 164 so UI+CLI land end-to-end.

- Subject: feat(cli): add interaction.respond for AskUserQuestion
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Adds a session RPC to let the UI **answer AskUserQuestion-style tool calls** during Claude remote sessions by injecting a `tool_result` into the Claude prompt stream.
- `cli/src/claude/claudeRemote.ts`:
  - Adds an optional `setUserMessageSender` hook that exposes a function to push `SDKUserMessage` instances into the `PushableAsyncIterable` driving the Claude prompt stream.
  - Clears the sender on exit (`finally`) to prevent stale injection after the prompt ends.
- `cli/src/claude/claudeRemoteLauncher.ts`:
  - Registers a new RPC handler: `interaction.respond`.
    - Validates `toolCallId` (required non-empty string) and `responseText` (required string).
    - Errors if there is no active remote prompt (`no active Claude remote prompt`).
    - Calls `handleClaudeInteractionRespond({ toolCallId, responseText, approveToolCall, pushToolResult })`.
  - Wires `setUserMessageSender` into the `claudeRemote(...)` call so the launcher can inject mid-turn messages.
- `cli/src/claude/utils/interactionRespond.ts` + test:
  - `createClaudeToolResultUserMessage(toolCallId, responseText)` constructs a `SDKUserMessage` containing `tool_result` with `tool_use_id`.
  - `handleClaudeInteractionRespond` approves the tool call then pushes the tool_result user message.
- `cli/src/claude/utils/permissionHandler.ts`:
  - Refactors permission response handling into `applyPermissionResponse`.
  - Adds `approveToolCall(toolCallId)` which synthesizes an “approved” permission response for that id.
  - Changes the `permission` RPC handler to return `{ ok: true }` (ack) instead of void, and reuses `applyPermissionResponse`.

### Verdict

- Keep/squash/split/drop/reorder: keep; important plumbing for interactive tools and mid-turn user responses.
- Coherence: good (Claude remote injection + interaction.respond + small permission handler refactor).
- Risk / reliability notes:
  - Correctness depends on `toolCallId` matching the tool_use id expected by Claude; this commit uses `tool_use_id: toolCallId` which aligns with Anthropic tool_result semantics.
  - The “active sender” guard is essential: without it, the UI could inject tool results outside of an active prompt and corrupt state.
  - Approving the tool call via `PermissionHandler.approveToolCall` ensures internal permission bookkeeping stays consistent with injected tool_result.
- Depends on:
  - UI-side support to call `interaction.respond` with the right `toolCallId` and `responseText` (likely follow-up commits in the tools/permissions area).
  - Existing permission request creation logic storing pendingRequests keyed by the tool call id.

### Rewrite Commit Message

- Keep as-is: `feat(cli): add interaction.respond for AskUserQuestion`

---

## 166 2025-12-24 ed4bc007308a `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ed4bc007308a patchFile=/tmp/leeroy-wip-pass2/166.patch patchSha256=313995b8b37901f7b3b7e63e5261a4eb57f65aa3df753791321da3e5ee35331c patchBytes=21029 patchLines=408 notesReviewed=yes reviewedAt=2026-01-24T23:29:59+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR32 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Codex approvals (app): add execpolicy approval decision + button + payload plumbing + reducer refresh of tool input; keep with Codex approvals/MCP interactions PR and squash with follow-ups (167/168/169) in rewrite.

- Subject: feat: add execpolicy approval option for Codex
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Adds a new Codex-specific approval decision that carries an **execpolicy amendment** (intended to permanently allow the proposed command) in the permission UI:
  - `expo-app/sources/components/tools/PermissionFooter.tsx`:
    - Extends decision union with `approved_execpolicy_amendment`.
    - Detects Codex context via `metadata.flavor === 'codex' || toolName.startsWith('Codex')`.
    - Extracts the proposed amendment command from tool input using either:
      - `proposedExecpolicyAmendment` (camel), or
      - `proposed_execpolicy_amendment` (snake),
      and normalizes it to a `string[]` (`execPolicyCommand`).
    - When present, shows a new button (translated key `codex.permissions.yesAlwaysAllowCommand`) and calls:
      - `sessionAllow(..., 'approved_execpolicy_amendment', { command: execPolicyCommand })`.
    - Tracks a dedicated loading state (`loadingExecPolicy`) to avoid concurrent conflicting approvals.
- Plumbs the new decision + payload through app sync/RPC types:
  - `expo-app/sources/sync/ops.ts`:
    - Extends `SessionPermissionRequest` with:
      - `decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | ...`
      - `execPolicyAmendment?: { command: string[] }`
    - Extends `sessionAllow` signature to accept `execPolicyAmendment`.
  - `expo-app/sources/sync/storageTypes.ts`, `expo-app/sources/sync/typesMessage.ts`, `expo-app/sources/sync/typesRaw.ts`:
    - Update the decision union to include `approved_execpolicy_amendment` so the UI can represent the final state.
- Fixes a UI correctness gap for “existing tool message already created” cases:
  - `expo-app/sources/sync/reducer/reducer.ts`:
    - When a permission request references an existing tool call message, it now **always refreshes `message.tool.input`** with the latest `request.arguments` (so the UI sees fields like `proposedExecpolicyAmendment` even if the tool message was created earlier).
    - Only creates `message.tool.permission` when missing (preserves existing state).
- Adds translations for the new button label:
  - `codex.permissions.yesAlwaysAllowCommand` in multiple locales.

### Verdict

- Keep/squash/split/drop/reorder: keep, but group/squash with the follow-up Codex execpolicy plumbing commits (167/168/169/175 etc) so UI + daemon + agent all agree on the protocol.
- Coherence: good (single theme: execpolicy approval UX + plumbing), with a small but relevant reducer fix.
- Risk / reliability notes:
  - This commit adds a new permission decision enum value; server/daemon/agent must tolerate/understand it (otherwise approvals may become no-ops). Ensure ordering in rewritten history so consumers land together.
  - There are no tests in this commit; later commits should validate end-to-end behavior (UI decision → RPC payload → daemon action).
- Depends on:
  - Server/daemon side permission handler supporting the new `decision` value and `execPolicyAmendment.command`.
  - Tool argument shape providing `proposed_execpolicy_amendment` / `proposedExecpolicyAmendment` for Codex tool calls.

### Rewrite Commit Message

- Suggested: `feat(codex): add execpolicy approval option for permission prompts`

---

## 167 2025-12-24 78adc9c58811 `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=78adc9c58811 patchFile=/tmp/leeroy-wip-pass2/167.patch patchSha256=f79673db33828b75c4c4857daf48e801c141a59084e0579208201fd35a55f208 patchBytes=29731 patchLines=722 notesReviewed=yes reviewedAt=2026-01-24T23:46:10+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR32 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Codex approvals (CLI): support approved_execpolicy_amendment end-to-end + version-aware elicitation handling + MCP tool-call begin/end surfacing. Large mixed; keep in Codex approvals/MCP PR (split protocol vs tool-call UX during rewrite).

- Subject: feat(codex): support execpolicy approvals and MCP tool calls
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Extends the permission/approval protocol to support **Codex execpolicy amendments** (the “always allow this command” flow) end-to-end on the CLI side, and also surfaces Codex-originated MCP tool calls into the Happy transcript.
- Permission protocol / types:
  - `cli/src/utils/BasePermissionHandler.ts`:
    - Extends `PermissionResponse` + `PermissionResult` with:
      - `decision: ... | 'approved_execpolicy_amendment' | ...`
      - `execPolicyAmendment?: { command: string[] }`
    - When resolving a response:
      - only returns `approved_execpolicy_amendment` if the amendment exists and has a non-empty command list
      - otherwise falls back to plain `approved` (prevents “approved_execpolicy_amendment” with missing payload).
  - `cli/src/api/types.ts` extends `AgentState.requests[*].decision` union with `approved_execpolicy_amendment`.
  - `cli/src/agent/acp/AcpBackend.ts` treats `approved_execpolicy_amendment` as an “approved” decision for mapping to ACP optionId selection.
- Codex MCP client support:
  - `cli/src/codex/codexMcpClient.ts` gets a major rewrite of elicitation handling:
    - Switches to handling `elicitation/create` requests via a `RequestSchema` extension (`ElicitRequestSchemaWithExtras`) so Codex-specific fields pass through.
    - Defines stable `codex_*` request fields (since ~v0.9) and extracts:
      - `codex_call_id` as the tool call id (falls back to `randomUUID()` if missing)
      - `codex_elicitation` (`exec-approval` vs `patch-approval`) to map to tool names (`CodexBash` vs `CodexPatch`)
    - Adds version-aware elicitation response formatting:
      - Chooses response style `decision` vs `both` based on detected codex version (overridable via `HAPPY_CODEX_ELICITATION_STYLE`).
    - Adds caching of `proposed_execpolicy_amendment`:
      - On notification events of type `exec_approval_request`, caches `proposed_execpolicy_amendment` keyed by `call_id`.
      - When the elicitation request arrives, consumes the cached amendment and includes it in tool input as `proposedExecpolicyAmendment` for the UI.
    - Maps `PermissionResult` → Codex `ReviewDecision`:
      - `approved_execpolicy_amendment` becomes the structured decision payload:
        `{ approved_execpolicy_amendment: { proposed_execpolicy_amendment: string[] } }`
      - Falls back to plain `approved` if the amendment is missing.
    - Updates MCP command selection:
      - `getCodexMcpCommand()` now always returns `mcp-server` for version >= `0.43.0-alpha.5`, else `mcp`; unknown version defaults to `mcp-server`.
    - Removes `sessionId` from `continue` tool args (`{ conversationId, prompt }`).
- Surfaces MCP tool calls into the session transcript:
  - `cli/src/codex/runCodex.ts`:
    - On Codex events `mcp_tool_call_begin` / `mcp_tool_call_end`, emits `tool-call` / `tool-call-result` messages to the Happy session.
    - Uses an `mcp__${server}__${tool}` name prefix so the frontend can treat these as MCP tools with minimal display.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be grouped with the UI-side execpolicy option commit (166) and the follow-up approvals/MCP tooling commits (175/208/211/etc) so the whole approval loop is coherent.
- Coherence: mostly coherent around “Codex approvals + MCP tooling”; the MCP tool-call surfacing is related but could stand alone as a separate commit in a rewrite.
- Risk / reliability notes:
  - Version-sensitive protocol handling is inherently brittle; the env override (`HAPPY_CODEX_ELICITATION_STYLE`) is a good escape hatch but should be documented/validated in integration tests.
  - No tests in this commit despite large protocol logic; later commits should cover:
    - mapping of `approved_execpolicy_amendment` decision payload
    - caching/consumption of `proposed_execpolicy_amendment`
    - MCP tool-call begin/end rendering semantics.
- Depends on:
  - App UI changes (166) to send `execPolicyAmendment.command` in permission approvals.
  - Codex MCP server event shapes (`exec_approval_request`, `elicitation/create` params, `mcp_tool_call_*`) and the daemon/server side respecting the decision payload.

### Rewrite Commit Message

- Keep as-is: `feat(codex): support execpolicy approvals and MCP tool calls`

---

## 168 2026-01-22 9dfa09bef8d8 `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9dfa09bef8d8 patchFile=/tmp/leeroy-wip-pass2/168.patch patchSha256=82cdd407e4325ceb57fa77e931c32d65c4cba3ab83f1df419fca8a5f20058c49 patchBytes=1618 patchLines=34 notesReviewed=yes reviewedAt=2026-01-24T23:46:30+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR32 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Codex approvals i18n completion: add missing execpolicy approval button label translations (it/ja); squash into 166 during rewrite.

- Subject: fix(i18n): add Codex execpolicy button text
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Adds missing translations for the new Codex execpolicy approval button label:
  - `expo-app/sources/text/translations/it.ts`: `codex.permissions.yesAlwaysAllowCommand`
  - `expo-app/sources/text/translations/ja.ts`: `codex.permissions.yesAlwaysAllowCommand`
- Context: commit 166 introduced the key and added translations for several locales, but Italian/Japanese were missing; this keeps the translation schema complete.

### Verdict

- Keep/squash/split/drop/reorder: keep, but it’s a perfect squash candidate into commit 166 (or a single “add execpolicy approval + translations” topic group) in a rewritten history.
- Coherence: perfect (single-key translation completion).
- Depends on: commit 166 introducing the `codex.permissions.yesAlwaysAllowCommand` key usage.

### Rewrite Commit Message

- Keep as-is: `fix(i18n): add Codex execpolicy button text`

---

## 169 2026-01-22 559d39da116d `reducer`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=559d39da116d patchFile=/tmp/leeroy-wip-pass2/169.patch patchSha256=eb72e7822d4b50128b65a45f8a56ec4e7a03c4229438b504a1ae6231913b9260 patchBytes=2997 patchLines=55 notesReviewed=yes reviewedAt=2026-01-24T23:46:56+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR32 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Codex approvals UI stability: keep permission/tool messages idempotent by only updating tool input when arguments changed; keep adjacent to 166 to avoid re-render churn.

- Subject: fix(reducer): keep permission messages idempotent
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Fixes an idempotence regression introduced by the “refresh tool input from AgentState.request arguments” change (166):
  - Previously, when an AgentState permission request referenced an existing tool-call message, the reducer:
    - always re-assigned `message.tool.input = request.arguments`, and
    - always marked the message as changed (`changed.add(existingMessageId)`),
    even when the arguments were identical.
  - Result: permission tool messages could be re-emitted/re-rendered on unrelated updates.
- New behavior in `expo-app/sources/sync/reducer/reducer.ts`:
  - Imports `compareToolCalls` and compares:
    - previous tool input vs the new request arguments for the same tool name.
  - Only updates `message.tool.input` and marks the message as changed when the arguments actually differ.
  - Still marks changed when `message.tool.permission` is newly created, preserving the “late arrival” update behavior.
- This keeps the reducer stable while still allowing late-arriving fields like `proposedExecpolicyAmendment` to update the existing message exactly once when they appear.

### Verdict

- Keep/squash/split/drop/reorder: keep; should be adjacent to commit 166 in a rewritten history (it’s effectively a follow-up correctness fix to keep the permission UI stable).
- Coherence: good (single focused reducer behavior fix).
- Depends on:
  - `expo-app/sources/utils/toolComparison`’s equality semantics; it must treat semantically identical tool-call argument payloads as equal.

### Rewrite Commit Message

- Keep as-is: `fix(reducer): keep permission messages idempotent`

---

## 170 2026-01-18 cc3c711ae28e `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=cc3c711ae28e patchFile=/tmp/leeroy-wip-pass2/170.patch patchSha256=2ae2f48f4e085956b66783b6277e62d15bf2a988d1085d362b512bf561cf0baf patchBytes=1608 patchLines=38 notesReviewed=yes reviewedAt=2026-01-24T23:47:37+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T13:46:10+0100 manualReviewedForPrPlan=yes notes=Claude local-runner correctness: append claudeArgs last so prompt/slash commands are positioned correctly; keep with Claude session handling PR.

- Subject: fix: move claudeArgs to end of args array for slash command support
- Reasons: correctness/UX; ensures the user prompt (including slash commands like `/help`) is positioned where the Claude CLI expects it

### Manual Review Notes

- Diffstat: `1 file changed, 5 insertions(+), 5 deletions(-)`
- What changed:
  - `cli/src/claude/claudeLocal.ts`:
    - Moves `opts.claudeArgs` to the end of the spawned argument array:
      - Previously, `claudeArgs` were appended before `--settings <hookSettingsPath>`.
      - Now, `--settings ...` is appended first (when present), then `claudeArgs` are appended last.
    - Adds an explicit comment: “Add custom Claude arguments LAST (so prompt/slash commands are at the end)”.
- Coherence / ordering notes:
  - Tight, single-file fix, clearly aligned with the message: moving arg ordering fixes a real “CLI interprets prompt incorrectly” class of issues.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Suggested: `fix(claude): append claudeArgs last for slash command support`

---

## 171 2026-01-19 d06d5a833f8e `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d06d5a833f8e patchFile=/tmp/leeroy-wip-pass2/171.patch patchSha256=5c8f631aee8021ca7e39a5cb765a569b23b35449e95a76c5e2e251385c07feff patchBytes=1766 patchLines=41 notesReviewed=yes reviewedAt=2026-01-24T23:47:58+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR31 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=Forward OS signals from CLI to Claude local child; treat as terminal switching/signal-handling reliability (non-tmux).

- Subject: Add signal forwarding to claudeLocal.ts
- Reasons: non-Conventional-Commits subject

### Manual Review Notes

- Adds OS-signal forwarding from the Happy CLI process to the spawned Claude local child process:
  - `cli/src/claude/claudeLocal.ts`:
    - On spawn, registers `process.on('SIGTERM'|'SIGINT'|'SIGHUP')` handlers that forward the same signal to the child (`child.kill(signal)`) when the child is still alive.
    - Cleans up those handlers on `child.on('exit')` to avoid leaking listeners across restarts/switches.
- Motivation (implicit): abort signals cover programmatic aborts (mode switching), but direct OS signals (Ctrl+C, kill) should also terminate the underlying Claude process to avoid orphaned subprocesses.

### Verdict

- Keep/squash/split/drop/reorder: keep; small reliability fix. Can be grouped with other “process lifecycle / signal handling” commits.
- Coherence: perfect (single concern, single file).
- Depends on: nothing explicit, but interacts with any other signal/abort orchestration in the CLI.

### Rewrite Commit Message

- Suggested: `fix(claude): forward OS signals to claude local process`

---

## 172 2025-11-20 1b7cb3bfea86 `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1b7cb3bfea86 patchFile=/tmp/leeroy-wip-pass2/172.patch patchSha256=965a84e5008cfafa3520667d4d4d9d211b18ca05fd25fe7056f56916f35888fd patchBytes=6005 patchLines=160 notesReviewed=yes reviewedAt=2026-01-24T23:48:32+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR33 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=Codex /clear semantics: treat as session boundary (queue isolate+clear) and reset internal state; includes tiny Claude log-label fix.

- Subject: feat: handle /clear command as session reset in codex
- Reasons: UX/correctness; `/clear` should reset the Codex session state deterministically (similar to “new conversation”) instead of behaving like a normal message

### Manual Review Notes

- Diffstat: `3 files changed, 41 insertions(+), 11 deletions(-)`
- What changed:
  - Adds `/clear` handling to Codex’s message queueing and main loop:
    - `cli/src/codex/runCodex.ts`:
      - Parses each user message with `parseSpecialCommand(...)`.
      - If the command is `clear`:
        - enqueues via `messageQueue.pushIsolateAndClear(...)` so the clear command becomes a “session boundary” (queue isolation + reset).
      - When processing a message batch:
        - detects `specialCommand.type === 'clear'`,
        - clears the MCP client session (`client.clearSession()`), resets internal flags (`wasCreated = false`, `currentModeHash = null`),
        - resets/aborts processors (`permissionHandler.reset()`, `reasoningProcessor.abort()`, `diffProcessor.reset()`),
        - sets `thinking = false`, keeps the session alive, and emits “Session reset.” as a status message,
        - re-emits ready (idle) and continues the loop.
  - Small Claude-side cleanup:
    - `cli/src/claude/runClaude.ts`: fixes a debug log label from `/compact` → `/clear` (log clarity), and removes stray whitespace.
  - Minor formatting cleanup in special command parsing:
    - `cli/src/parsers/specialCommands.ts`: whitespace-only changes; no functional changes visible in this diff.
- Coherence / ordering notes:
  - The commit message is accurate for Codex behavior; it also includes a tiny Claude log-label fix (still related to the `/clear` command).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `parseSpecialCommand` semantics and `MessageQueue2.pushIsolateAndClear` behavior (should remain stable).

### Rewrite Commit Message

- Suggested: `feat(codex): treat /clear as session reset`

---

## 173 2026-01-11 332ab2b0cd93 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=332ab2b0cd93 patchFile=/tmp/leeroy-wip-pass2/173.patch patchSha256=0e7a4d713ed05358397d1899edf3645cd9445e00b934d92bed64c493bf01104c patchBytes=3847 patchLines=96 notesReviewed=yes reviewedAt=2026-01-24T23:54:36+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=Claude remote launcher: treat abort errors as expected to avoid spurious process-error noise (reliability/UX).

- Subject: fix(cli): improve abort error handling to reduce spurious error messages
- Reasons: reliability/UX; prevents “process error” noise when a Claude run is intentionally aborted (mode switches, user aborts)

### Manual Review Notes

- Diffstat: `1 file changed, 58 insertions(+), 2 deletions(-)`
- What changed:
  - `cli/src/claude/claudeRemoteLauncher.ts`:
    - Adds structured error introspection for launch errors:
      - `getLaunchErrorInfo(e)` extracts printable fields (`name`, `message`, `code`, `stack`) safely.
      - `isAbortError(e)` detects aborts via:
        - `e instanceof AbortError` (SDK type),
        - or `{ name: 'AbortError' }`,
        - or `{ code: 'ABORT_ERR' }`.
    - Changes launch error handling:
      - logs structured error info with an `abortError` boolean.
      - if the error is an abort and the controller signal is aborted, emits “Aborted by user” and continues the outer loop (no spurious “Claude process error”).
      - for non-abort errors, continues to emit `Claude process error: ...` as before.
- Coherence / ordering notes:
  - Focused single-file improvement; message matches diff.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `AbortError` type stability (SDK) and Node’s abort error shaping across runtimes.

### Rewrite Commit Message

- Keep subject: `fix(cli): improve abort error handling to reduce spurious error messages`

---

## 174 2025-12-24 e956463db3e2 `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e956463db3e2 patchFile=/tmp/leeroy-wip-pass2/174.patch patchSha256=10e92d0373450683972e28cdfccaa743ee45ec967377a5fe4a4bccc8bfd70e89 patchBytes=2456 patchLines=61 notesReviewed=yes reviewedAt=2026-01-24T23:55:09+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR32 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=MCP bridge runner compatibility: spawn happy-mcp via process.execPath (bun or node) for Codex+Gemini (avoid relying on .mjs executable bits).

- Subject: fix: use runtime execPath for MCP bridge
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Fixes MCP bridge spawning for runtimes where the `.mjs` file is not directly executable:
  - `cli/src/codex/runCodex.ts` and `cli/src/gemini/runGemini.ts`:
    - Previously set `mcpServers.happy.command` to the script path `.../bin/happy-mcp.mjs` and passed args `['--url', ...]`.
    - Now sets:
      - `command: process.execPath` (node or bun binary used to run the current CLI),
      - `args: [<bridgeScript>, '--url', <url>]`.
    - This makes MCP bridge launch consistent across node/bun and avoids relying on shebang or executable bits on the `.mjs` file.
  - `cli/src/utils/spawnHappyCLI.ts` has a whitespace-only change (no behavior impact).

### Verdict

- Keep/squash/split/drop/reorder: keep; small but important reliability fix for MCP bridge execution.
- Coherence: good (single theme; one minor formatting change).
- Depends on: `process.execPath` being the desired runtime for the MCP bridge (intended: “use whatever is running the CLI”).

### Rewrite Commit Message

- Suggested: `fix(mcp): run happy-mcp bridge via process.execPath`

---

## 175 2026-01-23 f0a7d8d0b40c `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=f0a7d8d0b40c patchFile=/tmp/leeroy-wip-pass2/175.patch patchSha256=8820467c0173b29a1067ece29396bc891bcacf60dacb63662e70263ee0053d53 patchBytes=5271 patchLines=127 notesReviewed=yes reviewedAt=2026-01-24T23:55:40+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR32 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=Codex approvals correctness: key approvals/execpolicy amendment caching by MCP tool call id; includes focused unit tests.

- Subject: fix(codex): use mcp tool call id for approvals
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Fixes an important identifier mismatch in Codex elicitation/approval handling:
  - Codex can provide multiple ids (`codex_call_id`, `call_id`, and an MCP-level `codex_mcp_tool_call_id` / `mcp_tool_call_id`).
  - To ensure approval decisions and cached execpolicy amendments are associated with the correct tool request, this commit prefers the MCP tool call id when available.
- `cli/src/codex/codexMcpClient.ts`:
  - Adds `getCodexElicitationToolCallId(params)`:
    - prefers `codex_mcp_tool_call_id`
    - falls back to `codex_call_id`
  - Adds `getCodexEventToolCallId(msg)`:
    - prefers `mcp_tool_call_id` (or `codex_mcp_tool_call_id`)
    - falls back to `call_id` (or `codex_call_id`)
  - Uses these helpers for:
    - selecting `toolCallId` from `elicitation/create` params (permission request id)
    - caching `proposed_execpolicy_amendment` from `exec_approval_request` events keyed by the same id.
- `cli/src/codex/codexMcpClient.test.ts`:
  - Replaces an older “no stdout printing” test (tied to the prior handler implementation) with focused unit tests for tool call id selection logic.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a correctness fix that should sit immediately after the Codex execpolicy/elicitation refactor (167).
- Coherence: good (single theme: stable tool call id derivation).
- Depends on:
  - Codex MCP server including `codex_mcp_tool_call_id` / `mcp_tool_call_id` in relevant request/event payloads.
  - UI/client treating the tool call id as the stable key for permission resolution and execpolicy amendment mapping.

### Rewrite Commit Message

- Keep as-is: `fix(codex): use mcp tool call id for approvals`

---

## 176 2026-01-22 da620b6865ad `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=da620b6865ad patchFile=/tmp/leeroy-wip-pass2/176.patch patchSha256=e0ebaae88a249285a06cbe36e1f2546b87a800fa88342eb152b67c6c04ee4b3e patchBytes=100525 patchLines=2758 notesReviewed=yes reviewedAt=2026-01-24T23:57:40+01:00 notesUpdated=yes
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=Server-light final feature (flavors + sqlite + local files). needsSplit=yes: move optional UI serving + /files route hardening into PR23 during rewrite.

- Subject: feat(server): add full/light flavors with sqlite migrations
- Reasons: too many files (38)

### Manual Review Notes

- Introduces a major server “flavors” split while keeping one codebase + API surface:
  - `server/sources/startServer.ts` adds `startServer('full' | 'light')` and sets `process.env.HAPPY_SERVER_FLAVOR` for downstream conditional logic.
  - `server/sources/main.ts` becomes a thin “full” entrypoint (`startServer('full')`).
  - `server/sources/main.light.ts` is added as the “light” entrypoint (`startServer('light')`).
  - Process-level handlers (uncaughtException / unhandledRejection / warning / exit logging) are extracted from the old `main.ts` into `server/sources/utils/processHandlers.ts` and registered from both entrypoints.

- Adds SQLite-backed “light” flavor infrastructure (DB, migrations, schema generation):
  - `server/sources/storage/db.ts` changes from `export const db = new PrismaClient()` to an initialized singleton:
    - `initDbPostgres()` uses the standard `@prisma/client` generated client.
    - `initDbSqlite()` dynamically imports a **separately generated** sqlite Prisma client from `server/generated/sqlite-client` and instantiates it.
  - `server/prisma/sqlite/schema.prisma` is checked in but explicitly treated as generated:
    - `server/scripts/generateSqliteSchema.ts` generates `prisma/sqlite/schema.prisma` from `prisma/schema.prisma` (header blocks swapped to sqlite datasource + a dedicated generator output to `generated/sqlite-client`).
    - `server/scripts/generateSqliteSchema.spec.ts` enforces that sqlite schema stays in sync with the master schema.
    - `server/prisma/sqlite/migrations/*` adds a baseline sqlite migration and lockfile for `prisma migrate deploy`.
  - Light migration workflows:
    - `server/scripts/migrate.light.deploy.ts`: apply defaults, ensure dirs, then run schema generation + `prisma migrate deploy` using the sqlite schema path.
    - `server/scripts/migrate.light.new.ts`: creates migrations using an **isolated temp sqlite file** (avoids touching a user’s real local DB) via `prisma migrate dev --create-only`.
    - `server/scripts/migrate.light.resolveBaseline.ts`: marks the earliest migration dir as applied (for upgrading a pre-migrations sqlite DB).

- Adds local public-files backend for the light flavor (and gates it behind capability):
  - `server/sources/storage/files.ts` is refactored into a backend abstraction:
    - `initFilesS3FromEnv()` configures Minio/S3 write-only backend (full flavor).
    - `initFilesLocalFromEnv()` configures a local backend with **read + write** (light flavor).
    - `loadFiles()`, `getPublicUrl()`, `writePublicFile()`, `readPublicFile()` now error if backend is not initialized.
    - `hasPublicFileRead()` allows the API layer to only expose reads when supported.
  - `server/sources/flavors/light/files.ts` implements:
    - local filesystem storage under `HAPPY_SERVER_LIGHT_FILES_DIR` (default: `~/.happy/server-light/files`)
    - `normalizePublicPath()` with path traversal and Windows-drive rejection (used by both writer and reader)
    - `GET /files/*` public URLs via `PUBLIC_URL` (or localhost fallback)
  - API surface:
    - `server/sources/app/api/utils/enablePublicFiles.ts` adds `GET /files/*` when `hasPublicFileRead()` is true, with content-type sniffing and long-lived cache headers.
    - `server/sources/app/api/utils/enableOptionalStatics.ts` wires both UI static serving and `/files/*` into `startApi()`.
  - `server/sources/storage/uploadImage.ts` switches from direct Minio client usage to `writePublicFile(...)` (removes the old `resolveImageUrl` helper).

- Adds optional “serve the web UI” capability from the server process (any flavor):
  - `server/sources/app/api/uiConfig.ts` + spec: resolves `HAPPY_SERVER_UI_DIR`/`HAPPY_SERVER_UI_PREFIX` (and supports legacy `HAPPY_SERVER_LIGHT_UI_*` vars).
  - `server/sources/app/api/utils/enableServeUi.ts`:
    - serves a prebuilt UI directory either at root (`/`) or a prefix (`/ui`)
    - includes path escape protections (resolved path must remain under the configured root)
    - special-cases Expo export assets that are emitted as absolute root paths (`/_expo/*`, `/assets/*`, etc).
  - `server/sources/app/api/utils/enableErrorHandlers.ts` adds SPA fallback for unknown `GET` routes **only** when UI is mounted at `/` (and explicitly excludes API and asset prefixes from fallback).

- SQLite compatibility adjustments outside infra:
  - `server/sources/app/api/routes/userRoutes.ts` avoids `mode: 'insensitive'` in sqlite flavor (Prisma compatibility).
  - `server/sources/app/presence/timeout.ts` replaces `updateManyAndReturn` with `updateMany` to avoid relying on “RETURNING” behavior; uses the already-fetched `session.lastActiveAt` / `machine.lastActiveAt` for event payloads.

- Developer tooling and docs:
  - `server/package.json` adds scripts (`start:light`, `dev:light`, sqlite schema/migration helpers) and changes `postinstall` to generate both postgres and sqlite Prisma clients.
  - `server/.gitignore` ignores `generated/` so the sqlite client output is not committed.
  - `server/README.md` documents flavors, light defaults (data dir, sqlite db path, files dir), UI serving flags, and migration guidance.

### Notes / Risks / Follow-ups (for history rewrite planning)

- This commit is very large and bundles multiple “topics” (flavor split, sqlite infra, local files backend + routing, UI serving). For a clean history rewrite, it likely wants to be split by concern.
- `server/scripts/dev.full.ts` parses args **before** loading `.env.dev` via dotenv, so “read PORT from env” only works when `PORT` is set in the shell, not when only present in `.env.dev`. This is probably harmless (README suggests `PORT=... yarn dev`), but it’s a sharp edge to keep in mind when rewriting/splitting.
- `dev:light` runs schema generation + migrate deploy, but relies on `postinstall` (or previous runs) having produced `generated/sqlite-client` before `startServer('light')` calls `initDbSqlite()`.

### Verdict

- Keep/squash/split/drop/reorder: keep, but strongly consider splitting into multiple commits during rewrite:
  - (1) `feat(server): add startServer flavor entrypoints` (main/full vs main.light, process handlers)
  - (2) `feat(server-light): add sqlite client generation + baseline migrations` (schema generator + migrations scripts)
  - (3) `feat(server-light): add local public files backend and /files route` (backend abstraction + traversal protection)
  - (4) `feat(server): allow serving UI bundle from server` (uiConfig + enableServeUi + SPA fallback)
  - plus small sqlite-compat fixes (timeout updateMany, username query mode) placed after the infra that motivates them.
- Coherence: mostly coherent at the “deliver light flavor end-to-end” level, but too broad for a single long-term-reviewable commit.
- Depends on:
  - Prisma CLI / `prisma migrate` behavior for sqlite with the checked-in migrations.
  - `postinstall` running `prisma generate --schema prisma/sqlite/schema.prisma` to produce `server/generated/sqlite-client` for `initDbSqlite()` to import.
  - `PUBLIC_URL` correctness for generating `/files/*` links (and local reverse proxying when used remotely).

### Rewrite Commit Message

- Keep as-is (if not split): `feat(server): add full/light flavors with sqlite migrations`
- If split, use more specific subjects (examples):
  - `feat(server): add startServer and server flavors`
  - `feat(server-light): add sqlite schema generation and baseline migrations`
  - `feat(server-light): serve local public files under /files`
  - `feat(server): serve optional UI bundle from server`

---

## 177 2026-01-23 893ce7af5df0 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=893ce7af5df0 patchFile=/tmp/leeroy-wip-pass2/177.patch patchSha256=97c8738e46c630764e2cedd97ec2f6335bc17eb43072880b817124c4a299d7a5 patchBytes=4585 patchLines=146 notesReviewed=yes reviewedAt=2026-01-24T23:58:23+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR04 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=Auth redirect correctness: avoid hooks-order violations in RootLayout redirect path; includes regression test.

- Subject: fix(expo-app): avoid hooks order violation on redirect
- Reasons: correctness; ensures hook call order is stable even when the layout redirects unauthenticated users

### Manual Review Notes

- Diffstat: `2 files changed, 113 insertions(+), 1 deletion(-)`
- What changed:
  - Fixes hook ordering in the root app layout:
    - `expo-app/sources/app/(app)/_layout.tsx`:
      - moves `useUnistyles()` above redirect logic so hooks are called unconditionally in the same order.
      - This prevents a classic React “Rendered fewer hooks than expected” class of issues when a render starts non-redirecting and later becomes redirecting.
  - Adds a regression test:
    - `expo-app/sources/app/(app)/_layout.test.ts` (new):
      - renders `RootLayout` once authenticated, then updates to unauthenticated + non-public route,
      - asserts `tree.update(...)` does not throw.

### Verdict

- Keep/squash/split/drop/reorder: keep (correctness fix + test).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(expo-app): avoid hooks order violation on redirect`

---

## 178 2026-01-23 7afbc9a2decd `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7afbc9a2decd patchFile=/tmp/leeroy-wip-pass2/178.patch patchSha256=f014094d3724e30cbb0dfd1322b838be65dfbac3bf7c86e5a2ac8be402c187e7 patchBytes=5316 patchLines=140 notesReviewed=yes reviewedAt=2026-01-25T00:34:53+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=New-session wizard/resume pick reliability: persist resumeSessionId in drafts + safe clipboard paste helper (tests).

- Subject: fix(expo-app): persist resumeSessionId and harden clipboard paste
- Reasons: reliability/UX; ensures resume session id is preserved across app restarts and prevents paste crashes when clipboard read fails

### Manual Review Notes

- Diffstat: `5 files changed, 65 insertions(+), 3 deletions(-)`
- What changed:
  - Persists `resumeSessionId` in new-session drafts:
    - `expo-app/sources/sync/persistence.ts`: reads `resumeSessionId` from persisted JSON and includes it in the returned `NewSessionDraft` when present.
    - `expo-app/sources/sync/persistence.test.ts`: adds a roundtrip test for `resumeSessionId`.
  - Hardens clipboard paste flow:
    - Adds `expo-app/sources/utils/clipboard.ts` with `getClipboardStringTrimmedSafe()` (returns `''` if clipboard read throws).
    - Adds `expo-app/sources/utils/clipboard.test.ts` to cover success and failure behavior.
    - `expo-app/sources/app/(app)/new/pick/resume.tsx`: uses `getClipboardStringTrimmedSafe()` instead of calling `Clipboard.getStringAsync()` directly.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(expo-app): persist resumeSessionId and harden clipboard paste`

---

## 179 2026-01-23 9a13879b753a `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=9a13879b753a patchFile=/tmp/leeroy-wip-pass2/179.patch patchSha256=ee6374e5659b6afeda10ef1730af181290787a18b6dafb8fd123b1ea6e4e5b4c patchBytes=2273 patchLines=53 notesReviewed=yes reviewedAt=2026-01-25T00:35:24+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=Sessions list correctness: avoid false homeDir prefix matches in display formatting; includes regression test.

- Subject: fix(expo-app): avoid false homeDir prefix matches
- Reasons: correctness; prevents incorrect “path is inside home” detection (e.g. `/home/userfoo` is not under `/home/user`)

### Manual Review Notes

- Diffstat: `2 files changed, 19 insertions(+), 2 deletions(-)`
- What changed:
  - `expo-app/sources/sync/sessionListViewData.ts`:
    - Tightens `formatPathRelativeToHome(...)` membership check:
      - previously used `path.startsWith(normalizedHome)`,
      - now requires `path === normalizedHome` or `path.startsWith(normalizedHome + '/')`.
  - `expo-app/sources/sync/sessionListViewData.test.ts`:
    - Adds a regression test ensuring `/home/userfoo/repo` is not treated as under `/home/user`.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(expo-app): avoid false homeDir prefix matches`

---

## 180 2026-01-23 c107a779e07d `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c107a779e07d patchFile=/tmp/leeroy-wip-pass2/180.patch patchSha256=e3f7b3488d6c39963b04a49382076338d608bda6b8ab2fb5d3f52adf4d0e054c patchBytes=3330 patchLines=87 notesReviewed=yes reviewedAt=2026-01-25T00:35:58+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T14:04:18+0100 manualReviewedForPrPlan=yes notes=Capabilities UI reliability: set cache state to error on detect throw (avoid stuck loading) with hook regression test.

- Subject: fix(expo-app): avoid stuck loading in capabilities cache
- Reasons: reliability; ensures cache transitions to `error` when detection throws instead of remaining stuck in `loading`

### Manual Review Notes

- Diffstat: `2 files changed, 61 insertions(+), 1 deletion(-)`
- What changed:
  - `expo-app/sources/hooks/useMachineCapabilitiesCache.ts`:
    - Wraps `machineCapabilitiesDetect(...)` in a try/catch.
    - On throw:
      - checks whether a newer in-flight token has replaced this fetch (if so, returns without clobbering state),
      - otherwise sets cache state to:
        - `{ status: 'error', snapshot: prevSnapshot }` when a prior snapshot exists,
        - or `{ status: 'error' }` with no snapshot.
  - `expo-app/sources/hooks/useMachineCapabilitiesCache.hook.test.ts` (new):
    - Mocks `machineCapabilitiesDetect` to throw and asserts the hook’s cached state becomes `error` (not stuck `loading`).

### Verdict

- Keep/squash/split/drop/reorder: keep (important robustness fix + test).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(expo-app): avoid stuck loading in capabilities cache`

---

## 181 2026-01-23 35c0974eff1f `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=35c0974eff1f patchFile=/tmp/leeroy-wip-pass2/181.patch patchSha256=861d5bfb4c2caf443d4d1bd9dc482b807eda60c90b44dc2003e44c5b5b49c591 patchBytes=2920 patchLines=64 notesReviewed=yes reviewedAt=2026-01-25T00:36:44+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Mostly pending/messages UI + message-send mode type dedupe; needsSplit=yes to move useCLIDetection error-shape tweak into PR14 during rewrite.

- Subject: refactor(expo-app): remove dead code and dedupe types
- Reasons: small cleanup + reduces duplication; also removes an unused/duplicative error field

### Manual Review Notes

- Diffstat: `3 files changed, 5 insertions(+), 7 deletions(-)`
- What changed:
  - Dedupes the message-send mode type:
    - `expo-app/sources/app/(app)/settings/session.tsx`: removes a local `MessageSendMode` union and imports `MessageSendMode` from `expo-app/sources/sync/submitMode`.
  - Removes redundant conditional backgroundColor branching:
    - `expo-app/sources/components/PendingMessagesModal.tsx`: precomputes `backgroundColor` outside the pressed-state callback (no behavior change).
  - Removes a dead/unhelpful `error` string:
    - `expo-app/sources/hooks/useCLIDetection.ts`: drops `error: 'Detection error'` from the returned object when there is no snapshot and cached status is `error`.
      - This changes the hook output shape in that edge case; confirm no UI depends on the string.

### Verdict

- Keep/squash/split/drop/reorder: keep (small cleanup), but note the `useCLIDetection` output change during history rewrite if consumers relied on `error`.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `refactor(expo-app): remove dead code and dedupe types`

---

## 182 2026-01-23 b319d85e72c3 `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=b319d85e72c3 patchFile=/tmp/leeroy-wip-pass2/182.patch patchSha256=819e4753a283c457bb1a14ee9a17cd7596399461ed18b81bcde4eac78c24edfb patchBytes=870 patchLines=21 notesReviewed=yes reviewedAt=2026-01-25T00:37:12+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Resume feature i18n fix (inactive resumable label typo in es.ts); fold into PR19.

- Subject: fix(i18n): correct Spanish resumable label
- Reasons: translation correctness (typo fix)

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `expo-app/sources/text/translations/es.ts`:
    - Fixes typo: `reanundable` → `reanudable`.

### Verdict

- Keep/squash/split/drop/reorder: keep (tiny, focused).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(i18n): correct Spanish resumable label`

---

## 183 2026-01-23 46186162ae14 `happy-cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=46186162ae14 patchFile=/tmp/leeroy-wip-pass2/183.patch patchSha256=a2ea9fbfb29a945d1ff6c0eadf6884571707dbadec36deaf1d0017457c2444e1 patchBytes=2477 patchLines=69 notesReviewed=yes reviewedAt=2026-01-25T00:37:43+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR32 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Test stability for MCP schema mocks; needsSplit=yes (terminal timer cleanup belongs to PR31; MCP schema mocks to PR32; logger test tweak can fold into PR01/PR17 during rewrite).

- Subject: test(happy-cli): fix timer cleanup and MCP schema mocks
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Stabilizes and corrects a few `happy-cli` tests that were either flaky or mocking the wrong shape:
  - `cli/src/utils/terminalStdinCleanup.test.ts`:
    - Adds `afterEach(() => vi.useRealTimers())` so fake timers don’t leak across tests (common source of order-dependent flakiness).
  - `cli/src/codex/codexMcpClient.test.ts`:
    - Replaces a too-minimal MCP SDK schema mock (`ElicitRequestSchema: {}`) with a zod-backed mock that provides the schema objects the code expects (`RequestSchema`, `ElicitRequestParamsSchema`, `ElicitRequestSchema`), all as `z.object({}).passthrough()`.
    - This prevents runtime failures in tests when code calls zod methods on the mocked exports.
  - `cli/src/ui/logger.test.ts`:
    - Adjusts the test to call `logger.debugLargeJson(...)` (the API under test) instead of a generic `logger.debug(...)`.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a pragmatic “test stability” commit. It can be squashed with nearby test-only cleanup if we want fewer commits, but it’s coherent enough on its own.
- Coherence: acceptable. The logger test tweak is slightly “misc” compared to the subject line, but still test-only.
- Depends on:
  - `zod` being available in the test environment (it is already a dependency).
  - The MCP SDK exports used by the code (`@modelcontextprotocol/sdk/types.js`) remaining compatible with the mocked names.

### Rewrite Commit Message

- Keep as-is: `test(happy-cli): fix timer cleanup and MCP schema mocks`

---

## 184 2026-01-23 ac5bf2f3abfa `happy-cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ac5bf2f3abfa patchFile=/tmp/leeroy-wip-pass2/184.patch patchSha256=8e6963c9a91dede97b944e885b9786d5c0059dff553ac23360b7ef9f6c51affb patchBytes=1119 patchLines=29 notesReviewed=yes reviewedAt=2026-01-25T00:38:29+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Test reliability for runtime override selection (module caching); ok to fold into Foundations/DevX or squash into the owning CLI invocation change during rewrite.

- Subject: test(happy-cli): reset modules for runtime override
- Reasons: test reliability; ensures env-driven runtime overrides don’t leak via module caching between tests

### Manual Review Notes

- Diffstat: `1 file changed, 5 insertions(+), 1 deletion(-)`
- What changed:
  - `cli/src/utils/spawnHappyCLI.invocation.test.ts`:
    - Adds `beforeEach(() => { vi.resetModules(); })` so runtime-dependent modules are re-evaluated per test.
    - Keeps existing env cleanup in `afterEach` for `HAPPY_CLI_SUBPROCESS_RUNTIME`.
- Coherence:
  - Tight and clearly aligned with intent: module-level runtime selection logic often caches once per test file unless modules are reset.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `test(happy-cli): reset modules for runtime override`

---

## 185 2026-01-23 947b8d1814fb `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=947b8d1814fb patchFile=/tmp/leeroy-wip-pass2/185.patch patchSha256=adf28fb35c7a7d7bccaec3750954b396381ea8b4e4c008b21e8ef2f6dcc7f877 patchBytes=1017 patchLines=20 notesReviewed=yes reviewedAt=2026-01-25T00:39:01+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Small sessions-related cleanup (remove unused optimistic-only flag); squash into adjacent sessions list UX work.

- Subject: refactor(expo-app): remove unused optimistic flag
- Reasons: cleanup; removes an unused intermediate boolean

### Manual Review Notes

- Diffstat: `1 file changed, 1 deletion(-)`
- What changed:
  - `expo-app/sources/utils/sessionUtils.ts`:
    - Removes `isOptimisticOnly` (computed but unused).

### Verdict

- Keep/squash/split/drop/reorder: keep (tiny cleanup), or squash into adjacent session-status work if desired.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `refactor(expo-app): remove unused optimistic flag`

---

## 186 2026-01-23 8cc823d1ba2d `server-light`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8cc823d1ba2d patchFile=/tmp/leeroy-wip-pass2/186.patch patchSha256=d8313bf1e7ab9139ec5d34c47d940a2eb5d5a71bbfdf2092ccf08daac5fb10e2 patchBytes=3706 patchLines=81 notesReviewed=yes reviewedAt=2026-01-25T00:39:44+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Server-light hardening: validate PORT fallback + normalize public paths (Windows slashes) with tests; keep in server-light feature PR.

- Subject: fix(server-light): validate PORT and normalize paths
- Reasons: correctness + cross-platform hardening (invalid PORT should not produce invalid URLs; Windows path separators should be normalized)

### Manual Review Notes

- Diffstat: `4 files changed, 12 insertions(+), 7 deletions(-)`
- What changed:
  - Validates PORT when deriving the default public URL:
    - `server/sources/flavors/light/env.ts`:
      - replaces naive `parseInt(env.PORT)` with a validated integer check (`1..65535`), falling back to `3005` when invalid.
    - `server/sources/flavors/light/env.spec.ts`: adds regression test for invalid `PORT` fallback.
  - Unifies light public URL derivation and improves path normalization:
    - `server/sources/flavors/light/files.ts`:
      - `getLightPublicBaseUrl` now delegates to `resolveLightPublicUrl(env)` (dedupe logic).
      - `normalizePublicPath` now replaces backslashes via `replace(/\\/g, '/')` (fixes prior over-escaping).
    - `server/sources/flavors/light/files.spec.ts`: adds a test ensuring Windows-style `public\\users\\...` paths normalize correctly.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(server-light): validate PORT and normalize paths`

---

## 187 2026-01-23 8cb995a1abe1 `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8cb995a1abe1 patchFile=/tmp/leeroy-wip-pass2/187.patch patchSha256=f78f92edf9157e92097bc5feea135e202b9912b65eecb8918dd4e1eebe4ce39b patchBytes=1037 patchLines=21 notesReviewed=yes reviewedAt=2026-01-25T00:40:20+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Server-light schema-gen test fix (regex correctness); keep with server-light schema/migrations work.

- Subject: test(server): fix schema generation sort regex
- Reasons: test correctness; ensures the regex actually matches whitespace (not a literal `\\s`)

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `server/scripts/generateSqliteSchema.spec.ts`:
    - Changes the regex from `/sort\\s*:\\s*(Asc|Desc)/` to `/sort\s*:\s*(Asc|Desc)/` so `\s` is interpreted as whitespace by the JS regex engine.

### Verdict

- Keep/squash/split/drop/reorder: keep (tiny but important test fix).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `test(server): fix schema generation sort regex`

---

## 188 2026-01-23 04d9c60ef8e7 `happy-cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=04d9c60ef8e7 patchFile=/tmp/leeroy-wip-pass2/188.patch patchSha256=05518fd5348cf6b3a19a017209670cd9798a1528fcfbf2ed78991ff6a4ee08db patchBytes=855 patchLines=21 notesReviewed=yes reviewedAt=2026-01-25T00:40:59+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR30 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Gemini error formatting cleanup (dead branch); keep with agent error surfaces PR.

- Subject: refactor(happy-cli): remove unreachable gemini error branch
- Reasons: cleanup; removes a redundant fallback branch in Gemini error formatting

### Manual Review Notes

- Diffstat: `1 file changed, 2 deletions(-)`
- What changed:
  - `cli/src/gemini/utils/formatGeminiErrorForUi.ts`:
    - Removes an `else if (isErrorInstance)` fallback that assigned `errorMsg = formatErrorForUi(error)`.
    - The remaining branches already cover the meaningful Gemini error shapes; this branch was effectively dead/redundant.

### Verdict

- Keep/squash/split/drop/reorder: keep (small cleanup).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `refactor(happy-cli): remove unreachable gemini error branch`

---

## 189 2026-01-23 2616e5e19253 `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2616e5e19253 patchFile=/tmp/leeroy-wip-pass2/189.patch patchSha256=21e7b22d86392e531843090ee10424c638fac803cf67bda3abfb9017a5c5710e patchBytes=2725 patchLines=62 notesReviewed=yes reviewedAt=2026-01-25T00:41:36+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR23 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Serve-UI reliability: 404 (not crash) when index.html missing; includes focused test; keep in server UI/public-files PR.

- Subject: fix(server): handle missing UI index.html
- Reasons: reliability; serving the UI should fail gracefully (404) when the configured UI build dir is wrong or missing, not crash the request handler

### Manual Review Notes

- Diffstat: `2 files changed, 29 insertions(+), 1 deletion(-)`
- What changed:
  - `server/sources/app/api/utils/enableServeUi.ts`:
    - Wraps reading `index.html` in a try/catch.
    - On missing/unreadable file:
      - logs a warning with `{ err, indexPath }` (“check UI build dir configuration”),
      - returns `404` with `cache-control: no-cache` and `{ error: 'Not found' }`.
    - On success, keeps previous behavior of serving the HTML with `no-cache`.
  - Adds a focused server test:
    - `server/sources/app/api/utils/enableServeUi.spec.ts` (new):
      - mounts `enableServeUi` with an empty temp directory,
      - `GET /` returns `404` and `cache-control: no-cache` (and does not throw).

### Verdict

- Keep/squash/split/drop/reorder: keep (correctness + focused test).
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(server): handle missing UI index.html`

---

## 190 2026-01-23 911365366024 `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=911365366024 patchFile=/tmp/leeroy-wip-pass2/190.patch patchSha256=c8fdeeb877b02c42d7c48b314cb6b9cc5029eb4c5e9d36d9ebad3eaf60591ef3 patchBytes=1648 patchLines=42 notesReviewed=yes reviewedAt=2026-01-25T00:42:18+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:22:18+0100 manualReviewedForPrPlan=yes notes=Server-light tooling/test reliability: add server typecheck alias + ensure vitest imports in light specs; keep with server-light stack.

- Subject: fix(server): make build typecheck tests
- Reasons: reliability; ensures `typecheck` exists as a stable script and server tests explicitly import vitest helpers

### Manual Review Notes

- Diffstat: `3 files changed, 3 insertions(+)`
- What changed:
  - Adds a `typecheck` script alias:
    - `server/package.json`: adds `typecheck: "yarn -s build"` (build is `tsc --noEmit`), making it easier to call consistently from root scripts/tooling.
  - Fixes test files missing vitest imports:
    - `server/sources/flavors/light/env.spec.ts` and `server/sources/flavors/light/files.spec.ts`: add `import { describe, expect, it } from 'vitest'`.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Suggested: `fix(server): add typecheck script and vitest imports`

---

## 191 2026-01-23 0dd19e7fb257 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=0dd19e7fb257 patchFile=/tmp/leeroy-wip-pass2/191.patch patchSha256=420a47a5b66edff2b1f183db4ef7d61df10cfb4bc19aa925b96ce453c50da2e2 patchBytes=2263 patchLines=54 notesReviewed=yes reviewedAt=2026-01-25T00:55:34+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Codex resume installer default safety: set codexResumeInstallSpec default to empty; needsSplit=yes to move unrelated sessionRename cleanup in sync/ops into PR29 during rewrite.

- Subject: fix(expo-app): default codex resume spec to empty
- Reasons: safety/portability; avoids shipping a fork-specific default Codex resume installer spec as the global default setting

### Manual Review Notes

- Diffstat: `3 files changed, 2 insertions(+), 11 deletions(-)`
- What changed:
  - Defaults:
    - `expo-app/sources/sync/settings.ts`: changes `codexResumeInstallSpec` default from `@leeroy/codex-mcp-resume@happy-codex-resume` → `''`.
    - `expo-app/sources/sync/settings.spec.ts`: updates the expected default accordingly.
  - Removes dead/incorrect session lookup code:
    - `expo-app/sources/sync/ops.ts`: deletes an unused “get current session encryption” block in `sessionRename` (it was unrelated to rename’s actual source of truth and duplicated the storage lookup below).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none (but conceptually tied to the Codex resume installer/capabilities series).

### Rewrite Commit Message

- Keep subject: `fix(expo-app): default codex resume spec to empty`

---

## 192 2026-01-23 229e5fc36570 `server-light`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=229e5fc36570 patchFile=/tmp/leeroy-wip-pass2/192.patch patchSha256=3cfa929367e1afb15a7c5b134ffd216462cd369ac0d35386d3426101967f6268 patchBytes=1888 patchLines=42 notesReviewed=yes reviewedAt=2026-01-25T00:56:15+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Server-light correctness: form sqlite DB URL via pathToFileURL (proper file:/// + escaping) with test expectation update.

- Subject: fix(server-light): use file URLs for sqlite db
- Reasons: correctness; Prisma sqlite URLs are file URLs and should be formed with proper URL escaping and `file:///` prefix

### Manual Review Notes

- Diffstat: `2 files changed, 3 insertions(+), 2 deletions(-)`
- What changed:
  - `server/sources/flavors/light/env.ts`:
    - Uses `pathToFileURL(dbPath).toString()` instead of string concatenation (`file:${dbPath}`).
  - `server/sources/flavors/light/env.spec.ts`:
    - Updates expectation to `file:///home/test/.../happy-server-light.sqlite`.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(server-light): use file URLs for sqlite db`

---

## 193 2026-01-23 e6f597e436c5 `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e6f597e436c5 patchFile=/tmp/leeroy-wip-pass2/193.patch patchSha256=dc54424a65e56c0638a32fa8153e9b3ab73bd938201980c78e0f796e3cb45d72 patchBytes=2788 patchLines=43 notesReviewed=yes reviewedAt=2026-01-25T00:56:54+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Server-light integration/typecheck: Buffer.from on S3 putObject + tsconfig module settings; needsSplit=yes (keep Buffer.from fix + final tsconfig end-state in PR22; squash tsconfig churn with 194).

- Subject: fix(server): fix server-light typecheck
- Reasons: type correctness for `Uint8Array` uploads and TS module settings compatibility

### Manual Review Notes

- Diffstat: `2 files changed, 3 insertions(+), 3 deletions(-)`
- What changed:
  - Fixes Minio/S3 client typing/behavior:
    - `server/sources/storage/files.ts`: wraps `data` in `Buffer.from(data)` when calling `s3client.putObject(...)`.
  - Adjusts server TS module settings:
    - `server/tsconfig.json`:
      - changes `module` to `nodenext`,
      - sets `moduleResolution` to `nodenext`.
- Coherence / ordering notes:
  - This commit is immediately followed by 194, which changes the TS module settings again. During history rewrite, 193 and 194 should likely be squashed or resolved into a single “final tsconfig for typecheck” commit.

### Verdict

- Keep/squash/split/drop/reorder: keep, but squash with 194 (tsconfig churn).
- Depends on: TypeScript config expectations for the server package.

### Rewrite Commit Message

- If squashed: keep whichever tsconfig end-state we want, and keep the `Buffer.from` fix.

---

## 194 2026-01-23 1564e800e355 `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1564e800e355 patchFile=/tmp/leeroy-wip-pass2/194.patch patchSha256=5462f80f29297c9898605cd75ae9cdd05fa4f00cd69672982ed31c9fbb99a8bb patchBytes=2233 patchLines=30 notesReviewed=yes reviewedAt=2026-01-25T00:57:31+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=TS config churn follow-up: squash into 193 and keep only final server tsconfig module settings that make typecheck pass.

- Subject: fix(server): relax module settings for typecheck
- Reasons: typecheck compatibility; relaxes module config to avoid typecheck breakage under stricter `nodenext` settings

### Manual Review Notes

- Diffstat: `1 file changed, 2 insertions(+), 2 deletions(-)`
- What changed:
  - `server/tsconfig.json`:
    - changes `module` from `nodenext` → `ESNext`,
    - changes `moduleResolution` from `nodenext` → `node`.
- Coherence / ordering notes:
  - This is pure config churn relative to 193; they should be grouped together during rewrite.

### Verdict

- Keep/squash/split/drop/reorder: squash into 193 (choose one final module configuration).
- Depends on: TypeScript + runtime expectations for server build.

### Rewrite Commit Message

- If squashed: incorporate into 193 and avoid keeping a standalone tsconfig flip-flop commit.

---

## 195 2026-01-23 8ab11f2788c1 `happy-cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=8ab11f2788c1 patchFile=/tmp/leeroy-wip-pass2/195.patch patchSha256=afb436043cf24622040672e89f8cac022dc62b699fb95bf168a58cd8d7ad780f patchBytes=1277 patchLines=29 notesReviewed=yes reviewedAt=2026-01-25T00:58:11+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Capabilities robustness: parse CLI version semver from full combined stdout+stderr, not only first line.

- Subject: fix(happy-cli): parse CLI versions from full output
- Reasons: reliability; some CLIs may emit version strings outside the first line (or to stderr), so parsing only the first line is brittle

### Manual Review Notes

- Diffstat: `1 file changed, 3 insertions(+), 3 deletions(-)`
- What changed:
  - `cli/src/modules/common/capabilities/snapshots/cliSnapshot.ts`:
    - Combines `stdout`+`stderr` into one string.
    - Attempts semver extraction from:
      - `firstLine`, and if that fails,
      - the full combined output.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `extractSemver` behavior and CLI `--version` output formats.

### Rewrite Commit Message

- Keep subject: `fix(happy-cli): parse CLI versions from full output`

---

## 196 2026-01-23 21e096e9454e `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=21e096e9454e patchFile=/tmp/leeroy-wip-pass2/196.patch patchSha256=37006e85b70998764baca225929819c4341579e63d48fc4ccc7d4673d4219a13 patchBytes=8089 patchLines=149 notesReviewed=yes reviewedAt=2026-01-25T00:58:49+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Settings UI i18n: localize Session settings item title/subtitle in SettingsView with new keys across locales; fold into settings screens PR.

- Subject: fix(i18n): localize session settings item
- Reasons: i18n completeness; removes hard-coded “Session” strings in settings list and adds translations

### Manual Review Notes

- Diffstat: `10 files changed, 29 insertions(+), 2 deletions(-)`
- What changed:
  - `expo-app/sources/components/SettingsView.tsx`:
    - Replaces hard-coded title/subtitle strings with:
      - `t('settings.session')`
      - `t('settings.sessionSubtitleTmuxEnabled')` or `t('settings.sessionSubtitleMessageSendingAndTmux')`
  - Adds the three translation keys across locales (`ca`, `en`, `es`, `it`, `ja`, `pl`, `pt`, `ru`, `zh-Hans`).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(i18n): localize session settings item`

---

## 197 2026-01-23 7ca9e93196d6 `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=7ca9e93196d6 patchFile=/tmp/leeroy-wip-pass2/197.patch patchSha256=53fd77986785ca2a159580e1937ae145ad127adb87ad8ef48ea6c0b909e629e2 patchBytes=9742 patchLines=176 notesReviewed=yes reviewedAt=2026-01-25T00:59:32+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Tools UX i18n: translate ExitPlanMode tool view strings across locales; fold into AskUserQuestion/ExitPlan handling PR.

- Subject: fix(i18n): translate ExitPlanMode strings
- Reasons: i18n completeness; replaces English placeholder strings for ExitPlanMode tool UI with real translations

### Manual Review Notes

- Diffstat: `8 files changed, 40 insertions(+), 40 deletions(-)`
- What changed:
  - Updates `toolViews.exitPlanMode` translations in:
    - `expo-app/sources/text/translations/ca.ts`
    - `expo-app/sources/text/translations/es.ts`
    - `expo-app/sources/text/translations/it.ts`
    - `expo-app/sources/text/translations/ja.ts`
    - `expo-app/sources/text/translations/pl.ts`
    - `expo-app/sources/text/translations/pt.ts`
    - `expo-app/sources/text/translations/ru.ts`
    - `expo-app/sources/text/translations/zh-Hans.ts`
  - Replaces values like “Approve Plan / Reject / Response sent …” with localized equivalents.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: ExitPlanMode tool view using these keys (introduced earlier in the tool UX commits).

### Rewrite Commit Message

- Keep subject: `fix(i18n): translate ExitPlanMode strings`

---

## 198 2026-01-23 c4a81947f002 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c4a81947f002 patchFile=/tmp/leeroy-wip-pass2/198.patch patchSha256=fc35d802a1475928b9ac9aaab7c7b11812fc022516f692c7d9ea010fdda18fd1 patchBytes=4955 patchLines=136 notesReviewed=yes reviewedAt=2026-01-25T09:03:20+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR35 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Friends search UX: useSearch returns stable error code + UI localizes via errors.searchFailed; includes debounce/retry hook test.

- Subject: fix(i18n): localize search error
- Reasons: correctness + UX; switches from displaying raw error strings to a stable error code + localized message

### Manual Review Notes

- Diffstat: `3 files changed, 56 insertions(+), 8 deletions(-)`
- What changed:
  - Makes `useSearch` return a stable error code (instead of arbitrary string content):
    - `expo-app/sources/hooks/useSearch.ts`:
      - introduces `UseSearchError = 'searchFailed'`,
      - changes the hook return type to `error: UseSearchError | null`.
    - `expo-app/sources/hooks/useSearch.hook.test.ts` (new):
      - uses fake timers to drive debounce + retry, and asserts `error === 'searchFailed'` after retries fail.
  - Localizes the UI display:
    - `expo-app/sources/app/(app)/friends/search.tsx`:
      - maps `searchError === 'searchFailed'` to `t('errors.searchFailed')` and renders that instead of the raw error value.

### Verdict

- Keep/squash/split/drop/reorder: keep (behavior improvement + test).
- Depends on: `errors.searchFailed` translation key existing (it already does).

### Rewrite Commit Message

- Suggested: `fix(search): localize failure messaging via stable error codes`

---

## 199 2026-01-23 208f0222a997 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=208f0222a997 patchFile=/tmp/leeroy-wip-pass2/199.patch patchSha256=7bdee0d446ee03b8685de6983aaeff73f7d3d11a1639aee62c4874ccdd9847d4 patchBytes=17834 patchLines=346 notesReviewed=yes reviewedAt=2026-01-25T09:04:11+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Mixed new-session web UX + resume-gating i18n; needsSplit=yes: keep BaseModal web wizard presentation in PR26 and move Codex resume dialog localization keys into PR19 during rewrite (or keep together if review prefers).

- Subject: fix(i18n): localize Codex resume dialog
- Reasons: UX/i18n; replaces hard-coded English modal strings and improves the web UX around new-session flow

### Manual Review Notes

- Diffstat: `10 files changed, 119 insertions(+), 18 deletions(-)`
- What changed (mixed concern commit):
  - Localizes the “Codex resume not installed” confirmation dialog:
    - `expo-app/sources/app/(app)/new/index.tsx`:
      - replaces hard-coded title/body/confirm text with:
        - `t('errors.codexResumeNotInstalledTitle')`
        - `t('errors.codexResumeNotInstalledMessage')`
        - `t('common.openMachine')` (later moved to `connect.openMachine` in 200).
    - Adds translations for the new keys across locales.
  - Web UX refactor for the new-session screen:
    - `expo-app/sources/app/(app)/new/index.tsx`:
      - wraps the wizard in a `BaseModal` on web and adds an explicit modal header + close button,
      - reads `screenHeight` to constrain max modal height.
    - This is a meaningful UI change not directly implied by the subject.

### Verdict

- Keep/squash/split/drop/reorder: keep, but split during rewrite (recommended) because it mixes i18n strings with a substantial web UI refactor.
- Depends on: `BaseModal` behavior on web and translation key structure.

### Rewrite Commit Message

- Suggested split:
  - `fix(i18n): localize Codex resume not installed dialog`
  - `fix(web): present new session wizard in a modal`

---

## 200 2026-01-23 02ad6b4a8352 `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=02ad6b4a8352 patchFile=/tmp/leeroy-wip-pass2/200.patch patchSha256=d1419ce45a807f126dc4c21c56f17936ce8fbcc31d35baba569b7d7a200b6bed patchBytes=10293 patchLines=181 notesReviewed=yes reviewedAt=2026-01-25T09:04:54+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T14:29:07+0100 manualReviewedForPrPlan=yes notes=Follow-up taxonomy for new-session dialog: move openMachine key from common.* to connect.* across locales; keep adjacent to 199 in new-session PR.

- Subject: fix(i18n): move openMachine to connect scope
- Reasons: i18n taxonomy; groups “Open machine” under connect/restore flow instead of global/common

### Manual Review Notes

- Diffstat: `9 files changed, 9 insertions(+), 9 deletions(-)`
- What changed:
  - `expo-app/sources/app/(app)/new/index.tsx`:
    - changes confirm button text key from `t('common.openMachine')` → `t('connect.openMachine')`.
  - Moves the `openMachine` translation entry in multiple locales from the `common` section to the `connect` section (same string values, new scope).

### Verdict

- Keep/squash/split/drop/reorder: keep (small, coherent i18n refactor).
- Depends on: translation structure consistency across locales.

### Rewrite Commit Message

- Keep subject: `fix(i18n): move openMachine to connect scope`

---

## 201 2026-01-23 ce42bf56d7b6 `web`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ce42bf56d7b6 patchFile=/tmp/leeroy-wip-pass2/201.patch patchSha256=7b89641df6feec9562a56dce866b5e01ca8687518d877726567a72d1710c00c6 patchBytes=1462 patchLines=30 notesReviewed=yes reviewedAt=2026-01-25T09:05:34+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Web runtime fix: force CJS libsodium-wrappers build to avoid top-level await parse failures; keep with foundations/crypto web stability.

- Subject: fix(web): correct libsodium wrapper typing
- Reasons: web runtime correctness; avoids Metro web blank-page failures caused by top-level `await` in ESM builds, while keeping TypeScript typing usable

### Manual Review Notes

- Diffstat: `1 file changed, 12 insertions(+), 2 deletions(-)`
- What changed:
  - `expo-app/sources/encryption/libsodium.lib.web.ts`:
    - switches from `import sodium from 'libsodium-wrappers'` to a `require(...)` of the CommonJS build:
      - `libsodium-wrappers/dist/modules/libsodium-wrappers.js`
    - adds a detailed comment explaining why:
      - Metro web executed as classic scripts,
      - ESM builds can introduce top-level `await` via polyfills and crash parsing.
    - exports the required module with an explicit cast.
- Coherence:
  - Message matches the diff; the “typing” part is mostly about ensuring the default export is typed.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `libsodium-wrappers` continuing to ship the referenced CJS build path.

### Rewrite Commit Message

- Suggested: `fix(web): force CJS libsodium-wrappers build to avoid top-level await`

---

## 202 2026-01-23 f0417ca11ac5 `web`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=f0417ca11ac5 patchFile=/tmp/leeroy-wip-pass2/202.patch patchSha256=550d3a822036e75747a8c2fd8c3269c1dd9493d72685b1451e53ecfa5d4627bf patchBytes=851 patchLines=18 notesReviewed=yes reviewedAt=2026-01-25T09:06:11+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Follow-up typing fix to 201; squash into 201 in rewritten history.

- Subject: fix(web): keep libsodium wrapper namespace typing
- Reasons: TypeScript correctness; ensures the exported value preserves the `typeof sodiumType` namespace typing

### Manual Review Notes

- Diffstat: `1 file changed, 1 insertion(+), 1 deletion(-)`
- What changed:
  - `expo-app/sources/encryption/libsodium.lib.web.ts`:
    - changes the export cast from `sodium as unknown as sodiumType` to `sodium as typeof sodiumType`.

### Verdict

- Keep/squash/split/drop/reorder: keep; squash into 201 in a rewritten history (direct follow-up).
- Depends on: 201.

### Rewrite Commit Message

- If squashed: drop this commit and keep 201’s message.

---

## 203 2026-01-23 b6d3874bf175 `settings`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=b6d3874bf175 patchFile=/tmp/leeroy-wip-pass2/203.patch patchSha256=f8cf00040ad9e6f22b593cd3a78a4e8a1f2c843cef39aa267bb6362835b6d542 patchBytes=893 patchLines=22 notesReviewed=yes reviewedAt=2026-01-25T09:06:54+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Settings/experiments UX correctness: master experiments toggle also controls expCodexResume.

- Subject: fix(settings): include Codex resume in master experiment toggle
- Reasons: UX correctness; turning off the master “experiments” switch should also disable the Codex resume experiment flag

### Manual Review Notes

- Diffstat: `1 file changed, 2 insertions(+)`
- What changed:
  - `expo-app/sources/app/(app)/settings/features.tsx`:
    - when toggling experiments on/off, now also sets `expCodexResume` to the same value.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(settings): include Codex resume in master experiment toggle`

---

## 204 2026-01-23 a4d7faba961f `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a4d7faba961f patchFile=/tmp/leeroy-wip-pass2/204.patch patchSha256=fae26fc1c80f31081e159239c0c36c1d6bbeb572813a12230a3d9b2be1a93df7 patchBytes=23213 patchLines=338 notesReviewed=yes reviewedAt=2026-01-25T09:07:54+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Session UI i18n/copy: localize error fallbacks and profile info modal in SessionView; needsSplit=yes to move expInboxFriends translations into PR06 during rewrite.

- Subject: fix(i18n): localize session error fallbacks
- Reasons: UX/i18n; removes hard-coded English “Error / failed to …” fallbacks and localizes session-level error messaging

### Manual Review Notes

- Diffstat: `10 files changed, 67 insertions(+), 13 deletions(-)`
- What changed:
  - Localizes error modal title and fallback messages:
    - `expo-app/sources/-session/SessionView.tsx`:
      - replaces `Modal.alert('Error', ...)` with `Modal.alert(t('common.error'), ...)`,
      - uses `t('errors.failedToResumeSession')` and `t('errors.failedToSendMessage')` when the caught value isn’t an `Error`.
  - Localizes the “profile info” modal body:
    - `SessionView.tsx`: replaces hard-coded strings (“This session uses… Profiles are fixed…”) with:
      - `t('profiles.sessionUses', { profile })`
      - `t('profiles.profilesFixedPerSession')`
  - Adds new translation keys across locales:
    - `errors.failedToResumeSession`, `errors.failedToSendMessage`
    - `profiles.sessionUses`, `profiles.profilesFixedPerSession`
    - Also adds `expInboxFriends` / `expInboxFriendsSubtitle` translations (co-located in the same commit; slightly mixed but still “i18n completeness”).

### Verdict

- Keep/squash/split/drop/reorder: keep (user-facing correctness), but consider splitting out unrelated `expInboxFriends` translation additions during rewrite if they belong to a different feature.
- Depends on: `SessionView` using these keys; translation schemas across locales.

### Rewrite Commit Message

- Suggested: `fix(i18n): localize session UI fallbacks and profile modal copy`

---

## 205 2026-01-23 1ea2bddb8bbd `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1ea2bddb8bbd patchFile=/tmp/leeroy-wip-pass2/205.patch patchSha256=947dddc58e587ed05156cc9f9f5718acfe729a445322443c4dfdb6917b1678e7 patchBytes=1753 patchLines=45 notesReviewed=yes reviewedAt=2026-01-25T09:08:38+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR23 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Server public-files/image pipeline test correctness: validate processImage contract (original dimensions, resized pixel buffer length).

- Subject: test(server): validate processImage resize output
- Reasons: test correctness; ensures pixel buffer length reflects the resized dimensions (while width/height metadata remains original)

### Manual Review Notes

- Diffstat: `1 file changed, 11 insertions(+), 6 deletions(-)`
- What changed:
  - `server/sources/storage/processImage.spec.ts`:
    - makes dimensions explicit (`originalWidth/Height`, `targetWidth/Height`),
    - asserts `result.width/height` are original dimensions,
    - asserts `result.pixels.length === targetWidth * targetHeight * 4` (RGBA buffer length),
    - keeps thumbhash length check.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: `processImage` contract (returns original dimensions but resized pixel data).

### Rewrite Commit Message

- Keep subject: `test(server): validate processImage resize output`

---

## 206 2026-01-23 1e7d7f8a4747 `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1e7d7f8a4747 patchFile=/tmp/leeroy-wip-pass2/206.patch patchSha256=5a1c59132999e1cfef8bbb4c0ce93378543ec5f5bcb20b40cfa4ffc4b7da7176 patchBytes=621 patchLines=20 notesReviewed=yes reviewedAt=2026-01-25T09:09:48+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Server-light correctness: keep light server process alive (remove process.exit(0) after startServer resolves).

- Subject: fix(server): keep light server running
- Reasons: correctness; `main.light.ts` should not exit immediately after `startServer('light')` resolves (that would shut down the server process)

### Manual Review Notes

- Diffstat: `1 file changed, 3 deletions(-)`
- What changed:
  - `server/sources/main.light.ts`:
    - removes the `.then(() => process.exit(0))` continuation.
    - Now the process only exits on startup failure (catch) or external termination.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: none.

### Rewrite Commit Message

- Keep subject: `fix(server): keep light server running`

---

## 207 2026-01-23 61bf70183ebf `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=61bf70183ebf patchFile=/tmp/leeroy-wip-pass2/207.patch patchSha256=ad7f6fd4c987e6b1bc2e2582d195216aef90797d8924ff2e2497337a6c5d7c29 patchBytes=939 patchLines=24 notesReviewed=yes reviewedAt=2026-01-25T09:10:29+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Test correctness: set DEBUG before importing logger (env read at import time); can fold into foundations/test stability.

- Subject: test(cli): set DEBUG before importing logger
- Reasons: test correctness; `logger` reads env at import time, so setting `DEBUG` after import won’t enable debug-mode logging behavior

### Manual Review Notes

- Diffstat: `1 file changed, 2 insertions(+), 1 deletion(-)`
- What changed:
  - `cli/src/ui/logger.test.ts`:
    - moves `process.env.DEBUG = '1'` before importing `@/ui/logger`.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Depends on: logger module initialization reading env variables during import.

### Rewrite Commit Message

- Keep subject: `test(cli): set DEBUG before importing logger`

---

## 208 2026-01-23 61a18ac95e17 `tools`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=61a18ac95e17 patchFile=/tmp/leeroy-wip-pass2/208.patch patchSha256=4f8be371069810a2e3be4909fc95ce83aa3f21d23c226062ed56341f3a57e638 patchBytes=14286 patchLines=272 notesReviewed=yes reviewedAt=2026-01-25T09:11:18+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Tools safety: require permission id for ExitPlanMode approve/reject actions; adds i18n key + focused tests.

- Subject: fix(tools): require permission id for ExitPlanMode actions
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Hardens the “ExitPlanMode” tool UX by making the permission id mandatory for approve/reject actions:
  - `expo-app/sources/components/tools/views/ExitPlanToolView.tsx`:
    - `handleApprove` / `handleReject` now read `const permissionId = tool.permission?.id`.
    - If missing, they show a modal error (`Modal.alert(t('common.error'), t('errors.missingPermissionId'))`) and **return without** calling `sessionAllow/sessionDeny` and without marking the tool as responded.
    - Otherwise they call the RPC with a stable `permissionId` and only then set `isResponded`.
  - Adds translations for the new error key `errors.missingPermissionId` across all locales.
  - `expo-app/sources/components/tools/views/ExitPlanToolView.test.ts` adds explicit coverage for the “missing permission id” cases:
    - pressing approve/reject should not call RPCs
    - should display the modal error
    - should not transition into the “responded” state (buttons remain present).

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a correctness + safety hardening change (prevents silent no-op approvals and reduces state inconsistencies).
- Coherence: good (single behavior: require a permission id for this tool’s decision buttons).
- Depends on:
  - `tool.permission?.id` being the canonical id used by `sessionAllow/sessionDeny`.
  - `Modal.alert` being available in the runtime (native + web).
  - New i18n key `errors.missingPermissionId` existing (this commit adds it).

### Rewrite Commit Message

- Keep as-is: `fix(tools): require permission id for ExitPlanMode actions`

---

## 209 2026-01-23 5b36c9bf91c1 `tools`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=5b36c9bf91c1 patchFile=/tmp/leeroy-wip-pass2/209.patch patchSha256=de2d00371feefc5671e0d0c0195bcca95675ff8fe781d6a1fc9081b791f4fc51 patchBytes=1226 patchLines=30 notesReviewed=yes reviewedAt=2026-01-25T09:12:06+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Test follow-up to 208: avoid null permission shape in ExitPlanToolView tests; squash into 208.

- Subject: test(tools): avoid null permission in ExitPlanToolView tests
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Small type-alignment fix in tests:
  - `ExitPlanToolView.test.ts` changes `permission: null` to `permission: undefined` for the “missing permission” cases.
  - This matches typical `ToolCall.permission?: ...` shapes and avoids relying on `null` where the runtime may omit the field.

### Verdict

- Keep/squash/split/drop/reorder: squash into 208 during history rewrite (it’s a direct follow-up to make the new tests type-correct).
- Depends on: 208 (the tests were introduced there).

### Rewrite Commit Message

- If squashed: drop this commit and keep 208’s message.

---

## 210 2026-01-23 b7f63e928e4f `persistence`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=b7f63e928e4f patchFile=/tmp/leeroy-wip-pass2/210.patch patchSha256=46f96d0e09165e28b91c1a31626cf02420872d0500b1122a321e8e8a90eba993 patchBytes=3215 patchLines=87 notesReviewed=yes reviewedAt=2026-01-25T09:29:01+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T14:34:52+0100 manualReviewedForPrPlan=yes notes=Daemon/CLI race reliability: retry readDaemonState on ENOENT when state file appears shortly after start; includes regression test.

- Subject: fix(persistence): retry daemon state read when file appears
- Reasons: reliability (filesystem race between daemon state writer/reader)

### Manual Review Notes

- Fixes a race where `readDaemonState()` could return `null` even though the daemon state file appears “immediately after” the read begins:
  - `cli/src/persistence.ts`:
    - Removes the up-front `existsSync(configuration.daemonStateFile)` early-return.
    - Keeps the existing 3-attempt loop, and treats `ENOENT` specially:
      - if `ENOENT` and not the last attempt: waits `15ms` and retries
      - if `ENOENT` on attempt 3: returns `null`
    - Still fails closed for parse/corruption (`JSON.parse` errors → log + `null`).
- Adds a focused regression test for the “file appears shortly after start” case:
  - `cli/src/persistence.daemonState.test.ts`:
    - creates a temporary `HAPPY_HOME_DIR`
    - schedules a write of `configuration.daemonStateFile` after `5ms`
    - asserts `readDaemonState()` returns the written `pid`.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good (single bugfix + regression test).
- Notes:
  - The test uses real timers; it should be stable given the retry delay (`15ms`) and 3 attempts, but it is still timing-based. If it flakes, switch to fake timers (`vi.useFakeTimers`) and advance deterministically.
- Depends on:
  - Daemon state being written atomically (the code comment notes rename semantics).
  - `readDaemonState` callers accepting `null` as “daemon not running / unknown”.

### Rewrite Commit Message

- Keep as-is: `fix(persistence): retry daemon state read when file appears`

---

## 211 2026-01-23 55428fb0253c `tools`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=55428fb0253c patchFile=/tmp/leeroy-wip-pass2/211.patch patchSha256=c35ec65034680683cb38c6c312a7186b6bb3f1745451e93b301d64e8ec9bc16a patchBytes=4597 patchLines=114 notesReviewed=yes reviewedAt=2026-01-25T09:29:44+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Tools safety: AskUserQuestionView shows modal error + no-ops when permission id missing (align with ExitPlanMode hardening); includes targeted test.

- Subject: fix(tools): alert when AskUserQuestion permission id is missing
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Replaces a “throw on missing permission id” failure mode with a user-visible error and a safe no-op:
  - `expo-app/sources/components/tools/views/AskUserQuestionView.tsx`:
    - Previously: missing `tool.permission?.id` caused a thrown error.
    - Now: shows `Modal.alert(t('common.error'), t('errors.missingPermissionId'))` and returns early (no RPC call).
  - Adds a targeted test (`AskUserQuestionView.test.ts`) that:
    - selects an option to enable submit
    - presses submit with `permission: undefined`
    - asserts no submission/deny/message side effects and that the modal error is shown.
- Note: this commit relies on the i18n key `errors.missingPermissionId` introduced in 208.

### Verdict

- Keep/squash/split/drop/reorder: keep; it’s consistent with 208’s hardening approach (permission id must exist before user actions are accepted).
- Coherence: good.
- Depends on:
  - `Modal.alert` runtime support.
  - `errors.missingPermissionId` translation key (best kept adjacent to 208).

### Rewrite Commit Message

- Keep as-is: `fix(tools): alert when AskUserQuestion permission id is missing`

---

## 212 2026-01-23 dc6955f88abd `terminal`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=dc6955f88abd patchFile=/tmp/leeroy-wip-pass2/212.patch patchSha256=ed6b94e9fb61f1dd5c50c63cb39dfc022b5fd6269875dee5ab86f9d2dca7d44e patchBytes=4638 patchLines=118 notesReviewed=yes reviewedAt=2026-01-25T09:30:31+01:00 notesUpdated=no
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Terminal attachment security: encode sessionId -> filename (encodeURIComponent) for attachment info storage w/ legacy fallback; keep adjacent to 237/238 which fully close legacy traversal hole.

- Subject: fix(terminal): prevent sessionId path traversal in attachment info
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Addresses an important filesystem-safety issue in CLI terminal attachment metadata:
  - `cli/src/terminal/terminalAttachmentInfo.ts`:
    - Introduces `sessionIdToFilename(sessionId) = encodeURIComponent(sessionId)` and stores attachment info under `terminal/sessions/<encoded>.json`.
    - `readTerminalAttachmentInfo` now prefers the encoded filename and falls back to the **legacy raw filename** on `ENOENT`.
  - `cli/src/terminal/terminalAttachmentInfo.test.ts` adds coverage for:
    - writing with a traversal-ish `sessionId` (e.g. `../evil/session`) results in an encoded filename under the intended sessions dir
    - reading still supports legacy files named with the raw `sessionId` (back-compat).

### Notes / Risks / Follow-ups (for history rewrite planning)

- This commit fixes the primary write/read path by encoding the filename, but the legacy fallback path (`<raw-sessionId>.json`) is still potentially dangerous for `sessionId` values containing path separators. That hole is addressed by the later pair:
  - 237 (test): ensures legacy fallback is not used when traversal would occur
  - 238 (fix): blocks legacy fallback when `sessionId` contains `/` or `\\`

### Verdict

- Keep/squash/split/drop/reorder: keep, but treat as a 3-commit security patch series with 237/238 and keep them adjacent during rewrite.
- Coherence: good (single theme: safe on-disk naming for terminal attachment info).
- Depends on:
  - Call sites treating `sessionId` as untrusted (this change makes the storage layer robust).
  - `encodeURIComponent` being stable across platforms (Node).

### Rewrite Commit Message

- Suggested tweak (optional): `fix(cli): encode terminal attachment sessionId filenames`
- Current is acceptable: `fix(terminal): prevent sessionId path traversal in attachment info`

---

## 213 2026-01-23 bb685fbeff7b `terminal`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=bb685fbeff7b patchFile=/tmp/leeroy-wip-pass2/213.patch patchSha256=7c8a3f88614f35ea504045d16fc2ca04cc3e8d22887dd51c0c24e82fb33e8da7 patchBytes=1726 patchLines=41 notesReviewed=yes reviewedAt=2026-01-25T09:32:18+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=TMUX/terminal CLI argv correctness: validate --happy-starting-mode has a real value (not missing / not another flag); includes targeted test.

- Subject: fix(terminal): clarify missing --happy-starting-mode value
- Reasons: reliability (avoid silently consuming the next flag as a value; clearer CLI errors)

### Manual Review Notes

- Hardens `ensureRemoteStartingModeArgs(argv)` argument validation:
  - `cli/src/terminal/headlessTmuxArgs.ts`:
    - after finding `--happy-starting-mode`, it now rejects:
      - missing value (`undefined`)
      - or “value” that is actually another flag (starts with `--`)
    - throws a specific error: `Missing value for --happy-starting-mode (expected "remote" or "local")`.
- Adds a targeted test:
  - `cli/src/terminal/headlessTmuxArgs.test.ts` asserts `['--happy-starting-mode']` throws and mentions the flag.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good (single CLI argument correctness fix + test).
- Depends on: this helper being the canonical validator for headless tmux CLI argv shaping.

### Rewrite Commit Message

- Keep as-is: `fix(terminal): clarify missing --happy-starting-mode value`

---

## 214 2026-01-23 5bcc7d20cd6b `utils`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=5bcc7d20cd6b patchFile=/tmp/leeroy-wip-pass2/214.patch patchSha256=a232db21349f2034b285fc47397de2f56292370bfcd58f90cb1a9859f156a38e patchBytes=1683 patchLines=42 notesReviewed=yes reviewedAt=2026-01-25T09:32:38+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Test stability: make serverConnectionErrors retry/jitter deterministic by stubbing Math.random and ensuring cleanup; keep with CLI backoff/reconnect reliability.

- Subject: test(utils): stabilize serverConnectionErrors retry count test
- Reasons: test reliability (remove nondeterministic jitter behavior)

### Manual Review Notes

- Fixes a flaky test by making the exponential-backoff jitter deterministic:
  - `cli/src/utils/serverConnectionErrors.test.ts`:
    - `vi.spyOn(Math, 'random').mockReturnValue(0)` ensures the jittered delays are predictable.
    - wraps the test body in `try/finally` to always `handle.cancel()` and restore `Math.random`, preventing leakage into other tests.

### Verdict

- Keep/squash/split/drop/reorder: keep (or squash into the commit that introduced this reconnection/backoff test during history rewrite).
- Coherence: good (pure test stabilization).
- Depends on: backoff jitter being driven by `Math.random` (this makes the test deterministic).

### Rewrite Commit Message

- Keep as-is: `test(utils): stabilize serverConnectionErrors retry count test`

---

## 215 2026-01-23 82968aa8fe6c `ui`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=82968aa8fe6c patchFile=/tmp/leeroy-wip-pass2/215.patch patchSha256=43774cf387b7e18f68eb6c5eb81468fabb4d1c02135e4ea1345598fe0d5d5c41 patchBytes=10658 patchLines=262 notesReviewed=yes reviewedAt=2026-01-25T09:33:08+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Pending messages integrity: enforce abort→send→delete→close ordering in PendingMessagesModal; adds testIDs + regression tests incl send-failure behavior.

- Subject: fix(ui): send pending messages before closing modal
- Reasons: correctness + data integrity (avoid closing/removing pending messages before they are sent)

### Manual Review Notes

- Fixes the “Send now” flow ordering so the message is actually sent before the modal is closed and the pending record is removed:
  - `expo-app/sources/components/PendingMessagesModal.tsx`:
    - `handleSendNow` / `handleSendDiscardedNow` now do:
      1) `sessionAbort(sessionId)` (ensure session stops)
      2) `sync.sendMessage(sessionId, text)`
      3) delete the pending/discarded pending message
      4) then `props.onClose()`
    - previously the code deleted + closed first, then aborted + sent, which could lose data if later steps failed or the UI navigated away.
    - adds stable `testID`s for relevant action buttons and plumbs `testID` through `ActionButton`.
- Adds regression coverage:
  - `expo-app/sources/components/PendingMessagesModal.test.ts`:
    - asserts `abort → send → delete → close` ordering using `invocationCallOrder`
    - asserts that on send failure, it does not delete and does not close, and shows an alert.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good (single user-visible flow fix + test hooks + tests).
- Depends on:
  - `sessionAbort` being safe to call before sending a pending message.
  - `sync.sendMessage` semantics not requiring the session to remain “active” in UI state.

### Rewrite Commit Message

- Keep as-is: `fix(ui): send pending messages before closing modal`

---

## 216 2026-01-23 b7219c8bfe2b `ink`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=b7219c8bfe2b patchFile=/tmp/leeroy-wip-pass2/216.patch patchSha256=5e58977cf057fa2618b7dafcacda2b7744b0cf6a55984d5c260871d0721177d9 patchBytes=2959 patchLines=63 notesReviewed=yes reviewedAt=2026-01-25T09:33:26+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR31 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Ink lifecycle reliability: avoid async useInput handler; use cancellable timeouts and clear on unmount/messageBuffer changes (prevents callbacks after teardown).

- Subject: fix(ink): avoid async useInput handler
- Reasons: reliability (Ink input handlers should not be `async` / avoid dangling promises on unmount)

### Manual Review Notes

- Refactors `useInput` handling to avoid an `async` callback and to make delayed actions cancellable:
  - `cli/src/ui/ink/RemoteModeDisplay.tsx`:
    - changes `useInput(async ...)` to `useInput((...) => { ... })`
    - replaces `await new Promise(setTimeout)` with `setTimeout(() => onExit?.(), 100)` / `setTimeout(() => onSwitchToLocal?.(), 100)`
    - introduces `actionTimeoutRef` and clears timeouts when the component unmounts / `messageBuffer` changes, preventing callbacks firing after teardown.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good (single Ink lifecycle/input reliability fix).
- Depends on: consumers expecting a small delay before `onExit`/`onSwitchToLocal` is invoked (this preserves that behavior).

### Rewrite Commit Message

- Keep as-is: `fix(ink): avoid async useInput handler`

---

## 217 2026-01-23 d70a6f6669fe `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d70a6f6669fe patchFile=/tmp/leeroy-wip-pass2/217.patch patchSha256=ff8d0ea2b66af501ee8d4d6b7f4fdfc21ec3cb6bf3b99b228dccdbcde1f8a697 patchBytes=1956 patchLines=38 notesReviewed=yes reviewedAt=2026-01-25T09:33:54+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Claude robustness: trim CLAUDE_CONFIG_DIR overrides (treat as user input) + regression test; keep with Claude session reliability.

- Subject: fix(claude): trim CLAUDE_CONFIG_DIR overrides
- Reasons: correctness (treat env var overrides robustly; avoid creating paths with accidental whitespace)

### Manual Review Notes

- Ensures `CLAUDE_CONFIG_DIR` (or the explicit override param) is interpreted as a trimmed path:
  - `cli/src/claude/utils/path.ts`:
    - changes “use raw value if `.trim()` is truthy” to “use trimmed value”.
  - `cli/src/claude/utils/path.test.ts` adds a regression test showing whitespace is removed.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good.
- Depends on: `CLAUDE_CONFIG_DIR` being treated as user-provided input (this makes it more forgiving).

### Rewrite Commit Message

- Keep as-is: `fix(claude): trim CLAUDE_CONFIG_DIR overrides`

---

## 218 2026-01-23 47f638b73fca `utils`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=47f638b73fca patchFile=/tmp/leeroy-wip-pass2/218.patch patchSha256=24e9479a84f5a7da8b9fdeb89504d4eb9efa17ec0419831ceab08c40555a5a17 patchBytes=1588 patchLines=50 notesReviewed=yes reviewedAt=2026-01-25T09:34:15+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Backoff semantics test: failuresCount increments per attempt under custom backoff; keep adjacent to 219 (or squash into 219).

- Subject: test(utils): cover failuresCount with custom backoff
- Reasons: reliability/observability (ensure consistent `failuresCount` semantics for error callbacks)

### Manual Review Notes

- Adds a regression test defining desired behavior when `InvalidateSync` is configured with a custom backoff:
  - `cli/src/utils/sync.test.ts`:
    - supplies a custom `backoff` wrapper that retries a callback 3 times
    - `InvalidateSync`’s command always throws
    - asserts `onError` is called 3 times with `failuresCount` `[1, 2, 3]`.

### Verdict

- Keep/squash/split/drop/reorder: keep, but keep adjacent to 219 (the implementation fix).
- Coherence: good.
- Depends on: `InvalidateSync` calling `onError` for each failed attempt (not just once at the end).

### Rewrite Commit Message

- If kept separate: keep as-is: `test(utils): cover failuresCount with custom backoff`
- If squashed into 219: drop this commit and keep 219’s message.

---

## 219 2026-01-23 2abc70fef03c `utils`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=2abc70fef03c patchFile=/tmp/leeroy-wip-pass2/219.patch patchSha256=ba77d759456e68198cf8d274e54891c0f7372796e47aae5df7f869752a6df585 patchBytes=2008 patchLines=50 notesReviewed=yes reviewedAt=2026-01-25T09:34:44+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Backoff semantics fix: move failuresCount tracking into InvalidateSync so it works with custom backoff implementations; optionally squash 218 into this.

- Subject: fix(utils): track failuresCount for custom backoff
- Reasons: correctness (make `failuresCount` behavior consistent across default vs custom backoff implementations)

### Manual Review Notes

- Moves the responsibility for incrementing/reporting `failuresCount` into `InvalidateSync`, so it works even when callers provide a custom `backoff`:
  - `cli/src/utils/sync.ts`:
    - no longer passes a custom `onError` into `createBackoff(...)`; uses `createBackoff()` as-is.
    - wraps `this._command()` inside the callback given to `_backoff`:
      - on error: increments `this._lastFailureCount`, calls `this._onError(e, failuresCount)`, then rethrows so the backoff can retry.
    - outer `catch` is now a “fail-safe”:
      - if the backoff throws before any attempt happens, it reports a single failure (`failuresCount = 1`).
- This aligns with the behavior tested in 218 and avoids coupling error counting to a specific backoff implementation.

### Verdict

- Keep/squash/split/drop/reorder: keep (optionally squash with 218).
- Coherence: good (single behavior change + support for custom backoff).
- Notes:
  - If `createBackoff` previously handled logging/metrics via its own `onError`, this change stops passing it from `InvalidateSync` (by design). Audit other call sites if any depended on that side effect.
- Depends on:
  - the backoff retry loop re-invoking the provided callback on thrown errors (standard pattern).

### Rewrite Commit Message

- Keep as-is: `fix(utils): track failuresCount for custom backoff`

---

## 220 2026-01-23 dc8016f01c56 `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=dc8016f01c56 patchFile=/tmp/leeroy-wip-pass2/220.patch patchSha256=f8a27472675baae86971bc8cc9e8b89fd68ada04eda4f03689ca3cf09ec1b283 patchBytes=4592 patchLines=114 notesReviewed=yes reviewedAt=2026-01-25T09:35:12+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:39:11+0100 manualReviewedForPrPlan=yes notes=Server-light migration reliability: validate HAPPY_SERVER_LIGHT_DATA_DIR (trim/require) + centralize deploy plan args; includes unit tests.

- Subject: fix(server): validate HAPPY_SERVER_LIGHT_DATA_DIR for light migrations
- Reasons: reliability (fail fast with a clear error rather than crashing mid-migration)

### Manual Review Notes

- Refactors the light migration deploy script to validate + centralize its “plan”:
  - `server/scripts/migrate.light.deployPlan.ts`:
    - `requireLightDataDir(env)` throws if `HAPPY_SERVER_LIGHT_DATA_DIR` is missing/blank, and returns a trimmed value.
    - `buildLightMigrateDeployPlan(env)` returns a `dataDir` plus the exact `yarn` argument vectors used for schema generation and `prisma migrate deploy`.
  - `server/scripts/migrate.light.deploy.ts`:
    - uses `buildLightMigrateDeployPlan(env)` and ensures `mkdir(plan.dataDir, { recursive: true })`.
    - invokes `run('yarn', plan.schemaGenerateArgs, env)` and `run('yarn', plan.prismaDeployArgs, env)`.
- Adds unit tests:
  - `server/scripts/migrate.light.deployPlan.spec.ts` covers missing/empty trimming behavior and validates the computed args.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good (single script hardening + test coverage).
- Depends on:
  - `applyLightDefaultEnv(env)` setting the env consistently (this commit still validates in case it does not).
  - the schema location remaining `prisma/sqlite/schema.prisma` (plan encodes it explicitly).

### Rewrite Commit Message

- Keep as-is: `fix(server): validate HAPPY_SERVER_LIGHT_DATA_DIR for light migrations`

---

## 221 2026-01-23 b0085827322c `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=b0085827322c patchFile=/tmp/leeroy-wip-pass2/221.patch patchSha256=78599b9524d5aee01e8aca14a7a44ee0252e5c7247327d25eaf65e773d0aa721 patchBytes=3706 patchLines=85 notesReviewed=yes reviewedAt=2026-01-25T09:35:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR23 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=Serve-UI reliability: return 404 (not 500) when UI index missing in error handlers; includes regression test.

- Subject: fix(server): handle missing UI index in error handlers
- Reasons: reliability (serve “not found” instead of 500 when UI bundle is absent/misconfigured)

### Manual Review Notes

- Makes SPA index serving fail closed as 404 when the UI bundle is missing:
  - `server/sources/app/api/utils/enableErrorHandlers.ts`:
    - if `uiDirRaw` is missing: adds `cache-control: no-cache` and returns 404 JSON `{ error: 'Not found' }`.
    - wraps `stat(index.html)` + `readFile(index.html)` in a `try/catch`:
      - if `ENOENT` or `ENOTDIR`: returns the same 404 instead of throwing (which would have turned into a 500).
      - otherwise rethrows unknown errors.
- Adds regression test:
  - `server/sources/app/api/utils/enableErrorHandlers.spec.ts`:
    - sets `HAPPY_SERVER_UI_DIR` to a temp dir with no `index.html`
    - injects `GET /` and asserts `404` (not `500`).

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good.
- Depends on:
  - `enableErrorHandlers` being responsible for SPA fallback routing.
  - clients handling `{ error: 'Not found' }` for missing UI bundles (appropriate for local/dev).

### Rewrite Commit Message

- Keep as-is: `fix(server): handle missing UI index in error handlers`

---

## 222 2026-01-23 abf288056636 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=abf288056636 patchFile=/tmp/leeroy-wip-pass2/222.patch patchSha256=ff8a919aac319612e39ee8b56f391cc25d301d9c85716a63380bae36ec5e056b patchBytes=1914 patchLines=51 notesReviewed=yes reviewedAt=2026-01-25T09:36:02+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=Queue state parsing hardening: reject invalid messageQueueV1 inFlight shapes (null out overall) + test.

- Subject: fix(cli): reject invalid messageQueueV1 inFlight
- Reasons: correctness (avoid accepting partially-shaped persisted state)

### Manual Review Notes

- Tightens parsing validation for the `messageQueueV1` persisted structure:
  - `cli/src/api/messageQueueV1.ts`:
    - makes `inFlight` parsing explicit:
      - `undefined` → `undefined`
      - `null` → `null`
      - otherwise requires `parseInFlight(inFlightRaw)` to succeed; if it fails → `null` overall.
    - this avoids accepting arbitrary objects for `inFlight`.
  - `cli/src/api/messageQueueV1.test.ts` adds a case asserting an invalid `inFlight` object is rejected.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good.
- Depends on: downstream code treating `null` return from `parseMessageQueueV1` as “reset to defaults / ignore persisted state”.

### Rewrite Commit Message

- Keep as-is: `fix(cli): reject invalid messageQueueV1 inFlight`

---

## 223 2026-01-23 73ce32d242a0 `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=73ce32d242a0 patchFile=/tmp/leeroy-wip-pass2/223.patch patchSha256=7276f6144dbcfe2de6ed375f24e62e13cde97b36a45816b1690d7cc718592eaf patchBytes=2892 patchLines=44 notesReviewed=yes reviewedAt=2026-01-25T09:36:24+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR06 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=Settings/experiments i18n polish: translate Spanish experiment subtitles (pure translations).

- Subject: fix(i18n): translate Spanish experiment subtitles
- Reasons: UX polish (remove leftover English strings in Spanish locale)

### Manual Review Notes

- Updates `expo-app/sources/text/translations/es.ts` so the “experimental options” subtitles are properly translated (previously several were English placeholders).
- No functional code changes.

### Verdict

- Keep/squash/split/drop/reorder: keep (or squash into a broader i18n sweep commit during history rewrite).
- Coherence: good.
- Depends on: nothing (pure translations).

### Rewrite Commit Message

- Keep as-is: `fix(i18n): translate Spanish experiment subtitles`

---

## 224 2026-01-23 98bc00096e86 `new-session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=98bc00096e86 patchFile=/tmp/leeroy-wip-pass2/224.patch patchSha256=6fbcd1147947b83c51c4d7ae6a43121991b1e9abdaa57e657fbfe93e678879b4 patchBytes=26304 patchLines=388 notesReviewed=yes reviewedAt=2026-01-25T09:37:18+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=Mixed new-session changes: localize Codex resume banner (belongs to resume UX) + refactors that depend on 225/226; needsSplit=yes to move WizardSectionHeaderRow refactor to PR26 and permissionModeOptions refactor to PR10 during rewrite.

- Subject: fix(i18n): localize Codex resume banner
- Reasons: UX/i18n, but the commit also includes unrelated refactors (mixed concerns)

### Manual Review Notes

- Primary intent: localize the “Codex resume” banner strings in the new-session wizard:
  - `expo-app/sources/app/(app)/new/NewSessionWizard.tsx`:
    - replaces hardcoded strings like `Codex resume`, `Update available`, `System codex: ...`, `Registry check failed: ...`, and button labels with i18n keys under `newSession.codexResumeBanner.*`.
    - uses `t('status.unknown')` and `t('newSession.codexResumeBanner.notInstalled')` for fallback values.
  - Adds `newSession.codexResumeBanner` translations across many locales (`ca/en/es/it/ja/pl/pt/ru/zh-Hans`), including function-based strings for version rendering.
- Mixed concerns in the same commit:
  - Refactors the “Machine Selection” section header to use `WizardSectionHeaderRow`.
  - Refactors the permission mode option list to use `getPermissionModeOptionsForAgentType(agentType)` (instead of an inline array with hardcoded English descriptions).
- There is also visible indentation churn (tabs introduced in the diff) around the banner block; worth normalizing during rewrite to reduce noise.

### Verdict

- Keep/squash/split/drop/reorder: split (recommended) or squash with its dependencies.
- Coherence: mixed (i18n + UI refactor + option-list refactor).
- Ordering / dependency concerns:
  - This commit imports and uses `WizardSectionHeaderRow`, but the component is added in 225 (later in this branch history).
  - This commit imports and uses `permissionModeOptions`, but that helper module is introduced in 226 (later in this branch history).
  - During history rewrite, either reorder the series (225/226 before 224) or squash them into one coherent “new session wizard refactor + i18n” topic group.

### Rewrite Commit Message

- If split:
  - `fix(i18n): localize Codex resume banner`
  - `refactor(new-session): use WizardSectionHeaderRow for wizard sections`
  - `refactor(new-session): centralize permission mode options`
- If squashed: keep the “fix(i18n)” message but mention the refactor in the body.

---

## 225 2026-01-23 4fabec9b69b3 `new-session`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=4fabec9b69b3 patchFile=/tmp/leeroy-wip-pass2/225.patch patchSha256=88b9efe8a607d65f0a3cc93c36ee4b75264579b4bf348793e74a0c220cb853ae patchBytes=2067 patchLines=58 notesReviewed=yes reviewedAt=2026-01-25T09:37:37+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=New-session wizard UI primitive: add WizardSectionHeaderRow component; reorder before 224 in rewritten history.

- Subject: feat(new-session): add WizardSectionHeaderRow
- Reasons: UI consistency (reduce repeated header-row markup in wizard sections)

### Manual Review Notes

- Adds a small presentational component for a wizard section header row:
  - `expo-app/sources/components/newSession/WizardSectionHeaderRow.tsx`:
    - renders an `Ionicons` icon + title text
    - optionally renders an action icon button with accessible label.
- Note: the component relies on `rowStyle` to provide the layout (e.g. `flexDirection: 'row'` and any spacing). If the prior layout needed “space-between” behavior (icon/title on left, action on right), the caller’s `rowStyle` must encode that, since the component does not separate “left cluster” vs “right action” into distinct sub-views.

### Verdict

- Keep/squash/split/drop/reorder: keep, but reorder to land before 224 (since 224 uses it).
- Coherence: good (single new component).
- Depends on: `Ionicons` icon names remaining stable across platforms.

### Rewrite Commit Message

- Keep as-is: `feat(new-session): add WizardSectionHeaderRow`

---

## 226 2026-01-23 c461ed1cb4ef `permission`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c461ed1cb4ef patchFile=/tmp/leeroy-wip-pass2/226.patch patchSha256=a8d7b0277e06ee4c0239fd94aa9b683d8b5a30744a87f55e9a1e4da9f86bc24b patchBytes=4784 patchLines=88 notesReviewed=yes reviewedAt=2026-01-25T09:38:18+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR10 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=Permission UX helpers: centralize permission mode option lists/labels per agent type; note follow-up to i18n-ize description strings if they are user-visible.

- Subject: feat(permission): add permission mode option helpers
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Adds a UI-facing helper module that centralizes “permission mode” option lists and labels per agent flavor:
  - `expo-app/sources/sync/permissionModeOptions.ts`:
    - `getPermissionModeTitleForAgentType(...)` and `getPermissionModeLabelForAgentType(...)` select translated labels for Claude vs Codex vs Gemini.
    - `getPermissionModesForAgentType(...)` returns the allowed modes per agent type (`CLAUDE_PERMISSION_MODES` vs `CODEX_LIKE_PERMISSION_MODES`).
    - `getPermissionModeOptionsForAgentType(...)` returns option objects including `icon` names and a short description per option.
    - `normalizePermissionModeForAgentType(...)` delegates to `normalizePermissionModeForAgentFlavor(...)` to clamp unsupported modes.

### Notes / Risks / Follow-ups (for history rewrite planning)

- Option `description` strings are currently hardcoded English (not translated). If these surface in UI, they probably should become `t(...)` keys to keep i18n consistent.

### Verdict

- Keep/squash/split/drop/reorder: keep; belongs alongside other permission-mode UX work.
- Coherence: good (single responsibility: permission-mode labeling + option lists).
- Depends on:
  - `sync/permissionTypes` primitives (`PermissionMode`, `normalizePermissionModeForAgentFlavor`, mode lists).
  - `AgentType` set (`claude|codex|gemini`) matching the mapping logic.

### Rewrite Commit Message

- Keep as-is: `feat(permission): add permission mode option helpers`
- Optional scope tweak (if you want it closer to file location): `feat(sync): add permission mode option helpers`

---

## 227 2026-01-23 23b595edd0fe `sync`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=23b595edd0fe patchFile=/tmp/leeroy-wip-pass2/227.patch patchSha256=fae3d600fdbfd7dc688b5614ddbef2ddc14227771a98f46009978164cd37cceb patchBytes=973 patchLines=27 notesReviewed=yes reviewedAt=2026-01-25T09:38:41+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR29 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=Sessions list display cleanup: simplify home-relative path formatting (~ prefix) without behavior change.

- Subject: refactor(sync): simplify path formatting
- Reasons: small refactor (reduce branching; keep output identical)

### Manual Review Notes

- Simplifies home-relative path formatting for the session list UI:
  - `expo-app/sources/sync/sessionListViewData.ts` `formatPathRelativeToHome(...)`:
    - previously had 3 branches for `/`, empty string, and default.
    - now returns `relativePath ? \`~${relativePath}\` : '~'`.
- Behavior appears equivalent given `relativePath` is computed from `path.slice(normalizedHome.length)`:
  - when `relativePath` begins with `/`, both old and new yield `~/<...>` (because `~${relativePath}` produces `~/<...>`).
  - when `relativePath` is `''`, both yield `~`.

### Verdict

- Keep/squash/split/drop/reorder: keep (or squash into adjacent “session list UI” refactors during rewrite).
- Coherence: good (single tiny cleanup).
- Depends on: `relativePath` being either `''` or already slash-prefixed when non-empty (as implied by how it’s derived).

### Rewrite Commit Message

- Keep as-is: `refactor(sync): simplify path formatting`

---

## 228 2026-01-23 523989b4de8e `server-light`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=523989b4de8e patchFile=/tmp/leeroy-wip-pass2/228.patch patchSha256=b630328267969617b97d78d03deb46f520f2035bd6e33651487c756c1b9108c8 patchBytes=1425 patchLines=35 notesReviewed=yes reviewedAt=2026-01-25T09:39:02+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=Server-light safety: avoid handy master secret file race via atomic write (wx) + EEXIST read-back; keep adjacent to light-flavor init work.

- Subject: fix(server-light): avoid master secret race
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Fixes a real-world race when starting multiple “light” server processes concurrently:
  - `server/sources/flavors/light/env.ts` `ensureHandyMasterSecret(...)` now uses an atomic create:
    - writes the secret file with `flag: 'wx'` (fail if already exists)
    - on `EEXIST`, reads the existing file instead of overwriting
    - errors if the file exists but is empty (fail-closed).
- This pairs naturally with the light-flavor introduction in 176 (which first created/persisted `handy-master-secret.txt`).

### Verdict

- Keep/squash/split/drop/reorder: keep; should be adjacent to 176 in the rewritten history under the “server-light init reliability” topic.
- Coherence: good.
- Depends on: 176 (the initial introduction of `ensureHandyMasterSecret` and light flavor defaults).

### Rewrite Commit Message

- Keep as-is: `fix(server-light): avoid master secret race`

---

## 229 2026-01-23 d89a9427e765 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d89a9427e765 patchFile=/tmp/leeroy-wip-pass2/229.patch patchSha256=09a7cbdfe0ec774246a505a6adfa9ef7b0634bdff308a85682861263c46f73a2 patchBytes=3829 patchLines=91 notesReviewed=yes reviewedAt=2026-01-25T09:39:36+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR17 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=CLI socket reliability: clean up metadata waiters on disconnect (resolve false; remove listeners) + test.

- Subject: fix(cli): clean up metadata waiters on disconnect
- Reasons: reliability (avoid hanging waiters and event-listener leaks on socket disconnect)

### Manual Review Notes

- Hardens `ApiSessionClient.waitForMetadataUpdate()` so it does not hang forever if the socket disconnects while waiting:
  - `cli/src/api/apiSession.ts`:
    - registers a `socket.on('disconnect', ...)` handler that triggers cleanup and resolves `false`.
    - adds an idempotent cleanup guard (`cleanedUp`) so multiple signals (`metadata-updated`, abort, disconnect) cannot double-clean or double-resolve.
    - ensures the disconnect handler is removed during cleanup.
- Adds test coverage:
  - `cli/src/api/apiSession.test.ts`:
    - new test asserts `waitForMetadataUpdate()` resolves `false` when `disconnect` fires.
- Note: this commit also includes indentation/whitespace churn in the test file; worth normalizing in history rewrite to keep diffs clean.

### Verdict

- Keep/squash/split/drop/reorder: keep.
- Coherence: good (single behavior: clean up/resolve waiters on disconnect).
- Depends on: the socket implementation emitting a `disconnect` event compatible with this handler (e.g. socket.io client).

### Rewrite Commit Message

- Keep as-is: `fix(cli): clean up metadata waiters on disconnect`

---

## 230 2026-01-23 fedb8a66160a `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=fedb8a66160a patchFile=/tmp/leeroy-wip-pass2/230.patch patchSha256=b051d2500827992e30c437c46c726dfd4d7fa5e3147cbde7148a85111e07d630 patchBytes=1639 patchLines=36 notesReviewed=yes reviewedAt=2026-01-25T09:40:11+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR15 plannedAt=2026-01-25T14:42:48+0100 manualReviewedForPrPlan=yes notes=Preview-env contract test: accept lowercase env keys (adjacent to 231 impl; keep together in preview-env PR).

- Subject: test(cli): accept lowercase preview-env keys
- Reasons: define intended behavior before/alongside the implementation change

### Manual Review Notes

- Adds a test asserting the `preview-env` RPC accepts lowercase env var keys:
  - `cli/src/modules/common/registerCommonHandlers.previewEnv.test.ts`:
    - sets `process.env.npm_config_registry`
    - requests `keys: ['npm_config_registry']`
    - asserts policy and that the value is returned unredacted under `policy: 'none'`.

### Verdict

- Keep/squash/split/drop/reorder: keep, but keep adjacent to 231 (the implementation).
- Coherence: good (single test).
- Depends on: 231 (without it, the test should fail).

### Rewrite Commit Message

- If kept separate: keep as-is: `test(cli): accept lowercase preview-env keys`
- If squashed into 231: drop this commit and keep 231’s message.

---

## 231 2026-01-23 fc0ce89b13b7 `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=fc0ce89b13b7 patchFile=/tmp/leeroy-wip-pass2/231.patch patchSha256=aa8a6b5f3cbe7c42d787a67800c244bcd442cbbbe2df8100b11efb0d5140f40d patchBytes=3562 patchLines=71 notesReviewed=yes reviewedAt=2026-01-25T09:40:41+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR15 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=preview-env safety: accept lowercase keys while blocking prototype pollution keys; keep adjacent to 230 test in preview-env PR.

- Subject: fix(cli): allow lowercase preview-env keys
- Reasons: correctness + security hardening (input validation + prototype-pollution defense)

### Manual Review Notes

- Expands validation to accept lowercase env keys while strengthening safety:
  - `cli/src/modules/common/previewEnv/registerPreviewEnvHandler.ts`:
    - relaxes env key regex from `^[A-Z_][A-Z0-9_]*$` to `^[A-Za-z_][A-Za-z0-9_]*$`.
    - explicitly forbids dangerous keys: `__proto__`, `constructor`, `prototype` (prevents prototype pollution attacks when reflecting env keys back to callers).
    - uses `Object.create(null)` for `extraEnv` so it has no prototype.
    - reuses a single `isValidEnvVarKey` predicate for both `keys` and `sensitiveKeys` validation.
- Adds a safety regression test:
  - `cli/src/modules/common/registerCommonHandlers.previewEnv.test.ts`:
    - asserts requesting `keys: ['__proto__']` returns an error indicating invalid key.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a nice “accept lowercase” change with an important security follow-through.
- Coherence: mostly good (all changes are in the same RPC surface), though the commit message under-sells the prototype-pollution defense.
- Depends on:
  - RPC layer returning `{ error: string }` for handler errors (as the test asserts).
  - Consumers not relying on old strict-uppercase enforcement.

### Rewrite Commit Message

- Suggested improvement: `fix(cli): accept lowercase preview-env keys and block prototype pollution`
- Current is acceptable: `fix(cli): allow lowercase preview-env keys`

---

## 232 2026-01-23 ad95c5b01c25 `hooks`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=ad95c5b01c25 patchFile=/tmp/leeroy-wip-pass2/232.patch patchSha256=733a884857434803cf24e58f4eed1b7a444c07efcbf092a2e511f26c17c0b000 patchBytes=2158 patchLines=69 notesReviewed=yes reviewedAt=2026-01-25T09:41:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Capabilities hook contract: test stable useCLIDetection timestamp when loaded snapshot lacks checkedAt; squash into 234 series.

- Subject: test(happy): keep useCLIDetection timestamp stable
- Reasons: define stable timestamp semantics for “loaded snapshots missing checkedAt”

### Manual Review Notes

- Adds a new fake-timer test covering an edge case where the capabilities snapshot is “loaded” but has no `checkedAt` timestamps:
  - `expo-app/sources/hooks/useCLIDetection.hook.test.ts`:
    - uses `vi.useFakeTimers()` + `vi.setSystemTime(...)`
    - mocks `useMachineCapabilitiesCache` to return a `loaded` snapshot with `results: {}` (no `checkedAt`)
    - asserts `useCLIDetection(...).timestamp` remains stable across re-renders even as “now” changes.

### Verdict

- Keep/squash/split/drop/reorder: keep as part of the 232–235 “useCLIDetection timestamp stabilization” series; likely squash into 234 during rewrite.
- Coherence: good (single behavioral expectation expressed in tests).
- Depends on: 234/235 (implementation changes) to make the test pass reliably.

### Rewrite Commit Message

- If squashed: drop this commit and keep 234’s message.
- If kept separate: keep as-is: `test(happy): keep useCLIDetection timestamp stable`

---

## 233 2026-01-23 c251edf3c307 `hooks`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c251edf3c307 patchFile=/tmp/leeroy-wip-pass2/233.patch patchSha256=8ac0cb76b673f42e5bd761b9f12bc4bb4beb372adfe83f8ece3aabfcc04fdece patchBytes=3645 patchLines=113 notesReviewed=yes reviewedAt=2026-01-25T09:41:39+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Test hygiene follow-up for 232 (act()+timer cleanup); squash into 232/234.

- Subject: test(happy): fix useCLIDetection timestamp test
- Reasons: test hygiene (proper `act()` usage; timer cleanup)

### Manual Review Notes

- Makes the new timestamp test safe and deterministic:
  - `expo-app/sources/hooks/useCLIDetection.hook.test.ts`:
    - wraps `vi.useFakeTimers()` usage in `try/finally` and always calls `vi.useRealTimers()`
    - wraps initial `renderer.create(...)` in `act(...)` (react-test-renderer best practice)
    - avoids leaking fake timers into subsequent tests.

### Verdict

- Keep/squash/split/drop/reorder: squash into 232 (or into the final “stabilize timestamp” commit) during history rewrite; it’s a direct test follow-up.
- Coherence: good.
- Depends on: 232 (it only exists to make that test robust).

### Rewrite Commit Message

- If squashed: drop this commit and keep the “main” message (232/234).

---

## 234 2026-01-23 34fa4091d3cc `hooks`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=34fa4091d3cc patchFile=/tmp/leeroy-wip-pass2/234.patch patchSha256=f522777907fc44d6297900a370d381db8643cf2e6c802340711691e6ed472547 patchBytes=3538 patchLines=70 notesReviewed=yes reviewedAt=2026-01-25T09:43:58+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Capabilities hook reliability: stabilize useCLIDetection timestamp fallback when checkedAt missing; squash 232/233/235 into this in rewrite.

- Subject: fix(happy): stabilize useCLIDetection timestamp
- Reasons: reliability (avoid timestamp flapping across renders when `checkedAt` is missing)

### Manual Review Notes

- Fixes timestamp instability by introducing a stable fallback timestamp for “loaded but missing checkedAt” snapshots:
  - `expo-app/sources/hooks/useCLIDetection.ts`:
    - adds `fallbackDetectAtRef` to capture a single “loaded-at” time when:
      - `cached.status === 'loaded'`
      - `latestCheckedAt === 0`
      - and there has never been a successful `checkedAt` (`lastSuccessfulDetectAtRef.current === 0`)
    - resets the fallback when a real `checkedAt` arrives.
    - changes returned `timestamp` to:
      - `lastSuccessfulDetectAtRef.current || latestCheckedAt || fallbackDetectAtRef.current || 0`
      - notably, this removes the “always use `now`” fallback that caused flapping.
- Also refactors the capabilities request construction (temporarily) by importing shared request constants.
  - That part is immediately revised in 235.

### Verdict

- Keep/squash/split/drop/reorder: keep, but squash with 232/233/235 during rewrite to avoid the request-constant back-and-forth.
- Coherence: mostly good; the request-constant change is unrelated to the timestamp fix and should not survive as a separate commit.
- Depends on:
  - callers treating `timestamp: 0` as “unknown” when no snapshot exists (this preserves that behavior).
  - the capabilities protocol still producing `checkedAt` values in normal cases.

### Rewrite Commit Message

- Suggested final message (after squashing 232–235): `fix(hooks): stabilize useCLIDetection timestamp`

---

## 235 2026-01-23 31340e53294c `hooks`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=31340e53294c patchFile=/tmp/leeroy-wip-pass2/235.patch patchSha256=20441fb5c84ebd173a59154272d1c0297b021b7d3264fd571b05dabcae64bbc3 patchBytes=2000 patchLines=41 notesReviewed=yes reviewedAt=2026-01-25T09:44:24+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Back-and-forth cleanup: inline CLI detection request (undo part of 234); squash into 234.

- Subject: fix(happy): inline CLI detection request
- Reasons: simplify dependency surface (avoid shared-constant import for this hook)

### Manual Review Notes

- Reverts part of 234 by inlining the request object back into the hook:
  - `expo-app/sources/hooks/useCLIDetection.ts`:
    - removes import of `CAPABILITIES_REQUEST_NEW_SESSION*`
    - restores a `useMemo` that constructs:
      - `{ checklistId: 'new-session' }` when `includeLoginStatus=false`
      - and the override object when `includeLoginStatus=true`.
- This looks like a “keep this hook self-contained” follow-up and should be squashed with 234 for a clean history.

### Verdict

- Keep/squash/split/drop/reorder: squash into 234 during rewrite.
- Coherence: good, but it’s a back-and-forth correction rather than a standalone change.
- Depends on: 234 (it is effectively undoing one part of that commit).

### Rewrite Commit Message

- If squashed: drop this commit and keep the “stabilize timestamp” message (234).

---

## 236 2026-01-23 054250f408e2 `i18n`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=054250f408e2 patchFile=/tmp/leeroy-wip-pass2/236.patch patchSha256=024605aca8ca43a541838357ee03946ca858eaf9617fe72d96ecb3ad2d5ee4b5 patchBytes=21535 patchLines=394 notesReviewed=yes reviewedAt=2026-01-25T09:45:05+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Resume UX i18n: localize Codex resume install/update modals (machine + new-session) + deps.* status strings; unify key scopes with 224 during rewrite.

- Subject: fix(i18n): localize codex resume install modals
- Reasons: UX/i18n + more robust user-facing install errors

### Manual Review Notes

- Localizes Codex resume install/update flows in both “Machine details” and “New session” screens:
  - `expo-app/sources/app/(app)/machine/[id].tsx`:
    - replaces hardcoded English strings for the install/update/reinstall action and modal titles/descriptions with translated keys.
    - standardizes error/success `Modal.alert` headers to `t('common.error')` / `t('common.success')`.
    - adds reusable dependency-install status strings (`deps.*`) such as:
      - `installNotSupported`, `installFailed`, `installed`, `installLog`.
  - `expo-app/sources/app/(app)/new/index.tsx` similarly localizes the “install Codex resume?” modal (diff continues in file).
  - Adds translation blocks across many locales for:
    - `deps.*`
    - `newSession.codexResumeInstallModal.*`
- Naming consistency note for rewrite planning:
  - This commit uses `common.codexResumeBanner.*` / `common.codexResumeInstallModal.*` keys in some places, while 224 introduced `newSession.codexResumeBanner.*`. That split likely causes duplicated strings and should be unified during the i18n cleanup pass.

### Verdict

- Keep/squash/split/drop/reorder: keep; it’s a coherent i18n+UX polish change.
- Coherence: good (all changes are in the “Codex resume install/update UX” surface).
- Depends on:
  - the referenced translation keys existing in all supported locales (this commit adds them).
  - callers treating `deps.installNotSupported` as a safe message for CLI-side “not-supported”.

### Rewrite Commit Message

- Keep as-is: `fix(i18n): localize codex resume install modals`

---

## 237 2026-01-23 241f0ae24b1d `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=241f0ae24b1d patchFile=/tmp/leeroy-wip-pass2/237.patch patchSha256=eade827a038e651cf40824bd8746e951d432da56710508fbe64c14bfc9345421 patchBytes=1618 patchLines=40 notesReviewed=yes reviewedAt=2026-01-25T09:45:37+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Terminal attachment security series: add regression test preventing legacy sessionId path traversal; keep adjacent to 212 and 238.

- Subject: test(cli): prevent legacy sessionId path traversal
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Adds a regression test proving that the legacy raw-filename fallback must not be used for traversal-ish session ids:
  - Creates a “poisoned” legacy file path under `terminal/sessions/${sessionId}.json` where `sessionId = '../../pwned'`.
  - Asserts `readTerminalAttachmentInfo(...)` returns `null` rather than reading outside the intended sessions dir.
- This test highlights the subtle remaining risk introduced by the legacy fallback in 212.

### Verdict

- Keep/squash/split/drop/reorder: keep; in a rewritten history, place this immediately before 238 (RED → GREEN) and keep it adjacent to 212.
- Depends on: 212 (legacy fallback exists only after 212; this test is targeted at that behavior).

### Rewrite Commit Message

- Keep as-is: `test(cli): prevent legacy sessionId path traversal`

---

## 238 2026-01-23 a1e4a6dd3fbd `cli`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=a1e4a6dd3fbd patchFile=/tmp/leeroy-wip-pass2/238.patch patchSha256=17d4644af933f143aadcf2c747e71bef9b1c705e162c51515bf499013e2aa7fc patchBytes=1189 patchLines=23 notesReviewed=yes reviewedAt=2026-01-25T09:45:58+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR18 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Terminal attachment security fix: block legacy fallback when sessionId contains path separators (/, \\); completes 212+237 series.

- Subject: fix(cli): block legacy sessionId path traversal
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Closes the remaining traversal hole by gating the legacy fallback:
  - `cli/src/terminal/terminalAttachmentInfo.ts`:
    - before attempting legacy path, checks `params.sessionId.includes('/') || params.sessionId.includes('\\\\')` and refuses the fallback when present.
    - This ensures legacy fallback only applies to filename-safe session ids (e.g. `tmux:legacy`) and prevents traversal outside the sessions directory.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is the actual security fix that pairs with 237’s regression test and 212’s encoding change.
- Depends on: 212 + 237 (212 introduced the fallback, 237 defines the required behavior).

### Rewrite Commit Message

- Keep as-is: `fix(cli): block legacy sessionId path traversal`

---

## 239 2026-01-23 e2618e04a26b `a11y`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=e2618e04a26b patchFile=/tmp/leeroy-wip-pass2/239.patch patchSha256=ae2ca5dc593b6b7515371e38b3f2077b2bff2b9d0e29f8e00b096f7bfb9977b5 patchBytes=2596 patchLines=36 notesReviewed=yes reviewedAt=2026-01-25T09:46:21+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Resume banner a11y: add accessibilityRole/Label to Codex resume banner actions (install/update/reinstall/check updates); depends on 224 banner UI.

- Subject: fix(a11y): add labels to codex resume actions
- Reasons: accessibility (screen readers need roles/labels for icon-only buttons)

### Manual Review Notes

- Adds accessibility metadata for the Codex resume banner controls:
  - `expo-app/sources/app/(app)/new/NewSessionWizard.tsx`:
    - “check updates” `Pressable` now has `accessibilityRole="button"` and `accessibilityLabel={t('common.refresh')}`.
    - install/update/reinstall `Pressable` now has `accessibilityRole="button"` and a label matching the action:
      - `t('newSession.codexResumeBanner.install' | 'update' | 'reinstall')`.

### Verdict

- Keep/squash/split/drop/reorder: keep; high-value a11y improvement.
- Coherence: good.
- Depends on: 224 (the banner block must exist; it also defines the translation keys used for the label).

### Rewrite Commit Message

- Keep as-is: `fix(a11y): add labels to codex resume actions`

---

## 240 2026-01-23 d0f9ad5e8310 `claude`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=d0f9ad5e8310 patchFile=/tmp/leeroy-wip-pass2/240.patch patchSha256=5b83e3eac76bea990744a4af4d0ede76e491000db3f3bc44743195f731ecf02d patchBytes=1540 patchLines=37 notesReviewed=yes reviewedAt=2026-01-25T09:46:58+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR20 plannedAt=2026-01-25T14:47:03+0100 manualReviewedForPrPlan=yes notes=Claude scanner test cleanup: remove stale red-test scaffolding for transcriptPath now that feature exists; squash into transcriptPath feature commit during rewrite.

- Subject: test(claude): remove stale transcriptPath red-test scaffolding
- Reasons: test hygiene (remove intentional-red scaffolding once feature is implemented)

### Manual Review Notes

- Cleans up prior “RED test scaffolding” related to `transcriptPath` handling:
  - `cli/src/claude/utils/sessionScanner.test.ts`:
    - removes the comment and the `as any` casts that intentionally passed a “future API shape”.
    - adds a small guard (`if (!scanner) throw ...`) before calling `scanner.onNewSession(...)`.
    - updates `createSessionScanner(...)` call site to pass typed args without `as any`.
- This is a “follow-up cleanup” commit and should likely be squashed into the original feature commit that added `transcriptPath` support.

### Verdict

- Keep/squash/split/drop/reorder: squash into the `transcriptPath` feature commit during rewrite.
- Coherence: good (pure test cleanup).
- Depends on: the implementation now supporting the typed `transcriptPath` surface (so the casts are no longer needed).

### Rewrite Commit Message

- If squashed: drop this commit.
- If kept: keep as-is: `test(claude): remove stale transcriptPath red-test scaffolding`

---

## 241 2026-01-23 1464402758bf `codex`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=1464402758bf patchFile=/tmp/leeroy-wip-pass2/241.patch patchSha256=8d92eb3bed71a5bc1aea5116246841d7a5a5880ef2af17c294577b10edef88e9 patchBytes=3078 patchLines=74 notesReviewed=yes reviewedAt=2026-01-25T09:47:16+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR21 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=Codex MCP correctness: preserve falsy Ok/Err tool results via hasOwnProperty checks + unit tests.

- Subject: fix(codex): preserve falsy MCP tool results
- Reasons: safety/security-sensitive area

### Manual Review Notes

- Fixes a subtle but important correctness bug when handling MCP tool results:
  - Previously, `runCodex.ts` derived output via `result?.Ok || result?.Err || result`, which incorrectly treats legitimate falsy values (`false`, `0`, `''`, `null`) as “absent” and falls through.
  - Adds `extractMcpToolCallResultOutput(result)` that:
    - checks `hasOwnProperty('Ok')` / `hasOwnProperty('Err')` (so falsy values are preserved)
    - returns raw `result` if it’s not an Ok/Err wrapper.
  - Adds unit tests in `cli/src/codex/__tests__/extractMcpToolCallResultOutput.test.ts` covering falsy Ok/Err outputs and non-wrapper values.

### Verdict

- Keep/squash/split/drop/reorder: keep; this is a correctness fix for tool-call result propagation and should be near other MCP/tool-call handling commits.
- Coherence: good.
- Depends on: Codex MCP protocol using `{ Ok: ... } | { Err: ... }` wrappers (or similar).

### Rewrite Commit Message

- Keep as-is: `fix(codex): preserve falsy MCP tool results`

---

## 242 2026-01-23 57f84f0691c6 `server-light`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=57f84f0691c6 patchFile=/tmp/leeroy-wip-pass2/242.patch patchSha256=768b4f121bd2c2edef87a3e8ea83d26107bf2e2505dd887a6ddcefc9ca908a54 patchBytes=4848 patchLines=107 notesReviewed=yes reviewedAt=2026-01-25T09:47:54+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=Server-light security: strict public file path normalization + segment-wise encodeURIComponent URL building (encode #/?); expanded tests.

- Subject: fix(server-light): harden public file path encoding
- Reasons: security + correctness (path traversal defenses; correct URL encoding of `#`/`?`)

### Manual Review Notes

- Strengthens public file path normalization and URL building in light server flavor:
  - `server/sources/flavors/light/files.ts`:
    - `normalizePublicPath` now fails closed for:
      - null bytes (`\0`)
      - absolute paths (`/x`, `\\x`)
      - traversal (`..` segments in raw parts, including Windows-style `..\\x`)
      - drive-letter-ish paths (`C:\\x`, `C:/x`) via `:` rejection
      - empty/degenerate paths
    - switches from `path.normalize` to `path.posix.normalize` on a forward-slashed string, reducing platform-dependent normalization behavior.
    - `getLightPublicUrl` now encodes each path segment with `encodeURIComponent` instead of `encodeURI` so `#` and `?` cannot be interpreted as fragment/query by browsers/CDNs.
- Updates/expands tests:
  - `server/sources/flavors/light/files.spec.ts`:
    - verifies traversal/absolute/drive-letter paths are rejected
    - verifies normalization behavior for `foo//bar`, `foo/./bar`, and backslash input
    - adds `getLightPublicUrl` test ensuring proper encoding of `bar baz#qux?zap`.

### Verdict

- Keep/squash/split/drop/reorder: keep (security-sensitive).
- Coherence: good (single surface: public file URL/path safety).
- Depends on:
  - callers expecting strict rejection (previous behavior silently stripped leading `/`; this is more correct but could be breaking for misuses).

### Rewrite Commit Message

- Keep as-is: `fix(server-light): harden public file path encoding`

---

## 243 2026-01-23 0b7db6945325 `storage`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=0b7db6945325 patchFile=/tmp/leeroy-wip-pass2/243.patch patchSha256=24b9f7ab9cddb708484b41b3db7d20ee4f27722b9d34676a819ea47c4c5d2c48 patchBytes=3229 patchLines=92 notesReviewed=yes reviewedAt=2026-01-25T09:48:35+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR24 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=Storage config safety: validate S3_PORT range and fail-fast when bucket missing; adds focused tests.

- Subject: fix(storage): validate S3_PORT and bucket existence
- Reasons: reliability (fail fast for misconfiguration; prevent runtime surprises)

### Manual Review Notes

- Tightens S3 backend initialization:
  - `server/sources/storage/files.ts`:
    - parses `S3_PORT` only if present and validates it is an integer in `1..65535`; otherwise throws a clear error.
    - validates bucket existence during backend init:
      - previously called `bucketExists` but ignored the boolean return value
      - now throws when the bucket does not exist.
- Adds focused tests:
  - `server/sources/storage/files.spec.ts`:
    - asserts invalid `S3_PORT` throws
    - mocks `minio` client to return `bucketExists=false` and asserts `loadFiles()` rejects with a bucket-related error.

### Verdict

- Keep/squash/split/drop/reorder: keep (prod safety; clearer configuration errors).
- Coherence: good.
- Depends on:
  - `loadFiles()` calling `backend.init()` (so the bucket existence check actually runs).
  - Minio client’s `bucketExists` behavior matching the mocked contract.

### Rewrite Commit Message

- Keep as-is: `fix(storage): validate S3_PORT and bucket existence`

---

## 244 2026-01-23 74fc860b07b5 `queue`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=74fc860b07b5 patchFile=/tmp/leeroy-wip-pass2/244.patch patchSha256=a93a0d6e51fe2bd123eed3a30105d971f59f96cd6fe79fa01e065935b4df061a patchBytes=1203 patchLines=26 notesReviewed=yes reviewedAt=2026-01-25T09:49:14+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=MessageQueueV1 refactor: simplify stale inFlight reclaim logic (no behavior change); squash into queue series if desired.

- Subject: refactor(queue): simplify stale in-flight reclaim
- Reasons: small refactor (reduce dead branches; make intent clearer)

### Manual Review Notes

- Simplifies stale `inFlight` reclaim logic in the CLI message queue:
  - `cli/src/api/messageQueueV1.ts`:
    - when reclaiming a stale `inFlight`, it previously rebuilt the queue and then re-read `recoveredQueue[0]` (with a defensive “should never happen” branch).
    - now directly reclaims from the extracted `item` (`{ ...item, claimedAt: now }`) and keeps the queue transform (`recoveredQueue.slice(1)`).
- Behavior should be equivalent: `item` is the former `inFlight` payload without `claimedAt`, and the recovered queue always starts with that same item.

### Verdict

- Keep/squash/split/drop/reorder: keep (or squash into the surrounding messageQueueV1 series during rewrite; it’s not a standalone behavior change).
- Coherence: good.
- Depends on: invariant that `mq.inFlight` exists in this branch (otherwise this code path is not taken).

### Rewrite Commit Message

- Keep as-is: `refactor(queue): simplify stale in-flight reclaim`

---

## 245 2026-01-23 82bf82cb6715 `hooks`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=82bf82cb6715 patchFile=/tmp/leeroy-wip-pass2/245.patch patchSha256=4ec49451a79b83e57a354c92e9a696c3f255111c75b800de0fe4b9e9639d4f3a patchBytes=4129 patchLines=124 notesReviewed=yes reviewedAt=2026-01-25T09:49:50+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=Capabilities cache race fix: guard updates with inFlightToken match so stale requests can’t clobber newer loaded state; includes race test.

- Subject: fix(hooks): prevent stale capabilities cache overwrite
- Reasons: reliability (race condition: older in-flight request should not clobber newer loaded state)

### Manual Review Notes

- Fixes an important race in the machine capabilities cache:
  - `expo-app/sources/hooks/useMachineCapabilitiesCache.ts`:
    - on both error and success paths, checks `current.inFlightToken === token` before updating the cache entry.
    - removes the previous “preserve inFlightToken if newer request started” pattern, and instead treats token mismatch as a hard “do nothing” condition.
    - net effect: an older request resolving late cannot overwrite a newer request’s `loaded` snapshot.
- Adds a direct race regression test:
  - `expo-app/sources/hooks/useMachineCapabilitiesCache.race.test.ts`:
    - starts two `prefetchMachineCapabilities` calls
    - resolves the newer one first (`version: '2'`), then resolves the older one (`version: '1'`)
    - renders the hook and asserts the cached result remains `version: '2'`.

### Verdict

- Keep/squash/split/drop/reorder: keep (this is a real correctness/race fix with good coverage).
- Coherence: good.
- Depends on:
  - cache entry tracking via a monotonic `inFlightToken`
  - callers using `prefetchMachineCapabilities` in ways that can overlap (this makes that safe).

### Rewrite Commit Message

- Keep as-is: `fix(hooks): prevent stale capabilities cache overwrite`

---

## 246 2026-01-23 384b15252026 `expo`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=384b15252026 patchFile=/tmp/leeroy-wip-pass2/246.patch patchSha256=a5574a67093df15c1f6aef108ddab08b5d274ef8afd0a486bad5951202546b9c patchBytes=16538 patchLines=403 notesReviewed=yes reviewedAt=2026-01-25T09:50:50+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=Mixed user-action hardening; needsSplit=yes: pending send delete→discard fallback + messageQueueV1 discard support goes to PR13; AskUserQuestion submit error alert+test goes to PR12 during rewrite.

- Subject: fix(expo): harden tool submit and pending send
- Reasons: reliability + user-data safety (avoid losing pending messages; show errors instead of silent console logs)

### Manual Review Notes

- Pending message “send now” hardening:
  - `expo-app/sources/components/PendingMessagesModal.tsx`:
    - after `sessionAbort` + `sync.sendMessage`, it now tries `sync.deletePendingMessage`.
    - if delete fails, it falls back to `sync.discardPendingMessage` and only surfaces an error if *both* delete and discard fail (re-throws the delete error).
  - New test coverage:
    - `expo-app/sources/components/PendingMessagesModal.discardFallback.test.ts` asserts:
      - when delete fails after send, we discard and still close without showing an alert.
    - `expo-app/sources/components/PendingMessagesModal.test.ts` is updated to include `discardPendingMessage` in its mocks (pre-req for the new call path).
- Tool submit UX hardening:
  - `expo-app/sources/components/tools/views/AskUserQuestionView.tsx`:
    - replaces a `console.error(...)` on submit failure with `Modal.alert(...)` so failures are visible to users.
  - `expo-app/sources/components/tools/views/AskUserQuestionView.test.ts` adds a regression test asserting:
    - RPC submit failure shows an alert with the error message and does not send/deny.
- Adds “discard a single queued item” support in the sync metadata layer:
  - `expo-app/sources/sync/messageQueueV1.ts` adds `discardMessageQueueV1Item(...)`:
    - moves a queue item (or the matching inFlight item) into `messageQueueV1Discarded`
    - trims discarded history to a max (default 50).
  - `expo-app/sources/sync/messageQueueV1.test.ts` adds a case verifying a queued item moves into `messageQueueV1Discarded`.
  - `expo-app/sources/sync/sync.ts` adds `sync.discardPendingMessage(...)`:
    - updates metadata via `discardMessageQueueV1Item`
    - refetches pending messages to refresh local state.

### Verdict

- Keep/squash/split/drop/reorder: keep, but consider splitting during rewrite:
  - “Pending message send/delete/discard hardening” (modal + sync discard helpers)
  - “AskUserQuestion submit error alert” (tool UX)
- Coherence: mostly good (all changes are “user action hardening”), but it spans two fairly distinct UI surfaces.
- Notes:
  - `PendingMessagesModal` still alerts with hardcoded `'Error'` for the header in its main `catch` path (existing behavior); this commit improves error visibility elsewhere but does not fully unify alert localization.
- Depends on:
  - server-side semantics: discarding a pending message is an acceptable fallback after the message is already sent.
  - the “discarded” store being surfaced in UI in a way that users can clear/re-queue as needed.

### Rewrite Commit Message

- If split:
  - `fix(expo): discard pending message when delete fails after send`
  - `fix(tools): show error when AskUserQuestion submit fails`
- If kept as one: keep as-is: `fix(expo): harden tool submit and pending send`

---

## 247 2026-01-23 24b607abfa07 `server`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=24b607abfa07 patchFile=/tmp/leeroy-wip-pass2/247.patch patchSha256=4a04e67b3c9bb2f295b23b73efa7424d9664eecb1e59da02f7cbf3b25fe18e2a patchBytes=39760 patchLines=973 notesReviewed=yes reviewedAt=2026-01-25T09:52:04+0100 notesUpdated=yes
Pass3PrPlan: finalPr=PR22 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=Server-light maintenance: schemaSync replaces schema:sqlite, adds enum generation + drift tests, centralizes Prisma types/client with init-guarded db proxy; needsSplit=yes and move unrelated yarn.lock expo-router bump to PR02.

- Subject: Refactor schema sync and centralize Prisma types
- Reasons: non-Conventional-Commits subject; too many files (29)

### Manual Review Notes

- Builds on the “server-light sqlite schema generation” work and makes it more systematic:
  - Replaces the one-off sqlite schema generator with a unified `schemaSync` script:
    - `server/scripts/schemaSync.ts` generates both:
      - `server/prisma/sqlite/schema.prisma` (from `prisma/schema.prisma`)
      - `server/sources/storage/enums.generated.ts` (TypeScript exports for Prisma enums)
    - Adds `--check` support to fail CI when generated outputs are out of date.
    - Updates scripts and docs from `schema:sqlite` → `schema:sync`.
  - Adds enum generation and guards against drift:
    - `server/sources/storage/enums.generated.ts` is introduced (currently includes `RelationshipStatus`).
    - `server/scripts/schemaSync.spec.ts` verifies enum generation stays in sync with the master Prisma schema.
  - Centralizes Prisma types and DB client initialization into `server/sources/storage/prisma.ts`:
    - Exports `Prisma`, type aliases (`TransactionClient`, `PrismaClientType`), generated enums, and `initDbPostgres/initDbSqlite`.
    - Exposes `db` as a Proxy that throws a clear error if accessed before initialization (helps detect init-order bugs).
    - Adds a lightweight `isPrismaErrorCode(err, code)` helper to avoid coupling error handling to Prisma’s error classes.
    - `server/sources/storage/db.ts` becomes a re-export (`export * from "./prisma"`), and imports across the server are updated to pull types/enums from `@/storage/prisma` instead of `@prisma/client` in various places.
  - Small robustness/type tweaks:
    - `inTx.ts` uses `isPrismaErrorCode(e, 'P2034')` instead of `instanceof PrismaClientKnownRequestError`.
    - Multiple files change `import { Prisma } ...` to `import type { Prisma } ...` where only types were used.

### Notes / Risks / Follow-ups (for history rewrite planning)

- This is a broad refactor that spans generator scripts, runtime DB initialization, and many import rewrites. For a clean history rewrite, consider splitting:
  - (1) `refactor(server): replace schema:sqlite with schema:sync` (new script + script wiring + README updates + delete old generator)
  - (2) `feat(server): generate enums.generated.ts from prisma schema` (enum gen + tests)
  - (3) `refactor(server): centralize prisma client/types and add init-guarded db proxy` (imports + inTx error handling)
- The commit also includes a small `yarn.lock` change bumping `expo-router` to 6.0.22, which is unrelated to server schema sync. During rewrite, that hunk likely belongs with the Expo-router pinning/patching work (248/249).

### Verdict

- Keep/squash/split/drop/reorder: keep, but likely split as noted above during history rewrite.
- Coherence: mostly coherent around “schema sync + prisma centralization”, but the lockfile change is unrelated noise.
- Depends on:
  - `tsx`/Node ESM runtime behavior for `import.meta.url` dynamic imports (sqlite client).
  - Prisma schema format remaining parseable by `schemaSync`’s enum parser (regex-based).
  - `initDbPostgres/initDbSqlite` being called early (the `db` proxy is intentionally fail-fast otherwise).

### Rewrite Commit Message

- Suggested rewrite (single commit): `refactor(server): centralize prisma and add schema sync`
- If split, prefer scoped Conventional Commits per topic (examples):
  - `refactor(server): replace schema:sqlite with schema:sync`
  - `feat(server): generate enums.generated.ts from prisma schema`
  - `refactor(server): centralize prisma client and types`

---

## 248 2026-01-23 bf3027799623 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=bf3027799623 patchFile=/tmp/leeroy-wip-pass2/248.patch patchSha256=e0ac60343c52aa39ca18ad1211db8a15672cb2d084b40f4afda5e097d6e79132 patchBytes=7435 patchLines=134 notesReviewed=yes reviewedAt=2026-01-25T09:52:35+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=Expo web modal reliability: set EXPO_UNSTABLE_WEB_MODAL=1 in scripts, pin+patch expo-router 6.0.22; needsSplit=yes to move session-info gating tweak (expCodexAcp) into PR19 during rewrite.

- Subject: Set EXPO_UNSTABLE_WEB_MODAL env var in Expo scripts
- Reasons: non-Conventional-Commits subject

### Manual Review Notes

- Restores/forces Expo Router modal behavior on web by enabling an “unstable/experimental” flag everywhere relevant:
  - `expo-app/package.json`:
    - prefixes `expo start`, `expo run:*`, `expo prebuild`, and the `start:*`/`ios:*`/`android:*` variants with `cross-env EXPO_UNSTABLE_WEB_MODAL=1 ...`
    - pins `expo-router` from `~6.0.7` → `6.0.22` (exact version).
  - Root `package.json` adds a Yarn `resolutions` entry to enforce `expo-router@6.0.22` across the workspace.
  - Adds an Expo Router patch under `expo-app/patches-expo-app/expo-router+6.0.22.patch` that changes the web modal export to use `ExperimentalModalStack` instead of `BaseStack`.
- Also includes a small, separate behavior tweak:
  - `expo-app/sources/app/(app)/session/[id]/info.tsx` shows the Codex session id / resume affordances when either experiment flag is enabled (`expCodexResume || expCodexAcp`), not only `expCodexResume`.

### Notes / Risks / Follow-ups (for history rewrite planning)

- This commit bundles three concerns (env flag plumbing, dependency pin + patch, and a session-info gating tweak). Consider splitting for clarity:
  - (1) `fix(expo): enable EXPO_UNSTABLE_WEB_MODAL in dev scripts`
  - (2) `chore(expo): pin expo-router to 6.0.22 and add workspace resolution`
  - (3) `chore(expo): patch expo-router web modal stack`
  - (4) `fix(session-info): show codex session id for expCodexAcp` (if desired separately)
- Patch application reliability depends on postinstall behavior; 249 improves that, so keep 248 and 249 adjacent.

### Verdict

- Keep/squash/split/drop/reorder: keep, but likely split as above during rewrite.
- Coherence: moderate (mostly Expo-router/web-modal related, but includes an extra UI gating tweak).
- Depends on:
  - `cross-env` being available (it is already used elsewhere in scripts).
  - Expo Router version compatibility with the patch and the env flag.
  - 249 (or equivalent) ensuring patches under `expo-app/patches-expo-app` are actually applied when dependencies are not hoisted.

### Rewrite Commit Message

- Suggested rewrite (single commit): `fix(expo): enable web modals and pin expo-router`
- If split, use more specific messages as outlined above.

---

## 249 2026-01-23 c803115dcbdc `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-24T17:39:06+0100 sha=c803115dcbdc patchFile=/tmp/leeroy-wip-pass2/249.patch patchSha256=9c4991901c96cb2e76116df30042c346f92bd739a8c392306c786ff9d701c68e patchBytes=1785 patchLines=36 notesReviewed=yes reviewedAt=2026-01-25T09:52:58+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T14:52:20+0100 manualReviewedForPrPlan=yes notes=Expo postinstall reliability: resolve symlinked paths and run patch-package for both hoisted and non-hoisted deps; keep adjacent to 248.

- Subject: Improve postinstall script for symlinked paths and patching
- Reasons: non-Conventional-Commits subject

### Manual Review Notes

- Improves expo-app postinstall reliability in Yarn workspaces / symlinked execution contexts:
  - `expo-app/tools/postinstall.mjs`:
    - resolves symlinks when computing `toolsDir` (`fs.realpathSync(fileURLToPath(import.meta.url))`) so `expoAppDir` and `repoRootDir` are derived from the real filesystem location, not a symlink under `node_modules`.
    - runs `patch-package` twice:
      - once from repo root with `--patch-dir expo-app/patches` (root-hoisted deps)
      - once from `expo-app/` with `--patch-dir patches-expo-app` (non-hoisted deps like `expo-router`).
- This commit is a key “plumbing” dependency for 248’s `expo-router+6.0.22.patch` to be applied reliably.

### Verdict

- Keep/squash/split/drop/reorder: keep; belongs immediately after the commit(s) that add patches under `expo-app/patches-expo-app/*` (e.g. 248) so the system becomes functional.
- Coherence: good.
- Depends on:
  - `patch-package` being installed and its CLI path resolution in the script staying correct.
  - Yarn workspaces possibly invoking the script through symlinked paths (this commit explicitly addresses that).

### Rewrite Commit Message

- Suggested rewrite (optional): `fix(expo): make postinstall patching work for non-hoisted deps`
- Current subject is fine as long as it becomes Conventional Commits: `fix(expo): improve postinstall patching for symlinked paths`

---
## 250 2026-01-24 0c3b41030bbc `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=0c3b41030bbc patchFile=/tmp/leeroy-wip-pass2/250.patch patchSha256=0bd3baa81e7931d491c7b436da0542e820972d67520a31745bf81f6ff86bf942 patchBytes=4156 patchLines=110 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T15:57:43+0100 manualReviewedForPrPlan=yes notes=Foundations / DevX: Add Vitest stubs for Expo and React Native modules

- Subject: Add Vitest stubs for Expo and React Native modules
- Reasons: non-Conventional-Commits subject

### Manual Review Notes

- Commit message body summary:
  - Introduces stub implementations for `expo-localization`, `expo-modules-core`, and `react-native` to enable unit testing in a Node environment with Vitest. Also adds a Vitest setup file that mocks `react-native-mmkv` with an in-memory store for tests. These changes allow tests to run without requiring actual Expo or React Native dependencies.
- Key file touches (first 8 of 4):
- `A expo-app/sources/dev/expoLocalizationStub.ts`
- `A expo-app/sources/dev/expoModulesCoreStub.ts`
- `M expo-app/sources/dev/reactNativeStub.ts`
- `A expo-app/sources/dev/vitestSetup.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `chore: add vitest stubs for expo and react native modules`

---

## 251 2026-01-25 7d60e6fc77f2 `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=7d60e6fc77f2 patchFile=/tmp/leeroy-wip-pass2/251.patch patchSha256=2cb1340650b23f73dba70d5c69662a551ec2973835691614d43299576445056b patchBytes=122878 patchLines=3239 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR36 plannedAt=2026-01-25T15:57:44+0100 manualReviewedForPrPlan=yes notes=ACP agents end-to-end (runtimes + replay + tool normalization): cli(acp): support loadSession + replay capture; normalize tool events

- Subject: cli(acp): support loadSession + replay capture; normalize tool events
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds replay history capture + import helpers and publishSlashCommands / - Normalizes ACP tool events and permission request payloads / - Adds focused tests for tool normalization + permission mapping
- Key file touches (first 8 of 17):
- `M cli/src/agent/acp/AcpBackend.ts`
- `A cli/src/agent/acp/bridge/acpCommonHandlers.test.ts`
- `A cli/src/agent/acp/bridge/acpCommonHandlers.ts`
- `A cli/src/agent/acp/commands/publishSlashCommands.ts`
- `A cli/src/agent/acp/history/acpReplayCapture.ts`
- `A cli/src/agent/acp/history/importAcpReplayHistory.ts`
- `A cli/src/agent/acp/permissions/permissionMapping.test.ts`
- `A cli/src/agent/acp/permissions/permissionMapping.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(acp): support loadSession + replay capture; normalize tool events`

---

## 252 2026-01-25 e35328ab19ad `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=e35328ab19ad patchFile=/tmp/leeroy-wip-pass2/252.patch patchSha256=0fff6772b989f5b922826046aad7c8ae7032fc51a070f52a0d99aff12dbafc5a patchBytes=29266 patchLines=764 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR11 plannedAt=2026-01-25T15:57:45+0100 manualReviewedForPrPlan=yes notes=Permission modes for Codex/Gemini: cli(permissions): persist per-session allowlists; harden shell approvals

- Subject: cli(permissions): persist per-session allowlists; harden shell approvals
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Introduces BasePermissionHandler allowlist persistence and a shell command allowlist / - Adds tool identifier helpers to keep permission decisions consistent across agents / - Adds targeted tests for allowlist behavior and tool identifier
- Key file touches (first 8 of 7):
- `M cli/src/codex/utils/permissionHandler.ts`
- `M cli/src/gemini/utils/permissionHandler.ts`
- `A cli/src/utils/BasePermissionHandler.allowlist.test.ts`
- `M cli/src/utils/BasePermissionHandler.ts`
- `A cli/src/utils/permissionToolIdentifier.test.ts`
- `A cli/src/utils/permissionToolIdentifier.ts`
- `A cli/src/utils/shellCommandAllowlist.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(permissions): persist per-session allowlists; harden shell approvals`

---

## 253 2026-01-25 caf5adfe3449 `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=caf5adfe3449 patchFile=/tmp/leeroy-wip-pass2/253.patch patchSha256=75ee3269317ec81ffea960c6551a5e250bb35c2fb3a9a055da0b1ccee0e0d80f patchBytes=31768 patchLines=719 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR12 plannedAt=2026-01-25T15:57:45+0100 manualReviewedForPrPlan=yes notes=AskUserQuestion + ExitPlan native handling: cli(claude): support AskUserQuestion answers + ExitPlanMode negotiation

- Subject: cli(claude): support AskUserQuestion answers + ExitPlanMode negotiation
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Routes AskUserQuestion responses through the permission decision path / - Implements ExitPlanMode semantics via permission handler, with regression tests / - Removes legacy prompt/interaction respond helpers no longer used
- Key file touches (first 8 of 10):
- `M cli/src/claude/claudeRemoteLauncher.test.ts`
- `M cli/src/claude/claudeRemoteLauncher.ts`
- `D cli/src/claude/sdk/prompts.ts`
- `M cli/src/claude/sdk/types.ts`
- `M cli/src/claude/session.ts`
- `D cli/src/claude/utils/interactionRespond.test.ts`
- `D cli/src/claude/utils/interactionRespond.ts`
- `A cli/src/claude/utils/permissionHandler.exitPlanMode.test.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(claude): support AskUserQuestion answers + ExitPlanMode negotiation`

---

## 254 2026-01-25 011a3602411f `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=011a3602411f patchFile=/tmp/leeroy-wip-pass2/254.patch patchSha256=80a9b6538a5f5a7556fbd11a7d54ab17d01959898cc825b4e8c87a7d7d90df27 patchBytes=76169 patchLines=1754 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR13 plannedAt=2026-01-25T15:57:46+0100 manualReviewedForPrPlan=yes notes=MessageQueueV1 + Pending messages end-to-end: cli(api): observe self-broadcasts for pending-queue; add tool tracing. Split toolTrace module/tests into PR39; keep self-broadcast pending-queue wake + transcript recovery in PR13. needsSplit=yes

- Subject: cli(api): observe self-broadcasts for pending-queue; add tool tracing
- Reasons: mixed commit; split during rewrite

### Manual Review Notes

- Commit message body summary:
  - - Treats self-broadcasts as pending-queue signals to improve UI reliability / - Adds transcript recovery behavior in apiSession / - Introduces JSONL tool tracing (writer + unit tests)
- Key file touches (first 8 of 6):
- `M cli/src/api/apiSession.test.ts`
- `M cli/src/api/apiSession.ts`
- `A cli/src/toolTrace/toolTrace.test.ts`
- `A cli/src/toolTrace/toolTrace.ts`
- `M cli/src/utils/MessageQueue2.ts`
- `M cli/src/utils/waitForMessagesOrPending.ts`
- Split guidance: Split toolTrace module/tests into PR39; keep self-broadcast pending-queue wake + transcript recovery in PR13.

### Verdict

- Keep/squash/split/drop/reorder: keep (split during rewrite)
- Coherence: mixed

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(api): observe self-broadcasts for pending-queue; add tool tracing`

---

## 255 2026-01-25 809d2bb385e0 `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=809d2bb385e0 patchFile=/tmp/leeroy-wip-pass2/255.patch patchSha256=205b6562ec9cfac83d0604d4f09dbf2dd062530c3c437559e9c3fd8466dbe2c5 patchBytes=8781 patchLines=243 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR39 plannedAt=2026-01-25T15:57:46+0100 manualReviewedForPrPlan=yes notes=Tool UX: normalization + specialized views + tracing: cli(tool-trace): add fixture extraction + CLI script

- Subject: cli(tool-trace): add fixture extraction + CLI script
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds a small JSONL -> fixtures extractor (with truncation/sanitization) / - Adds a tsx script and npm script for local fixture generation
- Key file touches (first 8 of 4):
- `M cli/package.json`
- `A cli/scripts/tool-trace-extract.ts`
- `A cli/src/toolTrace/extractToolTraceFixtures.test.ts`
- `A cli/src/toolTrace/extractToolTraceFixtures.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(tool-trace): add fixture extraction + CLI script`

---

## 256 2026-01-25 553781556305 `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=553781556305 patchFile=/tmp/leeroy-wip-pass2/256.patch patchSha256=86755d55cbdd4c5b623b6052a86961e87e0980957f99fc18e4dd9092494bcc07 patchBytes=5542 patchLines=139 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR04 plannedAt=2026-01-25T15:57:47+0100 manualReviewedForPrPlan=yes notes=Auth + storage scoping: cli(auth): add --no-open to skip browser open

- Subject: cli(auth): add --no-open to skip browser open
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds --no-open (and compat aliases) to happy auth login / - Threads HAPPY_NO_BROWSER_OPEN through auth UI and openBrowser / - Adds a unit test to ensure browser open is skipped when the env is set
- Key file touches (first 8 of 4):
- `M cli/src/commands/auth.ts`
- `M cli/src/ui/auth.ts`
- `A cli/src/utils/browser.test.ts`
- `M cli/src/utils/browser.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(auth): add --no-open to skip browser open`

---

## 257 2026-01-25 5955ea5c21a3 `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=5955ea5c21a3 patchFile=/tmp/leeroy-wip-pass2/257.patch patchSha256=5c3d15dd412c57a024ef1d3a518de8cb013dfb7581e3bd8b86b4eef0f1564c8a patchBytes=91512 patchLines=2195 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR16 plannedAt=2026-01-25T15:57:47+0100 manualReviewedForPrPlan=yes notes=Daemon reliability (ownership + reattach/restart safety): cli(daemon): idempotent resume + attach files + shutdown policy

- Subject: cli(daemon): idempotent resume + attach files + shutdown policy
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Ensures resume requests are idempotent (avoid duplicate processes) / - Adds session attach files and startup metadata merge/update helpers / - Adds shutdown watchdog policy and a non-interactive auth gating regression test
- Key file touches (first 8 of 22):
- `M cli/src/api/apiMachine.spawnSession.test.ts`
- `M cli/src/api/apiMachine.ts`
- `M cli/src/claude/runClaude.ts`
- `A cli/src/daemon/findRunningTrackedSessionById.test.ts`
- `A cli/src/daemon/findRunningTrackedSessionById.ts`
- `D cli/src/daemon/persistedHappySession.ts`
- `A cli/src/daemon/run.noninteractiveAuth.test.ts`
- `M cli/src/daemon/run.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(daemon): idempotent resume + attach files + shutdown policy`

---

## 258 2026-01-25 cce05b18a5d7 `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=cce05b18a5d7 patchFile=/tmp/leeroy-wip-pass2/258.patch patchSha256=253db5316b529fce87dfa866b4c91899c6d21c41304d3429aad6939f44095d5a patchBytes=3183 patchLines=78 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR36 plannedAt=2026-01-25T15:57:48+0100 manualReviewedForPrPlan=yes notes=ACP agents end-to-end (runtimes + replay + tool normalization): cli(agents): wire ACP agents into factory/transport registry

- Subject: cli(agents): wire ACP agents into factory/transport registry
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Exposes Codex ACP + OpenCode factories from the agent factory index / - Registers OpenCode agent during initializeAgents / - Exports OpenCode transport from the transport handler registry
- Key file touches (first 8 of 4):
- `M cli/src/agent/factories/index.ts`
- `M cli/src/agent/index.ts`
- `M cli/src/agent/transport/handlers/index.ts`
- `M cli/src/agent/transport/index.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(agents): wire ACP agents into factory/transport registry`

---

## 259 2026-01-25 27bb2523f690 `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=27bb2523f690 patchFile=/tmp/leeroy-wip-pass2/259.patch patchSha256=827891b5b40429736503fc841da9ece678904d11272f6dcc29f59280e6cf0382 patchBytes=48493 patchLines=1352 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR36 plannedAt=2026-01-25T15:57:48+0100 manualReviewedForPrPlan=yes notes=ACP agents end-to-end (runtimes + replay + tool normalization): cli(opencode): add OpenCode ACP agent runtime

- Subject: cli(opencode): add OpenCode ACP agent runtime
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds OpenCode ACP runtime and transport handler / - Adds permission handler + session id helpers for OpenCode / - Includes focused unit tests for transport and OpenCode helpers
- Key file touches (first 8 of 11):
- `A cli/src/agent/factories/opencode.ts`
- `A cli/src/agent/transport/handlers/OpenCodeTransport.test.ts`
- `A cli/src/agent/transport/handlers/OpenCodeTransport.ts`
- `A cli/src/opencode/acp/openCodeAcpRuntime.ts`
- `A cli/src/opencode/runOpenCode.ts`
- `A cli/src/opencode/utils/opencodeSessionIdMetadata.test.ts`
- `A cli/src/opencode/utils/opencodeSessionIdMetadata.ts`
- `A cli/src/opencode/utils/permissionHandler.test.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(opencode): add OpenCode ACP agent runtime`

---

## 260 2026-01-25 abe1cf8ed370 `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=abe1cf8ed370 patchFile=/tmp/leeroy-wip-pass2/260.patch patchSha256=17865656ad84c9bb6b919925b2daafa1a5242142290e6d5534b721da4bf7ab6b patchBytes=114517 patchLines=2680 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR36 plannedAt=2026-01-25T15:57:49+0100 manualReviewedForPrPlan=yes notes=ACP agents end-to-end (runtimes + replay + tool normalization): cli(codex-acp): add runtime + capability gating. May split generic capabilities checklist wiring into PR14; keep Codex ACP runtime + session-id helpers in PR36. needsSplit=yes

- Subject: cli(codex-acp): add runtime + capability gating
- Reasons: mixed commit; split during rewrite

### Manual Review Notes

- Commit message body summary:
  - - Adds Codex ACP runtime + command resolution and session-id helpers / - Extends capabilities framework for ACP probing and Codex ACP dependency checks / - Updates API/metadata types to cover ACP session ids and capability flags
- Key file touches (first 8 of 24):
- `A cli/src/agent/factories/codexAcp.ts`
- `M cli/src/api/types.ts`
- `A cli/src/codex/acp/codexAcpRuntime.ts`
- `A cli/src/codex/acp/resolveCodexAcpCommand.ts`
- `M cli/src/codex/codexMcpClient.ts`
- `M cli/src/codex/runCodex.ts`
- `M cli/src/codex/types.ts`
- `A cli/src/codex/utils/codexSessionIdMetadata.test.ts`
- Split guidance: May split generic capabilities checklist wiring into PR14; keep Codex ACP runtime + session-id helpers in PR36.

### Verdict

- Keep/squash/split/drop/reorder: keep (split during rewrite)
- Coherence: mixed

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(codex-acp): add runtime + capability gating`

---

## 261 2026-01-25 d9100a80112b `cli`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=d9100a80112b patchFile=/tmp/leeroy-wip-pass2/261.patch patchSha256=23b17783030d08b2852adf0b0af4cfb84a5c2724854c9a3157a74f921230ecfb patchBytes=16297 patchLines=342 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR36 plannedAt=2026-01-25T15:57:49+0100 manualReviewedForPrPlan=yes notes=ACP agents end-to-end (runtimes + replay + tool normalization): cli(entry): refactor agent subcommands and startup flags

- Subject: cli(entry): refactor agent subcommands and startup flags
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds --permission-mode-updated-at parsing (for consistent session metadata) / - Adds opencode command wiring and aligns codex/gemini argument parsing / - Removes the legacy resume subcommand in favor of existing-session/resume flags
- Key file touches (first 8 of 1):
- `M cli/src/index.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `cli(entry): refactor agent subcommands and startup flags`

---

## 262 2026-01-25 2edc5305ca3f `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=2edc5305ca3f patchFile=/tmp/leeroy-wip-pass2/262.patch patchSha256=67ef926911223e5350fb39b7ee569bf6cb08979a4314a56cea74d55be5f2c583 patchBytes=9600 patchLines=226 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T15:57:49+0100 manualReviewedForPrPlan=yes notes=Foundations / DevX: expo-app(test): harden vitest stubs + unistyles typing

- Subject: expo-app(test): harden vitest stubs + unistyles typing
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Updates vitest config aliases/setup to improve node-test reliability / - Improves unistyles typing/bootstrap and normalizes formatting / - Adds RootLayout hook-order regression test and removes the old layout test location
- Key file touches (first 8 of 6):
- `M expo-app/index.ts`
- `R097 expo-app/sources/app/(app)/_layout.test.ts expo-app/sources/__tests__/app/_layout.test.ts`
- `M expo-app/sources/dev/expoModulesCoreStub.ts`
- `M expo-app/sources/dev/reactNativeStub.ts`
- `M expo-app/sources/unistyles.ts`
- `M expo-app/vitest.config.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(test): harden vitest stubs + unistyles typing`

---

## 263 2026-01-25 5575d7267cd0 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=5575d7267cd0 patchFile=/tmp/leeroy-wip-pass2/263.patch patchSha256=b0ad62a641044f3216a5e951e625030f79738a2f538ae2b2dc8bb315cf37d5ab patchBytes=47248 patchLines=979 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR39 plannedAt=2026-01-25T15:57:50+0100 manualReviewedForPrPlan=yes notes=Tool UX: normalization + specialized views + tracing: expo-app(tools): normalize tool inference + rendering inputs

- Subject: expo-app(tools): normalize tool inference + rendering inputs
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds shared tool-call normalization and parsing helpers / - Improves tool name inference and permission summaries / - Updates ToolView/ToolFullView to rely on normalized shapes
- Key file touches (first 8 of 10):
- `M expo-app/sources/components/tools/ToolFullView.tsx`
- `M expo-app/sources/components/tools/ToolView.tsx`
- `M expo-app/sources/components/tools/knownTools.tsx`
- `A expo-app/sources/components/tools/utils/normalizeToolCallForRendering.ts`
- `A expo-app/sources/components/tools/utils/parseJson.ts`
- `A expo-app/sources/components/tools/utils/parseParenIdentifier.ts`
- `A expo-app/sources/components/tools/utils/permissionSummary.ts`
- `A expo-app/sources/components/tools/utils/shellCommand.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(tools): normalize tool inference + rendering inputs`

---

## 264 2026-01-25 d115797fcdde `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=d115797fcdde patchFile=/tmp/leeroy-wip-pass2/264.patch patchSha256=a7292bb65cb18806924e11366e0e9bc9ef8debdd82f08ed6d269ff9925907ed7 patchBytes=177544 patchLines=4529 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR39 plannedAt=2026-01-25T15:57:50+0100 manualReviewedForPrPlan=yes notes=Tool UX: normalization + specialized views + tracing: expo-app(tools): add specialized tool views + tests

- Subject: expo-app(tools): add specialized tool views + tests
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds dedicated views for common tools (bash/read/grep/glob/web) and structured results / - Improves permission UI flows (pending/abort/decision) and adds regression tests / - Expands tool rendering test coverage for inference and fallback behavior
- Key file touches (first 8 of 40):
- `A expo-app/sources/components/tools/PermissionFooter.codexDecision.test.tsx`
- `A expo-app/sources/components/tools/PermissionFooter.stopAbortsRun.test.tsx`
- `M expo-app/sources/components/tools/PermissionFooter.tsx`
- `A expo-app/sources/components/tools/ToolFullView.inference.test.ts`
- `A expo-app/sources/components/tools/ToolFullView.permissionPending.test.tsx`
- `A expo-app/sources/components/tools/ToolView.acpKindFallback.test.tsx`
- `A expo-app/sources/components/tools/ToolView.exitPlanMode.test.ts`
- `A expo-app/sources/components/tools/ToolView.minimalSpecificView.test.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(tools): add specialized tool views + tests`

---

## 265 2026-01-25 23014814ea50 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=23014814ea50 patchFile=/tmp/leeroy-wip-pass2/265.patch patchSha256=ac9a64fe8faa1da51349bb7c7691b6ae97b4ac5f09ea07378eb139def291c430 patchBytes=62254 patchLines=1638 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR37 plannedAt=2026-01-25T15:57:51+0100 manualReviewedForPrPlan=yes notes=Agent registry end-to-end (selection + settings + UX helpers): expo-app(agents): introduce agent registry + UI behavior helpers

- Subject: expo-app(agents): introduce agent registry + UI behavior helpers
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds a typed agent registry (core + UI helpers) / - Centralizes enabled-agent logic, CLI warnings, and resume capability option derivation / - Includes tests for registry behavior and picker options
- Key file touches (first 8 of 17):
- `A expo-app/sources/agents/acpRuntimeResume.ts`
- `A expo-app/sources/agents/agentPickerOptions.test.ts`
- `A expo-app/sources/agents/agentPickerOptions.ts`
- `A expo-app/sources/agents/cliWarnings.test.ts`
- `A expo-app/sources/agents/cliWarnings.ts`
- `A expo-app/sources/agents/enabled.test.ts`
- `A expo-app/sources/agents/enabled.ts`
- `A expo-app/sources/agents/permissionUiCopy.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(agents): introduce agent registry + UI behavior helpers`

---

## 266 2026-01-25 b142327f5e32 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=b142327f5e32 patchFile=/tmp/leeroy-wip-pass2/266.patch patchSha256=f1795763d5268ba3987a045072dad6bc553e45a7f3946e68e6c2ed715de6e5ac patchBytes=251895 patchLines=5615 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR14 plannedAt=2026-01-25T15:57:51+0100 manualReviewedForPrPlan=yes notes=Capabilities end-to-end (protocol + wiring + reliability): expo-app(sync): capabilities protocol + resume/pending-queue plumbing. Very mixed (capabilities protocol + pending queue + resume + model options + permission defaults + profiles). Split during rewrite by feature PRs; keep core capabilities protocol + installable deps registry in PR14. needsSplit=yes

- Subject: expo-app(sync): capabilities protocol + resume/pending-queue plumbing
- Reasons: mixed commit; split during rewrite

### Manual Review Notes

- Commit message body summary:
  - - Adds installable dependency registry and machine capability request protocol / - Extends sync reducer/storage/types to support pending-queue + resume flows / - Adds tests for capabilities parsing, permission defaults, and resume/pending logic
- Key file touches (first 8 of 59):
- `A expo-app/sources/capabilities/codexAcpDep.test.ts`
- `A expo-app/sources/capabilities/codexAcpDep.ts`
- `A expo-app/sources/capabilities/codexMcpResume.test.ts`
- `A expo-app/sources/capabilities/codexMcpResume.ts`
- `A expo-app/sources/capabilities/installableDepsRegistry.test.ts`
- `A expo-app/sources/capabilities/installableDepsRegistry.ts`
- `A expo-app/sources/capabilities/requests.ts`
- `M expo-app/sources/hooks/useMachineCapabilitiesCache.hook.test.ts`
- Split guidance: Very mixed (capabilities protocol + pending queue + resume + model options + permission defaults + profiles). Split during rewrite by feature PRs; keep core capabilities protocol + installable deps registry in PR14.

### Verdict

- Keep/squash/split/drop/reorder: keep (split during rewrite)
- Coherence: mixed

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(sync): capabilities protocol + resume/pending-queue plumbing`

---

## 267 2026-01-25 552f517e9823 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=552f517e9823 patchFile=/tmp/leeroy-wip-pass2/267.patch patchSha256=77f286f96e79fefe976402b01c0cba9597da73e77150f45af4d1af4511f2f0fb patchBytes=31418 patchLines=783 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR09 plannedAt=2026-01-25T15:57:51+0100 manualReviewedForPrPlan=yes notes=Secrets / vault feature: expo-app(ui): add InlineAddExpander; inline add for secrets/env vars. Touches both SecretsList and EnvironmentVariablesList; split env-vars inline-add pieces into PR07 and keep secrets inline-add pieces in PR09. needsSplit=yes

- Subject: expo-app(ui): add InlineAddExpander; inline add for secrets/env vars
- Reasons: mixed commit; split during rewrite

### Manual Review Notes

- Commit message body summary:
  - - Introduces a reusable inline expander with Cancel/Save actions / - Refactors EnvironmentVariablesList and SecretsList to add items inline (no modal) / - Adds tests covering the new inline add flows
- Key file touches (first 8 of 5):
- `M expo-app/sources/components/EnvironmentVariablesList.test.ts`
- `M expo-app/sources/components/EnvironmentVariablesList.tsx`
- `A expo-app/sources/components/InlineAddExpander.tsx`
- `A expo-app/sources/components/secrets/SecretsList.test.ts`
- `M expo-app/sources/components/secrets/SecretsList.tsx`
- Split guidance: Touches both SecretsList and EnvironmentVariablesList; split env-vars inline-add pieces into PR07 and keep secrets inline-add pieces in PR09.

### Verdict

- Keep/squash/split/drop/reorder: keep (split during rewrite)
- Coherence: mixed

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(ui): add InlineAddExpander; inline add for secrets/env vars`

---

## 268 2026-01-25 cbaf39a63c60 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=cbaf39a63c60 patchFile=/tmp/leeroy-wip-pass2/268.patch patchSha256=bc170479691be614d53ed00e27711c0f589f246de7fdacea72e4d94695929645 patchBytes=109523 patchLines=2112 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR26 plannedAt=2026-01-25T15:57:52+0100 manualReviewedForPrPlan=yes notes=New session wizard end-to-end: expo-app(secret-requirement): add picker route + modularized modal

- Subject: expo-app(secret-requirement): add picker route + modularized modal
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Extracts SecretRequirementModal into a focused module and adds a full-screen picker route / - Adds pure helpers for applying results and gating auto-prompt behavior (with tests) / - Adjusts /new modal presentation + web CSS sizing to avoid hidden/stacked screens
- Key file touches (first 8 of 10):
- `M expo-app/sources/app/(app)/_layout.tsx`
- `A expo-app/sources/app/(app)/new/pick/secret-requirement.tsx`
- `M expo-app/sources/components/SecretRequirementModal.tsx`
- `A expo-app/sources/components/secretRequirement/SecretRequirementModal.tsx`
- `A expo-app/sources/components/secretRequirement/SecretRequirementScreen.tsx`
- `M expo-app/sources/theme.css`
- `A expo-app/sources/utils/secretRequirementApply.test.ts`
- `A expo-app/sources/utils/secretRequirementApply.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(secret-requirement): add picker route + modularized modal`

---

## 269 2026-01-25 36641d906490 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=36641d906490 patchFile=/tmp/leeroy-wip-pass2/269.patch patchSha256=b0225890ae33d32f4303a295855101b36b38646c8c648b03f29a5fdb4973e519 patchBytes=254136 patchLines=5113 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T15:57:52+0100 manualReviewedForPrPlan=yes notes=Resume end-to-end: expo-app(session): capability-gated resume flows + pending queue UX. Mixed resume UX + pending queue UI + capabilities gating + new-session changes; split by PR19/PR13/PR14/PR26 during rewrite. needsSplit=yes

- Subject: expo-app(session): capability-gated resume flows + pending queue UX
- Reasons: mixed commit; split during rewrite

### Manual Review Notes

- Commit message body summary:
  - - Wires agent registry + machine capabilities into new-session and session resume flows / - Adds pending message rendering and session notices for inactive/offline states / - Adds supporting UI components/tests for CLI detection and capability preflight
- Key file touches (first 8 of 33):
- `M expo-app/sources/-session/SessionView.tsx`
- `A expo-app/sources/-session/sessionResumeUi.test.ts`
- `A expo-app/sources/-session/sessionResumeUi.ts`
- `A expo-app/sources/__tests__/app/machine/machineDetails.capabilitiesRequestStability.test.ts`
- `M expo-app/sources/app/(app)/machine/[id].tsx`
- `M expo-app/sources/app/(app)/new/NewSessionWizard.tsx`
- `M expo-app/sources/app/(app)/new/index.tsx`
- `M expo-app/sources/app/(app)/new/pick/machine.tsx`
- Split guidance: Mixed resume UX + pending queue UI + capabilities gating + new-session changes; split by PR19/PR13/PR14/PR26 during rewrite.

### Verdict

- Keep/squash/split/drop/reorder: keep (split during rewrite)
- Coherence: mixed

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(session): capability-gated resume flows + pending queue UX`

---

## 270 2026-01-25 9a9d439e5499 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=9a9d439e5499 patchFile=/tmp/leeroy-wip-pass2/270.patch patchSha256=1d4a36ef73b6a702d1638e74f6bfb6d3884b849a8ee19b6d980e656b793aa457 patchBytes=11462 patchLines=200 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR37 plannedAt=2026-01-25T15:57:53+0100 manualReviewedForPrPlan=yes notes=Agent registry end-to-end (selection + settings + UX helpers): expo-app(settings): wire agent registry into settings

- Subject: expo-app(settings): wire agent registry into settings
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Updates settings screens to reflect agent registry defaults and available agents / - Keeps session/account settings aligned with new permission/default behavior
- Key file touches (first 8 of 2):
- `M expo-app/sources/app/(app)/settings/account.tsx`
- `M expo-app/sources/app/(app)/settings/session.tsx`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(settings): wire agent registry into settings`

---

## 271 2026-01-25 67ceab362d6b `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=67ceab362d6b patchFile=/tmp/leeroy-wip-pass2/271.patch patchSha256=a7ecc8cfda151be628e0f1808052a8e66d5c532a4afb59233bd1cdef2ce15ce3 patchBytes=6628 patchLines=168 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR35 plannedAt=2026-01-25T15:57:54+0100 manualReviewedForPrPlan=yes notes=Friends: UX + reliability (search errors + follow-ups): expo-app(experiments): add inbox friends experiment gate

- Subject: expo-app(experiments): add inbox friends experiment gate
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Adds an experiment flag and hooks for inbox/friends enablement / - Updates inbox/friends routes to gate access and routing based on the experiment
- Key file touches (first 8 of 7):
- `M expo-app/sources/app/(app)/friends/index.tsx`
- `M expo-app/sources/app/(app)/friends/search.tsx`
- `M expo-app/sources/app/(app)/inbox/index.tsx`
- `A expo-app/sources/experiments/inboxFriends.test.ts`
- `A expo-app/sources/experiments/inboxFriends.ts`
- `A expo-app/sources/hooks/useInboxFriendsEnabled.ts`
- `A expo-app/sources/hooks/useRequireInboxFriendsEnabled.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(experiments): add inbox friends experiment gate`

---

## 272 2026-01-25 05b45b4f505d `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=05b45b4f505d patchFile=/tmp/leeroy-wip-pass2/272.patch patchSha256=29618ee0b74b8b4192c5dfd0b47bbb2f0ec2995fbb9267721e9927691f53c43e patchBytes=8735 patchLines=158 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR37 plannedAt=2026-01-25T15:57:54+0100 manualReviewedForPrPlan=yes notes=Agent registry end-to-end (selection + settings + UX helpers): expo-app(settings): update features toggles for agents/inbox/codex. Settings toggles span agent registry + inboxFriends + codex ACP; split inboxFriends toggle into PR35 and keep agent toggles in PR37 (codex ACP toggle may live with PR36/PR37). needsSplit=yes

- Subject: expo-app(settings): update features toggles for agents/inbox/codex
- Reasons: mixed commit; split during rewrite

### Manual Review Notes

- Commit message body summary:
  - - Replaces expGemini toggle with agent-registry-backed experimental agent toggles / - Adds inbox friends experiment toggle / - Adds Codex ACP toggle and keeps Codex resume/acp mutually exclusive
- Key file touches (first 8 of 1):
- `M expo-app/sources/app/(app)/settings/features.tsx`
- Split guidance: Settings toggles span agent registry + inboxFriends + codex ACP; split inboxFriends toggle into PR35 and keep agent toggles in PR37 (codex ACP toggle may live with PR36/PR37).

### Verdict

- Keep/squash/split/drop/reorder: keep (split during rewrite)
- Coherence: mixed

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(settings): update features toggles for agents/inbox/codex`

---

## 273 2026-01-25 ec9225a8f935 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=ec9225a8f935 patchFile=/tmp/leeroy-wip-pass2/273.patch patchSha256=7852b1364ef65f88fff41589a7971d51a1a875515cd4ad7e26aeea0540ed4630 patchBytes=90752 patchLines=1529 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR03 plannedAt=2026-01-25T15:57:55+0100 manualReviewedForPrPlan=yes notes=i18n foundation sweep (optional): expo-app(i18n): add device locale helpers + update translations. Translations sweep: distribute keys into owning feature PRs; keep deviceLocales helpers where they best belong (likely PR03 or PR01) during rewrite. needsSplit=yes

- Subject: expo-app(i18n): add device locale helpers + update translations
- Reasons: mixed commit; split during rewrite

### Manual Review Notes

- Commit message body summary:
  - - Adds device locale helpers for native and shared runtime / - Updates translation dictionaries and text index wiring
- Key file touches (first 8 of 12):
- `A expo-app/sources/text/deviceLocales.native.ts`
- `A expo-app/sources/text/deviceLocales.ts`
- `M expo-app/sources/text/index.ts`
- `M expo-app/sources/text/translations/ca.ts`
- `M expo-app/sources/text/translations/en.ts`
- `M expo-app/sources/text/translations/es.ts`
- `M expo-app/sources/text/translations/it.ts`
- `M expo-app/sources/text/translations/ja.ts`
- Split guidance: Translations sweep: distribute keys into owning feature PRs; keep deviceLocales helpers where they best belong (likely PR03 or PR01) during rewrite.

### Verdict

- Keep/squash/split/drop/reorder: keep (split during rewrite)
- Coherence: mixed

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(i18n): add device locale helpers + update translations`

---

## 274 2026-01-25 98b7138360ff `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=98b7138360ff patchFile=/tmp/leeroy-wip-pass2/274.patch patchSha256=d59aa54252fde89b78efff2a8b0f707b0831199ba8eccdf95515d537c9a9baf3 patchBytes=7473 patchLines=179 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR19 plannedAt=2026-01-25T15:57:56+0100 manualReviewedForPrPlan=yes notes=Resume end-to-end: expo-app(agents): align resume helpers + temp session data typing

- Subject: expo-app(agents): align resume helpers + temp session data typing
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Refactors agent resume capability helpers to use the agent registry / - Adds getAgentVendorResumeId for vendor resume id lookup / - Types temp session data agentType as AgentId and adds regression tests
- Key file touches (first 8 of 3):
- `A expo-app/sources/utils/agentCapabilities.test.ts`
- `M expo-app/sources/utils/agentCapabilities.ts`
- `M expo-app/sources/utils/tempDataStore.ts`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(agents): align resume helpers + temp session data typing`

---

## 275 2026-01-25 63cda8b20882 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=63cda8b20882 patchFile=/tmp/leeroy-wip-pass2/275.patch patchSha256=7982daf7749e526488373bf61add1fbc7c530ea8131b9dcac8fe5b3c747bcea0 patchBytes=170996 patchLines=3424 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR38 plannedAt=2026-01-25T15:57:56+0100 manualReviewedForPrPlan=yes notes=UI list primitives + pending UI polish: expo-app(ui): add list/pending primitives and polish. UI primitive churn plus profile list model changes; consider splitting profile requirements/model changes into PR08 and leaving generic list/pending primitives in PR38. needsSplit=yes

- Subject: expo-app(ui): add list/pending primitives and polish
- Reasons: mixed commit; split during rewrite

### Manual Review Notes

- Commit message body summary:
  - - Adds reusable item-group row helpers and pending queue UI coverage / - Refines list and settings components for more consistent layout/behavior / - Updates profile list model helpers and adds targeted tests
- Key file touches (first 8 of 31):
- `M expo-app/sources/components/AgentInput.tsx`
- `M expo-app/sources/components/Avatar.tsx`
- `M expo-app/sources/components/EnvironmentVariableCard.tsx`
- `M expo-app/sources/components/Item.tsx`
- `M expo-app/sources/components/ItemGroup.dividers.test.ts`
- `M expo-app/sources/components/ItemGroup.dividers.ts`
- `A expo-app/sources/components/ItemGroupRowPosition.tsx`
- `A expo-app/sources/components/ItemGroupTitleWithAction.test.ts`
- Split guidance: UI primitive churn plus profile list model changes; consider splitting profile requirements/model changes into PR08 and leaving generic list/pending primitives in PR38.

### Verdict

- Keep/squash/split/drop/reorder: keep (split during rewrite)
- Coherence: mixed

### Rewrite Commit Message

- Suggested rewrite (single commit): `expo-app(ui): add list/pending primitives and polish`

---

## 276 2026-01-25 10dca4ca7057 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T15:57:43+0100 sha=10dca4ca7057 patchFile=/tmp/leeroy-wip-pass2/276.patch patchSha256=a6d8181c81bac7665d6be4f119d91d766642037a663b814f3218c7b665c9f817 patchBytes=3567 patchLines=81 notesReviewed=yes reviewedAt=2026-01-25T15:57:43+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR02 plannedAt=2026-01-25T15:57:56+0100 manualReviewedForPrPlan=yes notes=UI modal/overlay/popover + Expo web modal behavior: chore(expo): pin libsodium-wrappers and align patch-package dirs

- Subject: chore(expo): pin libsodium-wrappers and align patch-package dirs
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - - Pins libsodium-wrappers to 0.7.15 to avoid patching 0.7.16 / - Moves expo-router patch to expo-app/patches and updates postinstall patch-package invocation / - Updates yarn.lock to match dependency pin
- Key file touches (first 8 of 5):
- `M expo-app/package.json`
- `R100 expo-app/patches-expo-app/expo-router+6.0.22.patch expo-app/patches/expo-router+6.0.22.patch`
- `D expo-app/patches/libsodium-wrappers+0.7.16.patch`
- `M expo-app/tools/postinstall.mjs`
- `M yarn.lock`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `chore(expo): pin libsodium-wrappers and align patch-package dirs`

---

## 277 2026-01-25 cfe2ecd3e626 `expo-app`
Pass2FullDiff: CAPTURED at 2026-01-25T19:06:49+0100 sha=cfe2ecd3e626 patchFile=/tmp/leeroy-wip-pass2/277.patch patchSha256=077e2670d05ea95a64c6d431cd066e14a173813072989a7ebffc3196e82403e1 patchBytes=22191 patchLines=498 notesReviewed=yes reviewedAt=2026-01-25T19:06:49+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T19:06:49+0100 manualReviewedForPrPlan=yes notes=Foundations / DevX: mechanical StyleSheet→unistyles import migration across expo-app + add invariant test.

- Subject: refactor: migrate StyleSheet imports to react-native-unistyles
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - Replaces all StyleSheet imports from 'react-native' with imports from 'react-native-unistyles' across sources/.
  - Adds a test to enforce the invariant (guards against regressions).
- Key file touches (first 8 of 23):
- `M expo-app/sources/app/(app)/dev/colors.tsx`
- `M expo-app/sources/app/(app)/dev/inverted-list.tsx`
- `M expo-app/sources/app/(app)/dev/modal-demo.tsx`
- `M expo-app/sources/app/(app)/dev/shimmer-demo.tsx`
- `M expo-app/sources/app/(app)/dev/tools2.tsx`
- `M expo-app/sources/app/(app)/dev/typography.tsx`
- `M expo-app/sources/components/ChatHeaderView.tsx`
- `M expo-app/sources/components/CommandPalette/CommandPalette.tsx`
- No split expected (mechanical refactor + invariant test belong together).

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `refactor(expo-app): migrate StyleSheet imports to react-native-unistyles`

---

## 278 2026-01-25 8aaacbfaac7a `dev`
Pass2FullDiff: CAPTURED at 2026-01-25T19:06:49+0100 sha=8aaacbfaac7a patchFile=/tmp/leeroy-wip-pass2/278.patch patchSha256=919c70e5a7dc67ead028eef736595347efd56147858aa29d8e42dde40d759a90 patchBytes=1804 patchLines=45 notesReviewed=yes reviewedAt=2026-01-25T19:06:49+0100 notesUpdated=no
Pass3PrPlan: finalPr=PR01 plannedAt=2026-01-25T19:06:49+0100 manualReviewedForPrPlan=yes notes=Foundations / DevX: add Conventional Commits guidelines for tooling (copilot commit instructions).

- Subject: docs: add Conventional Commits instructions
- Reasons: n/a

### Manual Review Notes

- Commit message body summary:
  - Adds `.github/copilot-commit-instructions.md` documenting Conventional Commits guidelines for this repo.
- Key file touches (first 8 of 1):
- `A .github/copilot-commit-instructions.md`

### Verdict

- Keep/squash/split/drop/reorder: keep
- Coherence: good

### Rewrite Commit Message

- Suggested rewrite (single commit): `docs: add Conventional Commits instructions`

---
