# Proof audit, pass 1

Findings, in order of impact:

1. **The “no rendered surface” claim is contradicted by an existing mobile path.** The command palette enables “Triage an issue” from triage capability and permission, without checking `scratchWorkspaceRoot`. Mobile `useStartTriage` calls `projects.ensureScratch` first and displays the returned `Error.message` in a native “Could not start triage” alert. An ordinary authorized connection to an environment whose T3 home is inside a repository therefore renders the new diagnostic. No UI source changed, but rendered content does change. R4 needs before/after client captures; no new UI test is needed for this text.
2. **The supplied runtime transcript is stale and incomplete for the affected server outcomes.** Its before revision is [33f85846be](https://github.com/pingdotgg/t3code/commit/33f85846be), rather than the requested merge-base, and its after revision is [92e60e6a4b](https://github.com/pingdotgg/t3code/commit/92e60e6a4bb0a774a40cf96b78b85583543576f3), rather than the reviewed head. The after text says “otherwise set T3CODE_HOME”; the head says “otherwise point T3CODE_HOME (or --base-dir)”. It shows one WARN, but no bounded repeat-call sequence proving the count stays one, no config response, and neither RPC nor MCP response. It is useful historical evidence for R1, not adequate current proof for any affected row.
3. **The PR's test description also describes an older revision.** At the head there are 12 cases in `ManagedProjectFolders.test.ts`, including one extended existing checkout case; the body says 14 and describes two new standalone cases that have since been folded into it. The current case checks denial, both paths in the error, and one warning after repeated access. It does not compare the whole warning with the error or exercise transport/client rendering. Reuse this lowest-layer test for R1–R4; capture the actual transport output rather than add duplicate wrapper tests. This audit inspected tests but did not execute them or reproduce the claimed fail-first run.

Reviewed [#32342](https://git.cafe/pingdotgg/t3code/pull/32342), branch `fix/issue-32339`, pinned head [82d8f232de](https://github.com/pingdotgg/t3code/commit/82d8f232de4243c80c9fdba9244cd164eb8fbc58), against requested merge-base [7389423507](https://github.com/pingdotgg/t3code/commit/7389423507aff8f931fdd41fd0b752589d2d810e). GitCafe metadata agreed with these revisions when read. The complete pinned diff contains exactly the three specified server files. Local `main` is not the requested merge-base, so the supplied SHA range, rather than local `main`, was used. No code changes, server starts, pushes, comments, or client automation were performed.

The matrix below collapses identical external outcomes: outside-repository, missing-Git, and null detection all share R5. R1–R4 share one filesystem fixture but have distinct observer outputs. Repeated access and restart are phases of the same fixture, not additional permutations. Empty `git init`, a committed checkout, home equal to the root, and a home nested under the root all take the same detected-Git branch. Spaces, symlink/canonical path differences, macOS path prefixes, and Windows separators are path-data variants, not separate outcomes; use Git's returned root as the expected repository path. A linked worktree also takes this branch, though `.git` may be a file. Do not use a real home or live T3 state for any recipe.

Let **D** be `/tmp/lead-32342-r1/proof-audit-1`. The recipes are instructions for a later authorized runtime pass; none were executed here. Give before and after separate fresh databases under D. Use `T3CODE_DEV_SEED=0 vp run dev --home-dir <fixture-home>` from an already prepared checkout at the exact revision; record the resolved base directory and revision. Read the actual ports from the runner output. Use an authorized RPC/MCP driver and redact credentials and pairing URLs. The RPC wire method is `projects.ensureScratch`, with `{}` input. MCP uses `/mcp`, a credential with orchestration/thread-start permissions, and `tools/call` for `t3_thread_launch` with `{ "scratch": true, "title": "Scratch diagnostic proof" }`; omit `message`, `projectId`, and `workspaceStrategy`. The denied launch fails before provider/model selection, so no paid agent turn is necessary.

| Row / named fixture | Required data and observable outcome: merge-base → head | Diff impact | Minimum appropriate proof and why | How to put the real server into the row | Existing evidence / status |
| --- | --- | --- | --- | --- | --- |
| **R1 — repository-denied / log and config** | Fresh empty Git worktree R and T3 home H below it, Git installed, same server instance, repeated config/ensure calls. `scratchRoot=None`, config omits `scratchWorkspaceRoot`, `isScratchProject=false`, and `folderForThread=None` on both revisions. Before: no scratch diagnostic. After: one WARN with H, Git's R, remediation including `--base-dir`, and restart advice. Cache reuses denial across ordinary repeated callers. | **Affected:** new diagnostic and cached reason; availability unchanged. | Existing checkout behavior test plus a short **before/after real-server transcript**. Trigger `server.getConfig` (or its config subscription) explicitly, then repeat config and ensure calls and show the bounded WARN count is one. Record omitted config field. Startup alone does not establish that the lazy probe ran. No screenshot for log/config output. | Create e.g. D/before-repo and D/after-repo with `git init`; H is each repo's `t3home`. Launch each pinned revision with explicit `--home-dir H` and no seeding, then drive config and ensure. Empty and committed repositories need not both be proven. Moving the fixture's `.git` aside without restarting leaves cached denial; restarting under the now-outside home transitions to R5. Never alter a real repository. | **STALE / PARTIAL.** Body's older transcript shows a single WARN in the right kind of fixture. Wrong revisions and wording; no call sequence, count, or config response. Current source test covers paths/count; its reported run is stale. |
| **R2 — repository-denied / RPC error** | Same H/R as R1 and authorized `projects.ensureScratch({})`. Before: `OrchestrationDispatchCommandError.message` is the generic unavailable sentence. After: same wrapper with the full H/R diagnostic, matching the WARN. No project/folder creation occurs. | **Affected:** outward RPC message; RPC schema unchanged. | Reuse the checkout test, plus a **before/after RPC response capture in a short runtime transcript**. Preserve method, input, error tag, and full decoded message; compare with R1's warning. A screenshot cannot prove wire serialization. No duplicate RPC wrapper test is required. | Use R1's running servers and an authorized driver. Call twice on head to also contribute to R1's count proof. This is reachable even when UI normally hides the scratch action. | **MISSING.** No `projects.ensureScratch` response in the PR body. |
| **R3 — repository-denied / MCP result** | Same H/R, authorized MCP context, valid scratch-only launch input. Before: generic `OrchestratorMcpFailure`; after: `code=orchestration_error` and full H/R diagnostic. The advertised tool uses `failureMode=return`; the standard MCP registration emits `isError=true` with JSON error content in a text block. Capture the actual response rather than assume a `structuredContent.error` shape. | **Affected:** outward MCP message; tool schema unchanged. | Reuse the checkout test, plus a **before/after MCP tools/call response capture in a short runtime transcript**. Retain `isError`, decoded error code/message, and content framing; show equality with R1/R2 after normal JSON decoding. A screenshot or startup WARN cannot prove this transport. | Use R1's servers and an authorized `/mcp` driver with the scratch-only input above. An invalid credential, absent start permission, or `scratch` combined with project/strategy hits earlier unchanged guards and does not prove this row. Denial occurs before starting any agent. | **MISSING.** Body only asserts pass-through; it supplies no MCP response. |
| **R4 — repository-denied / mobile triage alert** | Mobile client paired with the R1 environment, standard permissions and triage capability. Open command palette → “Triage an issue”. Before: “Could not start triage” alert contains generic unavailable text plus the CLI fallback. After: same alert contains the new H/R diagnostic plus that fallback. No stale config or synthetic client error is needed. Other scratch error toasts/alerts also consume the RPC message if reached, but normal scratch entry points remain hidden by omitted config. | **Affected rendered content, despite no UI-file diff.** This contradicts the PR's no-visible-surface premise. | Shared checkout test and R2's response transcript, plus **before/after real mobile screenshots** with the complete alert text readable. This proves the existing render path and whether the longer message is usable. No copy-only UI test. No need to demand screenshots of unchanged hidden scratch entry points across every client. | Connect a real test mobile client to each R1 server and choose “Triage an issue” from the palette. The command is gated on triage capability/permission, not scratch availability, and calls scratch before preparing triage. No provider turn or successful triage preparation is needed. Read-only audit did not run this flow. | **CONTRADICTED / MISSING.** PR says no rendered surface and supplies no captures. Source trace establishes this reachable changed-content path; screenshots remain uncollected. |
| **R5 — scratch-available control** | Variant A: existing writable H outside every Git worktree, Git installed, no unsupported VCS override. Variant B: Git genuinely absent from server executable resolution (H can even be inside a repository). Both resolve `scratchRoot=H/scratch`, include config root, emit no new denial WARN, and allow ensure/bootstrap. Matching scratch project returns true, unrelated/missing/read-failed project false; root-strategy thread gets a folder only for the scratch project. Git nonzero/false detection also returns null and lands here, not R6. | **Control:** probe representation changed, observable availability/success behavior retained. The `repositoryRoot=null` branch replaces old `isRepository=false`. | Existing outside-home creation/concurrency/folder cases and missing-Git case are the appropriate service tests. A head config/ensure **runtime transcript** is a useful control; no changed-output before/after requirement or screenshot for this row. Do not add duplicate surface tests. | A: launch with H under D/outside-home after confirming no ancestor repository. B: launch the already prepared server in an isolated runtime with Git absent from all resolved executable paths; retain Node/package-manager tooling or use the built server. Ordinary `PATH` changes may be insufficient if executable resolution restores a shell environment. The current service test injects a real-shaped ENOENT at `VcsProcess`. Do not uninstall host Git. | **CONTROL COVERAGE IN SOURCE; runtime partial.** PR reports an older UI control outside a repo, not a current response transcript. Missing-Git is covered by an existing test; no missing-Git runtime shown. Not a new diagnostic-proof blocker. |
| **R6 — unknown probe failure / generic denial** | Existing H, detection fails for a reason other than missing Git, with no interrupt. Typed failures and defects are swallowed into unavailable. Both revisions: no scratch root, false scratch classification, no allocated scratch folder, no new repository-denial WARN, generic unavailable message through RPC and MCP. No repository-specific remediation may be invented. | **Control:** fallback cache now holds an error object instead of None; visible generic behavior retained. Internal error payload gains baseDir, and GitWorkflow's diagnostic operation/span is renamed. | A focused service failure test or harness transcript is the smallest protection for this preserved branch; a **runtime RPC/MCP transcript** is the right optional control evidence, not a screenshot. Existing checkout test does not cover it. Do not require a new UI copy test or treat this unchanged branch as a changed-message proof blocker. | A stock-server fixture is possible: put `{ "vcs": { "kind": "jj" } }` in H/.t3code/vcs.json before any probe. `VcsProjectConfig` accepts jj, while registry registers only Git, so detection fails with unsupported operation, which GitWorkflow wraps. Restart before each trial because the outer probe caches failures forever. Alternatively an isolated failing process adapter can force EACCES/timeout. A Git command merely exiting nonzero is **not** this fixture; the detector treats that as null/R5. | **UNPROVEN CONTROL.** No focused generic-probe-failure case or runtime response shown in the audited test/body. This is a preservation gap, not evidence that the message changed here. |
| **R7 — interrupted probe / retry** | First detection is held in progress, caller is interrupted (including an interrupt mixed with failures), and a later caller retries. Both revisions preserve interruption rather than return generic unavailable, invalidate/abandon the probe, and permit fresh detection. Subsequent success follows R1 or R5. A later interrupted run is an exception to ordinary completed-cache reuse, not a new normal once-only phase. | **Control:** `onInterrupt` moves to scratchProbe so both root and ensure retain invalidation; intended external behavior unchanged. | A deterministic **Effect service harness/test transcript** using Deferred/Queue milestones, fiber interruption, and a later completed call is appropriate. No sleeps, timeouts, repeated polling, screenshots, or UI tests. There is no completed RPC/MCP error response to capture for an interrupted invocation. | No stock flag/fixture reliably targets the exact internal interruption branch. A driver abort while an isolated Git adapter is blocked could reach it, but requires lifecycle control/instrumentation and must show that no other waiter kept the shared probe alive. Prefer the lowest-layer deterministic harness. Do not call a 5-second Git timeout an interruption: it is R6. Do not kill the server to claim retry proof. | **UNPROVEN CONTROL.** The existing cancellation case is for named-project creation, not this probe. Neither probe-interruption proof nor a retry transcript is supplied. Not a new message-proof blocker. |

All affected rows R1–R4 can use the same minimal checkout test; “test plus transcript per affected row” does not require four copies of the test or four servers. One before server and one after server, each driven through config, RPC, MCP, and mobile triage, can produce the entire evidence bundle. Capture the exact requested revisions and avoid publishing credentials or unrelated data.

Caller and branch inventory supporting the matrix:

- **Changed GitWorkflow method:** [repositoryRoot](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/git/GitWorkflowService.ts#L274) has one production caller, [ManagedProjectFolders' cached probe](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/project/ManagedProjectFolders.ts#L240). It maps a Git handle to its root, null/non-Git to null, and wraps typed detection errors as GitManagerError; interruption/defects propagate to the outer cause handler. The operation and tracing span rename from isRepository to repositoryRoot are observable instrumentation changes on these same probe paths; no new fixture is needed solely for the label.
- **Root/identity branches:** Root present → R1–R4; root null → R5; missing Git → R5; noninterrupt failure/defect → R6; any interrupt → R7. [GitVcsDriver.detectRepository](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/vcs/GitVcsDriver.ts#L621) already catches missing executable and returns null, so a normal missing-Git run does not necessarily exercise ManagedProjectFolders' additional `isGitMissing` defense. The non-Git-handle mapping is defensive: the current [registry](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/vcs/VcsDriverRegistry.ts#L63) registers only Git; selecting unsupported jj fails into R6 rather than returning a jj handle.
- **scratchRoot direct readers:** [ws config loading](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/ws.ts#L1922), reached by `server.getConfig` and config subscription; [ScratchThreadPromotion.promote](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/project/ScratchThreadPromotion.ts#L71), which retains its existing non-scratch promotion rejection; internal `isScratchProject` and `folderForThread`. All share one cached probe.
- **ensureScratchProject direct callers:** [projectsEnsureScratch RPC](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/ws.ts#L2922) wraps the service message in OrchestrationDispatchCommandError; [MCP t3_thread_launch scratch path](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/mcp/toolkits/project/handlers.ts#L118) wraps it in OrchestratorMcpFailure with code orchestration_error. The class ScratchUnavailableError itself is server-local, not a newly added wire contract. Allowed roots continue through unchanged mkdir/bootstrap/icon logic and its existing ScratchFolderError/ScratchProjectError behavior; those downstream failures and named-project scaffolding are outside this diff.
- **isScratchProject direct callers:** [ThreadLaunchService.launch](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/orchestration-v2/ThreadLaunchService.ts#L841) normalizes scratch worktree strategy to root; [ScheduledTaskService save](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/scheduledTasks/ScheduledTaskService.ts#L1403) performs the same normalization; [ScratchThreadPromotion](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/project/ScratchThreadPromotion.ts#L72) classifies the source and candidate threads. Absent root stays false; present root compares resolved project paths, and absent/read-failed projects remain false. No new diagnostic is returned by these bool-only consumers.
- **folderForThread direct caller:** [ThreadLaunchService.launch](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/orchestration-v2/ThreadLaunchService.ts#L909). No root or a non-scratch project yields None; a matching scratch project allocates its ordinary per-thread folder. Both decisions remain unchanged.
- **Shared client RPC path:** [projectCommands.openScratch](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/packages/client-runtime/src/state/projectCommands.ts#L172) forwards ensure failures. [Web useScratchProject](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/web/src/hooks/useScratchProject.ts#L22) displays their messages in existing error toasts. Its consumers include DraftHeroHeadline, NoProjectsHero, CommandPalette, chat keybindings, draft environment routing, prompt links, and desktop web links. Web triage also uses the command, but checks scratch availability before starting. Normal scratch entry points continue to gate on the unchanged omitted/present config root. Web toast text can still inherit the new message on a failed/stale in-flight path; no new renderer or layout was introduced.
- **Mobile client:** NewTaskRouteScreen, new-task-flow-provider, prompt links, and triage all use the shared command and have existing failure alerts. Most normal scratch choice/environment routing uses availableScratchWorkspaceRoot. The concrete ungated path is [CommandPalette canTriage](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/mobile/src/features/keyboard/CommandPalette.tsx#L275) → [triage action](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/mobile/src/features/keyboard/CommandPalette.tsx#L350) → [useStartTriage openScratch/failure](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/mobile/src/features/triage/useStartTriage.ts#L68) → [native alert](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/mobile/src/features/triage/useStartTriage.ts#L35). Server [triage capability](https://github.com/pingdotgg/t3code/blob/82d8f232de4243c80c9fdba9244cd164eb8fbc58/apps/server/src/environment/ServerEnvironment.ts#L274) stays true independently of scratch. This verifies R4 by code, without claiming visual execution.
- **Surfaces without changes:** No contract, setting, provider adapter, Electron IPC, or client rendering implementation changes. Local and remote clients get the destination server's same config/error; MCP uses that destination's home/root. The exception to “no UI change” is the inherited visible error content above. No provider-specific matrix or paid agent turn is needed.

Existing PR-body proof coverage: **no affected row is adequate at the pinned revisions**. R1 is historical/stale; R2 and R3 lack response captures; R4 is omitted under a contradicted premise. R5 has an older outside-repo observation and existing service tests. R6/R7 have no supplied focused proof, but are unchanged-output controls, not additional changed-diagnostic blockers. Refresh the test-run description to match the consolidated head test instead of claiming the old two new cases.

Coverage: all three changed files, pinned base implementation, production callers of the probe/root/ensure/classification/allocation paths, RPC/MCP mappings and MCP error framing, shared client command, web/desktop consumers, and mobile entry/error paths. GitCafe PR body, comments, reviews, issue, and revision-bound status were read. Current checks were still pending when read; this is a proof audit, not CI approval. No runtime or test execution was performed, in accordance with the read-only brief. Completed within the 45-minute budget.

FRICTION: none

VERDICT: INSUFFICIENT R1 stale log/config transcript; R2 missing RPC responses; R3 missing MCP responses; R4 contradicted no-render claim and missing mobile captures
