# Proof audit, pass 1 — mobile PR worktree retargeting

PR [#27658](https://git.cafe/pingdotgg/t3code/pull/27658), also mirrored as [#27658](https://github.com/pingdotgg/t3code/pull/27658).
Reviewed head [63d02cd849](https://github.com/pingdotgg/t3code/commit/63d02cd849afcc565380bb56b924d2bdb34fecc5), branch `fix/issue-27616`, against merge-base [6575ce3250](https://github.com/pingdotgg/t3code/commit/6575ce32508bd7908444241f3719aa33aaf77f90) on `main`.
Read-only review of `/home/ubuntu/wt/pr-27658`. No captures exist in this pass. The PR description's reported manual runs are claims to corroborate, not attached proof. No browser, device, server, test runner, or provider was launched by this audit. Tests were read, not rerun. The worktree was clean at the requested head. Disk had 12 GB available.

## Findings first

1. **Proof blocker: no reviewable before/after recordings.** The core defect is a state transition, not a static layout change. A still of the final `New worktree` label cannot establish that it survived a machine move, that the PR chip belongs to the same draft, or that the move did not lose text. Required recordings are listed below; stable endpoint PNGs may supplement them.
2. **The proposed negative fixture needs two separate cases.** A known different repository is normally excluded from the environment picker. An unknown identity can match by folder basename or title, but must drop the PR workspace. With the valid same-repository project still registered on B, adding a third mismatched project on B does not exercise fallback: the identity match wins. Use separate catalog phases on disposable B, described below. Do not label a picker-omission capture as proof that retargeting reset a workspace.
3. **Labels cannot prove the machine-local path was cleared or that Send uses the PR.** The new test uses a PR selection whose path is already null. The moved-path harness row needs an app-store transcript, and one actual Send on B needs destination thread/worktree evidence. The PR explicitly says sending was not exercised. These are outstanding proof requirements, not confirmed implementation bugs.

No confirmed code defect was found in this scoped source audit. This is a proof plan, not a merge-readiness verdict.

## Exact changed surface and callers

Only `NewTaskFlowProvider` is a React component in the production diff. `new-task-project-selection.ts` and `use-composer-drafts.ts` are helper/store modules; the other two changed files are tests. There are no new styles, assets, dimensions, theme checks, platform checks, or presentational components in the diff.

| Changed unit | Complete production call/render path | Inputs and branches relevant to proof |
|---|---|---|
| [NewTaskFlowProvider](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/new-task-flow-provider.tsx#L220) | Sole JSX caller: `RootStackLayout` in [Stack.tsx](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/Stack.tsx#L501). Always returns `ActiveNewTaskContext` → `NewTaskFlowContext.Provider` → `children`. | `active` false suppresses draft creation and active-new-task publication; true/omitted allows them. `flowKey` omitted uses inactive key; a draft route supplies its route-specific key. `routeDraftId` absent permits a new draft; present binds a loaded persisted draft or resolves pending/missing. String-array route parameters are unwrapped by the caller. `children` supplies the navigator subtree; the changed component has no direct visual conditional. |
| `carryDraftContentTo` | Called only by `setProject` and `selectEnvironment` inside the provider. Sole production caller of `retargetNewTaskDraft`. | Existing new-task key retargets that same key; null/non-new-task key creates a fresh draft. Explicit `setProject` omits the option. Machine `selectEnvironment` supplies it only from `isSameRepository`. |
| `selectEnvironment` / `switchEnvironment` | Environment picker row → `switchEnvironment`; draft screen `cycleHost` → `switchEnvironment`; normal draft → `selectEnvironment`. Exposed `selectEnvironment` has no other production consumer. | Same selected environment is a no-op. Non-scratch moves synchronously. Scratch uses `openScratch`, shows disabled picker rows while pending, applies success via `setProject`, alerts non-interrupted failure, ignores stale completions. A match retargets and sets project key; no match retains draft key and clears project key. |
| [isSameRepository / resolveEnvironmentProjectMatch](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/new-task-project-selection.ts#L49) | Only production consumer of both is the provider. | Match priority: equal repository grouping key → equal nonempty basename unless known mismatch → equal title unless known mismatch → first target project → null. Equality requires BOTH identities; key is `origin.canonicalKey ?? canonicalKey`, not title, root path, or raw remote URL. Null left/right is false. Several matching projects choose the first in the existing catalog order. |
| [retargetNewTaskDraft](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/state/use-composer-drafts.ts#L1878) | Provider's `carryDraftContentTo`; all other direct callers are tests. | Same environment AND project stamp returns unchanged before considering the option. Otherwise, option true AND `pullRequestReference !== undefined` preserves every workspace field except setting `worktreePath: null`. False/omitted option, absent selection, or undefined PR reference drops workspace. It does NOT force `mode: worktree`, validate the URL, or require a nonempty PR reference. Text, context, model/runtime/interaction settings, import receipts and original `createdAt` survive. Attachments keep bytes; uploads stamped to another environment lose their upload reference, target-stamped/unstamped ones retain it. Missing draft normalizes empty state and gets a new stamp. |

All live context consumers, including indirect visible effects:

| Consumer | Render/interaction variants to collapse into fixtures |
|---|---|
| [NewTaskDraftScreen](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx#L144) | No selected project → null; no selected model → saved-draft/editor fallback with environment control and no workspace row; otherwise full thread composer. Non-scratch hero shows project title (one line, 250px cap); scratch hero says `No project` and omits workspace controls. Local + null path → `Current checkout`; local + path → `Current worktree`; worktree → `New worktree`. Valid PR URL + worktree → `From PR #N` (or source-control client's terminology); otherwise branch/local, `From branch`, `From branch (remote)`, or `Choose branch`. Connected + branches pending + no local refs replaces that label with `Loading branches…`; cached/nonempty refs prevent this replacement. PR context chip is separate from workspace selection and survives reset. Hero is omitted when available vertical space is below 180; keyboard reduces space. Environment title truncates to one line, offline dot appears when disconnected. Project/machine/workspace controls lock during submit or content import; non-Git disables workspace controls. Send guards model, branch, Git-known state, permission, attachments/context and clone/import readiness; offline/uploading queues. |
| [NewTaskEnvironmentPickerRouteScreen](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx#L185) | 0/1/many environment rows, selected checkmark, last-row border, one-line names/two-line status, connection state/expanded diagnosis/retry time/reconnect button. Normal environment list requires a saved connection and repository/name eligibility, and deduplicates environment IDs. Scratch list requires connected scratch-capable environments. Android custom header/28px card; iOS native header/16px card. Tapping selected machine is a no-op; successful move dismisses picker. |
| `cycleHost` in draft screen | Registered only with >1 eligible environment. Wraps through the same environment list; no-op while interaction locked; uses pending scratch destination if a switch is in flight. `dispatchHardwareKeyboardCommand("cycleHost")` exercises app command handling but is not hardware-key proof. |
| [NewTaskBranchPickerRouteScreen / BranchPickerScreen](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx#L238) | Worktree title `Base branch` plus Start from remote toggle; local title `Branch`, no toggle. Branch choice and remote toggle rewrite workspace without PR reference. Zero refs splits loading/error-with-retry/search-no-matches/empty. Nonempty list splits selected/current/worktree/remote/default badge, pagination spinner, disabled checkout. Search is debounced 150ms. Local branch checkout can alert on failure; new-worktree base selection does not checkout. Android search/header vs iOS native search/bottom toolbar. These reused list branches have no layout change in this PR. |
| [NewTaskRouteScreen](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx#L151) | Picker over a draft calls `setProject`, including same-repository/different-project selection; outside draft starts a new route. Grouped project row shows workspace count when >1, otherwise path; search may empty list. Catalog zero states cover loading saved connections/no connections/unavailable or unsupported/connecting/no projects. Scratch start, add project and add environment are separate paths. Android and iOS row/header treatment differs. These paths were not changed; select-project reset is the relevant guard. |
| [NewTaskThreadSettingsRouteScreen](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx#L1567) | Reads destination model/provider, prompt/options/runtime; no workspace rendering and no machine-switch callback. Provider availability can change destination composer to the no-model fallback. |
| [CommandPalette](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/keyboard/CommandPalette.tsx#L272) | Reads active draft's project environment for triage target; it does not implement a second draft-machine switch. Palette/new-task navigation reaches the ordinary draft/project path. Settings manages connections, not an alternative retarget operation. |
| [RootStackLayout / draft route](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/Stack.tsx#L501) and [flow state](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/new-task-flow-state.ts#L100) | No draft/inactive, active draft, draft covered by another thread, and new route key. A restored route is pending until draft/shell readiness, opens from stamped environment+project, or returns to project picker if draft/project is gone. Persisted-draft reopen exercises changed store state outliving route flow state. |
| [Thread-list pending draft row](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/thread-list-v2-items.tsx#L388) | Not a `useNewTaskFlow` consumer, but reads changed draft's project stamp and branch; draft title derives from text or attachment count (1 vs plural). Reopening targets draft ID. List row does not show the composer's `From PR` workspace label. |

Other exported functions in `new-task-project-selection.ts` and all other composer-store APIs are unchanged. No web/desktop UI, RPC schema, provider adapter or scheduling entry point changes here. One model/provider usable on both servers is sufficient; six-provider Cartesian duplication is not warranted.

## Seed recipe and evidence rules

Use only disposable server homes and pooled dev-client state. Do not run against the live T3 home. This audit seeds nothing.

- **Catalog S:** A/P-A and B/P-B have different filesystem roots, but BOTH hydrated `repositoryIdentity` values produce the same grouping key. Make A's checkout branch `main` and B's `local-feature` so reset is obvious; destination defaults should be `local`. Keep an available model and source-control-write/orchestration permissions on both. Use an existing accessible PR with a nonempty head branch and canonical provider URL. The description's example is [#24707](https://github.com/pingdotgg/t3code/pull/24707); choose a current accessible PR if it cannot supply this data. Ensure `Work on PR` starts a NEW thread, rather than continuing an existing linked thread. Start-a-new-thread/AI helper actions converge on the same draft creation function.
- **Create/register:** Add project in the app, or isolated-server `POST /api/projects/mutate` with `{type:"project.create", commandId:<uuid>, projectId:<uuid>, title:<title>, workspaceRoot:<existing disposable path>}`. The concrete HTTP/auth pattern is in [measure-renders.mts](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/scripts/measure-renders.mts#L122). Read `GET /api/orchestration/shell` to record actual identity, project IDs, roots, and catalog ordering. Do not assume two remotes or folder names imply equal identity.
- **Catalog U-basename:** For the negative phase on B, unregister its known-match project record from disposable state (preserve its folder for the positive phase), then register an ordinary Git repo with no remote, same basename as P-A, and different title. This gives Git capability with identity null; [RepositoryIdentityResolver](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/server/src/project/RepositoryIdentityResolver.ts#L193) returns null without a usable remote. Confirm null from the shell, rather than racing enrichment. If P-B remains, it wins the match and this fixture is invalid.
- **Catalog U-title:** Same as above, but use different basename and exactly matching project title. This reaches the next fallback, not the basename fallback.
- **Catalog D:** B contains only a project with a DIFFERENT known grouping key, but matching basename/title. B should disappear from A's eligible environment list. A generic Git repo with no remote is NOT a known-mismatch fixture. If a second known match remains on B, B remains eligible and selects that match.
- **Catalog M:** B contains at least two known-equal-identity projects; make titles/paths/checkouts distinct. Record real shell order and expect the first identity match, not an arbitrary clone. Can add an unknown/mismatched same-name project ahead of both to show identity priority.
- **Route:** Normal seed path is Pulls → chosen PR → Work on PR → `Thread` with `{draft:{draftId:<new-task-key>}}`. The producer is [usePullRequestThreads](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/pull-requests/usePullRequestThreads.ts#L98). It writes `{mode:"worktree", branch:pr.headBranch, worktreePath:null, startFromOrigin:false, pullRequestReference:pr.url}` and inserts PR context. Open environment/branch/project pickers from this same draft, not as unrelated standalone deep links. Actual routes are `NewTaskEnvironment`, `NewTaskBranch`, `NewTaskProject`, `NewTaskThreadSettings`.
- **Harness:** No server RPC can seed device-local draft workspace fields. For H01/H02, seed the app's exported `createNewTaskDraft`, `updateComposerDraftSettings`, `setComposerDraftText` and context insertion functions through a dev inspector driver, then open the actual `Thread` draft route. `appAtomRegistry` plus `getComposerDraftSnapshot` supplies before/after observations. The repository's [inspector connection example](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/scripts/measure-renders.mts#L78) provides transport, not a prebuilt draft-seeding command. A capturer must build the scratch driver/module lookup; no global draft setter is assumed. Alternatively, with the disposable app stopped, seed its `Paths.document/composer-drafts/drafts.json`, schemaVersion 1, then reopen. Label these runs **HARNESS — RESTORED DRAFT SHAPE**; do not imply Work on PR normally creates them. Do not rewrite workspace state during the observed transition.
- **Per-run output:** Save recordings, endpoint PNGs, driver and raw sanitized transcripts to a NEW timestamped directory below the lead's assigned scratch root. Include build revision, device resolution/DPR, platform, theme, keyboard state, catalog phase, draft ID, source/destination project+environment, grouping keys, and steps. Keep credentials/pairing URLs out of uploaded evidence. Do not delete raw evidence before upload.
- **Time/interaction:** Every row below is `recording`: start before the action, show its actual trigger, and continue through settled state. Still images alone cannot satisfy these rows. Await a real UI/store/query milestone; do not declare success after an arbitrary delay. Clips can cover several rows only when the row-to-timestamp mapping and fresh-draft resets are explicit.
- **Before/after:** For rows marked Δ, capture the same data and steps on merge-base and head; label both. Guard rows marked `=` need head recordings and may be combined with that comparison session. H01/H02 need both revisions as their retarget behavior changes. Store/RPC transcripts are supplemental to recordings.

## Fixture matrix

`Δ` = diff changes this state/transition, `=` = required boundary or retention guard. `P/L` = Android Pixel 9 portrait, light, 1080×2424 physical px / DPR 2.625 if the device matches the PR description; record actual values if different. All rows default to P/L. Every listed required row currently has status **PENDING — no capture**.

| ID / named fixture | Diff | Data and app seed | Expected visible states / interaction | Minimum evidence | Status |
|---|---|---|---|---|---|
| F01 — PR machine round trip | Δ | Catalog S; new Work on PR draft with visible PR chip and prompt, no attachments. | Record initial A → environment picker → B → picker → A. Head keeps `New worktree · From PR #N` and chip/text throughout settled endpoints. Base resets to destination current checkout while chip remains. Also tap the already-selected environment once: no reset. | **recording**, P/L before+after; same row also P/dark head. Capture picker rows/checkmarks and branch-loading transient when present. | PENDING |
| F02 — keyboard machine cycle | Δ | S, fresh PR draft, >1 eligible machine. | Invoke `cycleHost` twice; B then A must match F01. Preserve context/text. | **recording**, P/L before+after. App `dispatchHardwareKeyboardCommand("cycleHost")` is an acceptable labelled command-harness run; only claim hardware-key coverage if an actual key event is injected and observed. | PENDING |
| F03 — unknown identity / basename match | = | U-basename on B, known P-A on A; fresh PR draft. | B appears by same basename; switching selects B but drops workspace, so destination-local checkout/default branch replaces PR row, while PR context chip/text remain. Returning to A does not resurrect the cleared PR workspace. | **recording**, P/L head + shell identity transcript. | PENDING |
| F04 — unknown identity / title-only match | = | U-title; ensure basenames differ and titles match, no known-match project on B. | Same reset as F03, reached through title fallback. | **recording**, P/L head + shell identities/paths. Can follow F03 in one explicitly reset clip. | PENDING |
| F05 — known different repository omitted | = | D, fresh A PR draft. | Open environment picker: B absent even with matching folder/title. With only A eligible, machine-cycle has no next destination. Draft remains unchanged. | **recording**, P/L head + shell keys. Do not claim this exercised retarget-to-mismatch. | PENDING |
| F06 — explicit project choice resets | = | S plus a second selectable project; Settings → General → Project grouping → Keep separate if grouping hides it. Fresh PR draft. | Tap project name → select another project (include a different project with equal repository identity). Uses `setProject`, drops PR workspace and keeps PR chip/text. Reselecting the exact same project is a no-op because stamp matches. | **recording**, P/L head. Show target project/path, not just title. | PENDING |
| F07 — ordinary new worktree resets | = | S; ordinary new task, `mode:worktree`, chosen base branch, NO PR reference/context. | Switch A → B; destination uses its default workspace, not source's branch/worktree choice. | **recording**, P/L head. | PENDING |
| F08 — explicit local checkout resets | = | S; ordinary local-mode draft with explicit branch and null path. | Switch A → B; destination current checkout/default selection replaces explicit source branch. | **recording**, P/L head. Can share F07 clip with a fresh seed. | PENDING |
| F09 — existing worktree path resets | = | S; local-mode draft selecting a real secondary A worktree (`worktreePath` nonnull), no PR. | Initial label `Current worktree`; after B, source path is not reused and row becomes destination default/current checkout. | **recording**, P/L head + store path transcript. | PENDING |
| F10 — switch to current checkout and back | Δ + = | Fresh F01 draft moved to B. | Toggle workspace to local: PR workspace selection clears, label becomes `Current checkout` or `Current worktree` for B's real checkout; chip remains context. Toggle back to worktree: normal base branch label, no automatic PR restoration. | **recording**, P/L head; include fresh Work on PR reset before next row. | PENDING |
| F11 — choose another base branch | Δ + = | Fresh PR draft moved to B; at least two B branch refs. | Open Base branch; choose a B branch. Row changes from `From PR` to `From <branch>`; chip remains; switching back A does not restore PR selection. | **recording**, P/L head, including picker selection. | PENDING |
| F12 — change Start from remote | Δ + = | Fresh PR draft moved to B; Base branch picker, initial false. | Toggle Start from remote. Current code clears PR reference and shows normal `From <branch> (remote)`; toggling off gives normal local-base label, not PR restoration. This is a separate clearing interaction from choosing a branch. | **recording**, P/L head. | PENDING |
| F13 — leave / reopen / restore moved draft | Δ | S; PR draft moved to B with recognizable prompt. | Leave to thread list: draft belongs to B's project and retains head branch. Reopen its draft row: same draft, B, `From PR`, chip and text. Flush/persist and relaunch once to prove route restoration, not just retained provider memory; include transient hydration. | **recording**, P/L before+after + sanitized draft ID/project stamp transcript. | PENDING |
| F14 — offline / reconnect / refs settle | Δ | S hydrated/cache-warm; move PR draft to B, then stop/disconnect only disposable B and reconnect it. Separate cold-refs subrun if warm cache never loads. | Offline dot/status and reconnecting/error presentation are honest; PR selection stays through disconnect and reconnect. With connected + pending + zero refs, label may temporarily be `Loading branches…`; settled row returns to `From PR`, not current checkout. Cached refs skip loading label. | **recording**, P/L before+after; show offline, retry/reconnect and settled milestones. | PENDING |
| F15 — several destination matches | Δ | M; record real ordered projects and distinct roots/titles; one PR draft on A. | B appears once. Switch chooses first equal-identity project in catalog order, preserves PR worktree, and does not choose earlier unknown/different-name candidate. Grouped picker can show plural workspace count. | **recording**, P/L before+after + ordered shell/project stamp transcript. | PENDING |
| F16 — content/attachments survive move | Δ + = | S PR draft, text+PR context, one image and one small file through normal app pickers. Upload on A first; both device files must still exist. | Move to B and back; prompt, chip, attachment previews/names/counts survive along with PR row. A-upload stamps must not be reused on B; allow destination reupload without losing bytes/context. | **recording**, P/L head + app upload/store transcript. Existing target-stamped and unstamped variants are store-only branches; see branch coverage below. | PENDING |
| F17 — destination has no selected model | Δ | S identity shape, but disposable B has no usable provider/model; clear device/draft fallback selections in a labelled seed if needed and confirm `selectedModel === null`. | Move draft to B: saved-draft fallback editor and machine control; workspace row is absent by design. Return to A: preserved PR row/chip/text reappear. A visually similar unavailable model with a nonnull selection is not this row. | **recording**, P/L head + model/workspace snapshot; label any injected model-state seed. | PENDING |
| F18 — scratch machine switch | = | New task → No project; both environments support connected scratch folders. | Scratch hero/no workspace row; switch uses scratch creation and destination project, not PR-preservation branch. Show successful switch and pending-disabled rows if observable. No PR is manufactured in scratch via normal UI. | **recording**, P/L head. Scratch failure/stale-completion variants are unchanged logic and not additional required captures. | PENDING |
| F19 — non-Git destination | = | Negative catalog phase: B's only eligible same-basename/title project is a real non-Git directory, identity null; A starts with fresh PR draft. | B matches weakly, PR workspace resets; non-Git status resolves to local/no branch and disabled workspace controls; PR chip remains context. Observe unknown-Git → known-non-Git settling if present. | **recording**, P/L head + shell/isRepo transcript. This can extend F03 but cannot replace its Git-capable unknown-identity case. | PENDING |
| F20 — Send from destination uses PR | Δ | S; fresh PR draft moved to B; harmless prompt and disposable/replay provider. Source and destination roots deliberately different. | Send on B; destination thread prepares its own PR worktree and uses the PR head. The old current-checkout outcome on base is the bug. UI transition to thread must correspond to destination worktree, not just a retained label. | **recording**, P/L before+after + sanitized queued creation / server thread-workspace / Git HEAD evidence. Must show destination environment/project, PR reference, no source absolute path, and actual B worktree/head. No need to duplicate every provider. | PENDING |
| H01 — restored PR with machine-local path | Δ | **HARNESS**: seed new-task draft with normal PR context; workspace `{mode:worktree, branch:<head>, worktreePath:<real A-only path>, startFromOrigin:false, pullRequestReference:<PR URL>}`. Same-identity S. | Switch A → B → A; keeps mode/branch/reference/explicit false, clears path to null on first move and never resurrects source path. Both A/B UI can say `New worktree`, so visuals alone cannot prove clearing. | **recording**, P/L before+after + exact workspace/project snapshots before/after. | PENDING |
| H02 — restored local mode with PR reference | Δ | **HARNESS**: same as H01 but mode local; use A-only path and a branch name valid/known on B. | Code preserves local mode, branch, reference but clears path, so `Current worktree` can become `Current checkout`; it does NOT normalize to `New worktree`. PR chip stays separate. The local send path omits PR reference. Document actual behavior; do not assert normal Work on PR creates this shape. | **recording**, P/L before+after + workspace snapshots (include omitted PR in built creation if inspected). | PENDING |

## Minimum viewport × theme × row set

- **Required:** P/L × F01–F20, H01–H02. For Δ rows, both revisions as specified; guard rows need head only. This is 22 semantic rows, not 22 full independent walkthroughs: clearly labelled subruns may share recordings.
- **Required theme spot check:** Pixel 9 portrait/dark × F01 at head. Use a long destination label and long project title in this run to exercise their one-line truncation; retain readable PR/workspace controls. No style changed, so duplicating all guard rows in dark is unnecessary.
- **Optional if cheap:** iPhone 17 Pro portrait/light × F01 before+after. Covers iOS native picker header, safe areas, navigation and keyboard dismissal; record actual viewport. It is not a prerequisite for the mobile-only logic proof, but the PR's existing iPhone verification claim should be backed by this or removed/qualified. iOS dark, tablet, landscape/split column, and every keyboard height are not independent minimum rows for this data-preservation diff. If a capture exposes clipping, add the failing geometry before calling proof sufficient.
- **No still-only substitutions:** Every minimum row contains interaction or time. PNG endpoints are useful comparison aids, not replacements for recordings. P/dark × F01 is also a recording.

## Remaining branches and why they do not multiply the minimum captures

| Branch family | Disposition / seeding |
|---|---|
| Option false/absent, no workspace, undefined PR | Covered by F03/F04/F06/F07/F08/F09 and source tests. Untouched default selection naturally has undefined workspace; can include in F07 setup. Full contextless empty draft is visually the ordinary new-task composer, not another PR fixture. |
| Exact same stamp; same environment; null/non-new-task key; absent draft/stamp/createdAt | Same project/environment no-op covered F01/F06. Null/non-new-task key and missing-store stamp create empty drafts, reachable at fresh creation or via a labelled store harness. No prior PR workspace exists to preserve, so no extra screenshot claim is useful. Observe source logic or unit tests; do not add artificial blank-draft captures to F01 proof. |
| `isSameRepository` null left/right, one identity unknown, both unknown | False by code. Known→unknown is F03/F04/F19; both-unknown/unknown→known can be additional parameterized helper/store checks, same visible reset as F03. Equal origin grouping key despite different canonical keys follows contracts helper; verify real shell keys, don't infer a mismatch from canonical key alone. |
| First-project fallback / empty target projects | Pure matcher returns first/null. A known mismatch selected by forced `selectEnvironment` is a **labelled provider harness**, since normal eligibility hides it (F05). Empty target is likewise an injected catalog/removal race: no matching project means no retarget; draft key remains, project key null, ordinary selected-project fallback or null follows. Not a real selectable empty environment in the normal picker. Cannot claim natural UI coverage with the two-server S fixture. |
| PR reference empty/invalid; branch null; explicit startFromOrigin undefined/true; followsCurrentCheckout | Schema permits these restored shapes. Predicate preserves any defined string, even empty or invalid, and resolver may show a normal branch/Choose branch instead of From PR; null branch can fill from B refs. These are **optional labelled recovery harness** variants, not extra supported Work on PR flows. H01/H02 cover the meaningful machine-local-path and mode distinctions. Do not treat undefined and empty PR strings as the same store predicate. |
| Attachment count 0/1/many; upload stamp A/B/undefined; no prompt | 0 in F01; plural visible attachment case in F16. Store retains target-stamped and unstamped attachments, strips only another-environment stamps. Existing store test explicitly exercises A/B stamps. Attachment-only title singular/plural and upload error/cap/permission UI were not changed; no Cartesian capture set is needed. A screenshot cannot prove stamp ownership; use transcript/tests. |
| Branch list empty/error/search/pagination/current/worktree/remote/default; local checkout failure | Complete presentation branches listed above. F11/F12 hit worktree branch-list controls, F14 hits relevant loading/error/reconnect. The PR does not change branch list rendering, search debounce, badge priority, or checkout command; do not require a full branch-picker visual regression suite. If cold-query error cannot be induced safely with a real backend, an extra capture must be labelled a query-state harness. |
| Zero/one/many environments, long labels, diagnosis expansion/retry time | One and many covered F05/F01/F15; zero occurs with no selectable/saved project or artificial null source (ordinary blank/picker path). Offline/expanded diagnosis/retry are covered when visible in F14; their exact text/timer formatting is unchanged. Long-label dark F01 is the one presentation stress check. |
| No project / no connections / pending hydration / missing route draft / covered inactive draft | Provider props and route resolver inventoried above. F13 covers valid restore and transient readiness. Missing/deleted project returns picker, not PR retarget; no-project/catalog error screens are unchanged. Opening another thread above draft and returning can extend F13; no new capture matrix dimension. |
| Scratch pending/error/interrupted/superseded switch | Source-only unchanged async guard family; F18 covers real scratch alternate path. For deterministic failures or simultaneous completion ordering a labelled scratch-command harness is required. No new logic for this family in diff. |
| Theme/platform/width/keyboard, import/clone/submit locks, model availability | All styling branches reused. P/dark and optional iOS F01 sample presentation. F17 covers no-model branch; disabled/unavailable-but-nonnull model, clone banner, share import, runtime/provider options and permission alert UIs are unchanged. Avoid a model×provider×theme×width product for this fix. |

## Current source-test coverage and limits

- [Composer-store changed regression test](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/state/use-composer-drafts.test.ts#L1893) checks ordinary branch drop even with option true, PR A→B→A preservation, and omitted-option reset. Its PR path starts null; it does not prove H01/H02, the live UI caller passing the option, or Send on B.
- [Project-selection tests](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/features/threads/new-task-project-selection.test.ts#L78) check identity match and unknown/different mismatch predicates; existing cases exercise title fallback. No new browser/native integration test was added.
- [Existing upload-stamp test](https://github.com/pingdotgg/t3code/blob/63d02cd849afcc565380bb56b924d2bdb34fecc5/apps/mobile/src/state/use-composer-drafts.test.ts#L2121) checks foreign upload removal versus target upload retention; it does not itself cover simultaneous PR preservation.
- The PR reports 91/91 tests and `check:changed` passing. This pass did not independently rerun checks, inspect CI, capture devices, or validate author recordings. These are deliberately outside this fixture-derivation pass.

FRICTION: none

VERDICT: INSUFFICIENT F01–F20 and H01–H02 recordings; F01 dark spot check; H01/H02 store transcripts and F20 destination-worktree runtime proof
