# Preview default appearance — proof audit, pass 1

PR [#24658](https://git.cafe/pingdotgg/t3code/pull/24658) ([GitHub mirror](https://github.com/pingdotgg/t3code/pull/24658)). This is a code-derived capture plan, not evidence that any row passes.

Candidate: [04d02d5](https://github.com/pingdotgg/t3code/commit/04d02d50e48c9b713c5ae1d944df41a96ffabe7b), full SHA `04d02d50e48c9b713c5ae1d944df41a96ffabe7b`, branch `takeover/pr-16983`.
Baseline/merge-base: [edb2b2f](https://github.com/pingdotgg/t3code/commit/edb2b2f3657b40a08c401f2c769823c58c82db45), full SHA `edb2b2f3657b40a08c401f2c769823c58c82db45`.

Scope: six production files and three test files; no JSX, CSS, or component layout changed. The visible output is the remote page and the existing Preview menu → Appearance value. Earlier captures and the [older matrix](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d320486a95e7e1f1/proof-matrix.md) do not fill this matrix. All expected base/head differences below are predictions from code.

## Findings before capture

1. **Missing evidence:** every affected row is presently unverified. The requested revisions need new, labelled before/after evidence; PR-body images at other SHAs do not count.
2. **Web settings cannot be clicked:** at the pinned head, the path is **Settings → Integrations → Browser → Default browser appearance**, route `/settings/integrations`. The browser defaults, including this select and Link target, are disabled outside Electron by `previewDefaultsDisabled = !isElectron`. There is no `/settings/browser` page for this setting at this SHA. Use the real persisted client-settings API below in Vite web and label that preparation; do not claim a successful Settings click or weaken the disabled UI. This is existing behavior, not a newly established defect in the diff.
3. **System needs a renderer-qualified claim:** standalone server Chromium launches headless. System clears its page override; it does not mean “follow the web viewer's theme.” On the ordinary Linux headless path the native result is expected to be Light. Light pixels alone cannot prove the new null reset. Pair the System recording with the first-application protocol/test evidence described below. A dark native-host/live-switch claim requires the optional calibrated native-renderer extension, not viewer DevTools emulation.
4. **Popup identity matters:** a trusted `target=_blank` click inside the streamed server page must create a server-adopted popup. An Electron `onOpenLink` event can instead call the URL-opening helper and use the current saved default; that is a different path. Capture child/opener IDs and the inherited snapshot. If an already-loaded popup remains under an Idle/empty overlay, preserve that failure/limitation; do not hide the overlay or relabel decoded frames as a visible app success.
5. **Mutable checkout:** inspection began with the requested head, then the shared worktree moved to a different commit. Subsequent derivation used pinned Git objects, with selected source copies under this audit's scratch directory. The capturer must have the lead prepare the exact candidate and baseline; do not simply capture whatever `HEAD` currently contains.

No additional implementation bug is asserted by this proof-planning pass. No browser, server, test runner, provider turn, or capture was started.

## Complete affected-path inventory and collapse

The rows below describe all production callers of the three changed helpers and the changed server/contract paths. Aliases are inventoried, not multiplied into a screenshot quota.

| Changed unit | Callers / paths at the pinned head | Relevant branches and collapse |
|---|---|---|
| [openUrlInPreview / openFileInPreview](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/web/src/browser/openFileInPreview.ts#L57) | `useOpenLink`; `ChatMarkdown.openExternalLinkInPreview`; `ElectronBrowserHost.onOpenLink`; `useChatTerminalActions.runProjectScript` auto-open; `DesktopWebLinkCoordinator` incoming HTTP URL; and `openFileInPreview`. File helper callers: `ChatMarkdown.openMarkdownFileInPreview`, `FilePreviewPanel.handleOpenInBrowser`, `DesktopWebLinkCoordinator` incoming file. `useOpenLink` consumers: ChatMarkdown deferred links, GitActionsControl, BrowserDocumentFrame, PullRequestChecksPopover, PullRequestSummaryTab, and `useOpenChangeRequestLink` in `lib/openPullRequestLink.ts`. | Hydrated defaults succeed vs settings failure (no request); runtime `server` adds the saved enum, runtime undefined omits both runtime and new enum. Foreground opens/activates the panel; background updates the tab and conditionally restores prior selection if the user has not changed selection in flight. Optional profile override vs saved profile; saved viewport passed through. File availability/access failure, workspace-file vs media-file asset, asset creation failure/invalid URL vs success all precede the same opening helper. R2 uses the real Files/HTML asset path; other successful callers share the changed line. No new visual branch by URL length, file extension, profile count, background focus, or asset kind. |
| [openPreviewSession](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/web/src/components/preview/openPreviewSession.ts#L34) | `addBrowserSurface` through `useRightPanelActions.createBrowserSurface` (right-panel add Browser/profile action; preview toggle with no active tab via command/keybinding); `openDiscoveredPort`; `PreviewView.navigateToResolvedUrl` when no existing server surface/desktop tab can navigate; `PreviewView.handleMoveTab` between runtimes; `reopenClosedView` for a closed browser. | Hydration success/error; explicit runtime override vs automatic selection; `server` sends preference, desktop/undefined omits server seed. URL omitted creates Idle vs supplied creates Loading; explicit/default viewport and profile. Failure returns; success applies snapshot/remembers URL. Address navigation of an existing tab and toggling an already-open tab do not create a tab or read a fresh default. Reopen/move create a new tab using current default rather than copying old appearance. R1 and R5 cover fresh creation and repeated default reads. |
| [openTerminalLinkInPreview](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/web/src/components/preview/openTerminalLinkInPreview.ts#L41) | `ThreadTerminalDrawer.openLinkInPreview` (the thread terminal link handler). | In-app path requires no force-external modifier, HTTP(S), preview available, nonempty thread ID, and saved `browserLinkTarget:'app'`; otherwise external fallback. Await defaults; server vs undefined runtime as URL helper. RPC interrupt stops; other failure logs/falls back; success records visit/applies tab/opens panel. R3 deliberately reaches success. External, error, and interrupted branches are unchanged and do not exercise the new theme seed. |
| [PreviewOpenInput](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/packages/contracts/src/preview.ts#L185) and [Manager.open](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/server/src/preview/Manager.ts#L182) | Shared preview command → authenticated `preview.open` WS handler → manager. Server-internal opens: `adoptPopup` and automation `open` in ServerBrowser. No new mobile caller or provider-specific adapter. | Optional Light/Dark/System is copied into the session snapshot and opened event when supplied; omission stays omission. This is in-memory preview session state, not a durable database migration. `PreviewAppearancePreference` already exists; no new menu choices. Client default is local to that web origin. Automation open does not supply a client default; old callers that omit it continue with effective System. Agent ownership/reveal and profile branches are unchanged. Core recordings retain sanitized open request/receipt/snapshot to distinguish explicit Light/System from omission. No separate provider or old-client visual matrix is needed. |
| [ServerBrowser creation/rendering](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/server/src/preview/ServerBrowser.ts#L650) | `ensureTab` → `createTab` from opened events, viewer/automation lookup; `applyRendering` at creation, resized/adjust events, and automation appearance setting. | New page vs desktop-attached page vs adopted popup all initialize applied appearance to null. Explicit Light/Dark invoke matching emulation; System or omitted snapshot invokes `emulateMedia({colorScheme:null})` once. Same appearance on unrelated updates skips emulation. Isolation for agent/incognito vs profile context, viewport fill vs sized, and zoom do not add theme branches. R1/R2/R3 exercise enum values; R3 also needs first-reset evidence; R4 covers adopted creation. Native desktop attach is a separately labelled optional extension below. |
| [adoptPopup](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/server/src/preview/ServerBrowser.ts#L1190) | Playwright page `popup` listener. | Closing opener/capacity rejection closes child; HTTP(S) URL vs initial non-HTTP/blank URL decides initial navigation snapshot; retain opener profile and automation ownership/reveal. Applied preference non-null is copied, including literal System (not its resolved Light/Dark); null is omitted. Normal user can click only after creation applied a preference. R4 contrasts opener Dark with saved default Light on native-Light renderer, proving this path rather than accidentally passing via current default. Light/System copies have the same copying branch; they need no separate mandatory popup recordings for this scoped fix. |
| Three changed test files | `openPreviewSession.test.ts`: both session and URL helpers reread Light→Dark→System; `openTerminalLinkInPreview.test.ts`: Dark payload; `ServerBrowser.test.ts`: initial null reset, no repeat for zoom, later Dark adjustment, explicit Light/Dark opens and popup inheritance. | These support mechanism claims, not visual captures. Do not mark a real-app row captured because a mocked test passed. The initial-reset case uses a desktop-attachment fixture; distinguish it from standalone web execution. |

Existing rendering consumers, without changes: `RightPanelSurfaceContent` → `PreviewPanel` → `PreviewView`. Permission/capability unavailable renders the existing access/unavailable message. A null/Idle snapshot renders the empty start page; Loading and first streamed frame render loading state; Success shows the page; LoadFailed/download shows the existing failure/download surface. Server runtime in web uses `ServerBrowserSurface`; primary local Electron can use the native BrowserSurfaceSlot for a server-owned tab; legacy desktop tabs use their existing desktop defaults. Fill/sized viewport changes device toolbar and scaling. Inline/sheet/maximized panel, visible/hidden state, and narrow toolbar affect framing, not color-scheme dispatch. Profile count changes add-menu/submenu presentation; counts/titles/long URLs change tab chrome only. None needs an error/loading/length/count capture permutation for this diff. A real page must finish loading in every proof row, and an overlay covering it is not proof of the page's appearance.

## Real-app setup for the capturer (not executed in pass 1)

1. Have the lead prepare isolated checkouts/builds at the two exact SHAs. Confirm `git rev-parse HEAD` before each run and save it with the artifacts. Do not reset the shared audited worktree. Start each with `T3CODE_DEV_SEED=0 vp run dev`; for fresh state pass an explicit `--home-dir` below the run directory. Seed=0 only disables importing live data; it does not clear an existing test database. Never start against live T3 home.
2. All files for this audit/capture belong below `/tmp/lead-24658-r1/proof-audit-1/`. Each driver execution creates a new timestamped run directory there, with separate base/candidate state, logs, project and artifacts; do not delete/overwrite earlier runs. Read actual web/server ports from `[dev-runner]`. Use normal authorized dev authentication, keep credentials/startup pairing URLs out of uploaded output, and do not set VITE_HTTP_URL/VITE_WS_URL.
3. Create a tiny scratch project in that run directory, optionally `git init` it, with the HTML fixture below saved as `scheme.html`. In the real web app, Add project/Open existing directory to that scratch project, then New thread; remain in the created draft route `/draft/<draftId>`. `ChatView` builds a local draft thread with environment/thread IDs, so Browser creation does not require submitting a prompt or starting a provider. If using an explicitly persisted fixture thread instead, record its real `/<environmentId>/<threadId>` route and seed it through the normal orchestration create API, not database writes.
4. Start a scratch static server on the **T3 server/renderer host**, e.g. `python3 -m http.server 49173 --bind 127.0.0.1 --directory <run-directory>/project`, capturing its PID/log. Choose a free port and substitute it everywhere if occupied. The fixture URL is `http://127.0.0.1:49173/scheme.html`. A remote viewer's localhost differs; this URL is intentionally consumed by the server browser on the same host. The file-preview row instead uses the app-generated workspace asset URL.
5. Confirm the environment advertises server browsing and the browser is installed/ready through the existing setup flow. The actual opening request and resulting snapshot **must say `runtime:'server'`**; a legacy desktop page is not this regression. Use preview-operate and file/terminal access grants needed by the row. If unavailable, report the missing prerequisite rather than silently falling back to a mock or external browser.
6. Inspect Settings → Integrations → Browser → Default browser appearance. In Vite web it is disabled at these revisions. Seed through the production API in the authenticated app page, await persistence, then show/read the disabled setting's saved value as context:

```js
const settings = await import('/src/hooks/useSettings.ts');
await settings.ensureClientSettingsHydrated();
await settings.updateClientSettings({
  browserDefaultAppearance: 'dark', // substitute light/system per row
  browserLinkTarget: 'app',
  browserDefaultViewport: { _tag: 'fill' },
});
settings.getClientSettings().browserDefaultAppearance;
```

This is **real-app fixture preparation**, not a labelled harness render: it uses the same saved settings and opening handlers as the UI. Do not call `__setClientSettingsForTests`, replace the desktop bridge, or pass colorScheme directly to the opening RPC for the default-propagation rows. Apply separately on baseline and candidate origins/profiles. For R5 change only `browserDefaultAppearance` in the same mounted app, using this API, and visibly label the API action in the recording. There is no enabled web Settings control to demonstrate. Electron can use the actual Settings select, but that is a different client/setup and must be labelled.
7. Keep viewer UI theme **Light**, viewer viewport **1440×1000**, browser viewport **fill**, normal default profile, same fixture and host for all five mandatory rows. Record renderer kind/native scheme; do not use viewer-theme or DevTools emulation as a renderer-host calibration. The pure headless host is expected to be Light; confirm it using the System probe and report the observation. No cross-product of UI theme or profile/width is needed: no theme-dependent JSX/layout changes here.

HTML fixture (a ready copy is also saved at `/tmp/lead-24658-r1/proof-audit-1/scheme.html`):

```html
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Preview appearance proof</title>
<style>
  :root { color-scheme: light dark; background: #fff; color: #17202a; font: 18px/1.5 system-ui, sans-serif; }
  body { margin: 24px; overflow-wrap: anywhere; }
  @media (prefers-color-scheme: dark) { :root { background: #101820; color: #f7f9fb; } }
  a { color: inherit; }
</style>
<h1>Preview appearance proof</h1>
<p id="scheme"></p><p id="identity"></p><p id="changes"></p>
<a href="/scheme.html?case=popup" target="_blank" rel="opener">Open popup</a>
<script>
  const media = matchMedia('(prefers-color-scheme: dark)');
  const token = crypto.randomUUID(); let changes = 0;
  function paint() {
    document.querySelector('#scheme').textContent = `prefers-color-scheme: dark = ${media.matches} (${media.matches ? 'DARK' : 'LIGHT'})`;
    document.querySelector('#identity').textContent = `document ${token}; case ${new URL(location.href).searchParams.get('case') ?? 'opener'}; window.opener ${window.opener ? 'present' : 'absent'}`;
    document.querySelector('#changes').textContent = `Media changes since document load: ${changes}`;
  }
  paint(); media.addEventListener('change', () => { changes++; paint(); });
</script>
```

## Minimum fixture matrix

All five rows are affected. All require **recording** because an opening, click, attachment, or preference change is part of the assertion; extracted stills can supplement but cannot replace them. One baseline recording and one candidate recording may cover multiple rows with explicit row/time ranges. Minimum product is **1440×1000 viewer × Light viewer UI × R1–R5 × both exact revisions**, using Light-native headless server Chromium, not a six-state host-theme cross-product. Capture the full real app and its streamed page; keep a clear page/probe crop as supplemental evidence.

| ID / named fixture | Diff affected / required capture | Exact real-app seed and drive | Predicted baseline → candidate / acceptance | Status |
|---|---|---|---|---|
| **R1 — saved Dark, fresh Browser (reported regression)** | Yes; mandatory **recording**, 1440×1000 / viewer Light / renderer native Light. | API-seed saved default **Dark**. In the draft/thread click the right-panel add **+ → Browser** (use the thread's panel launcher if it is closed). This calls addBrowserSurface/openPreviewSession; then enter the static fixture URL in the address bar and submit. Keep the born tab's ID. | Baseline request/snapshot lacks preference, page normally Light. Candidate request and born snapshot say `colorScheme:'dark'`, Preview menu → Appearance says Dark, page is DARK=true. Record through first loaded/streamed frame; opening blank then navigating the SAME tab still proves the initial seed. Do not set per-tab appearance to manufacture the result. | **NO CAPTURES YET** |
| **R2 — saved Light, Files → browser (URL helper)** | Yes; mandatory **recording**, same viewport/host. | API-seed default **Light**. Open right-panel **Files**, select scratch `scheme.html`, click its action titled **Open file in preview browser** (confirm the actual accessible label if wording differs). It creates a workspace-file asset URL and calls openFileInPreview → openUrlInPreview. | Baseline may already look Light; candidate must carry explicit `colorScheme:'light'` from the real opening request into snapshot/menu and page LIGHT=false-for-dark. Light pixels alone do not prove the saved preference was forwarded. Retain the app-generated URL and tab ID. This covers the changed URL helper without requiring a real agent-generated Markdown message. | **NO CAPTURES YET** |
| **R3 — saved System, real terminal link and first rendering application** | Yes; mandatory **recording**, same viewport/host; mechanism evidence also required. | API-seed **System** and `browserLinkTarget:'app'`. Open the thread terminal through **+ → Terminal**. Print the static fixture URL with `printf '%s\n' 'http://127.0.0.1:49173/scheme.html'`; click the recognized URL without Ctrl/Cmd/force-external modifier. This must reach openTerminalLinkInPreview and create a fresh server tab. | Baseline omits preference; candidate snapshot/menu explicitly System. Both can visibly be Light on headless Linux. Candidate must clear initial media override, not pin to viewer theme. Attach a sanitized first-application runtime trace showing the reset, or the exact-head focused `ServerBrowser.test.ts` transcript for “a system tab clears the light Playwright forces on attach, without redoing it.” Label that mocked attachment test as mechanism support, not real native-host visual proof. If claiming the live runtime reset itself, test output alone is insufficient: include its actual protocol trace or the calibrated optional extension below. | **NO CAPTURES YET** |
| **R4 — popup inherits Dark despite current saved default Light** | Yes; mandatory **recording**, same viewport/host. | Keep saved default **Light**. Open fixture in a server tab; use **Preview menu → Appearance → Dark** on the opener (unchanged adjustment path, on BOTH baseline and candidate). Verify opener is DARK=true and applied snapshot Dark. Then trusted-click fixture **Open popup**, the actual `target=_blank rel=opener` anchor inside the streamed page. Do not open the anchor through an external viewer or synthetic window creation. | Baseline child lacks copied Dark and normally becomes System/Light; candidate child inherits literal Dark, is DARK=true, and retains opener relationship. Save opener/child IDs and child snapshot. Opposite saved default proves inheritance rather than a fresh-default open. Record child adoption and loaded presentation. If raced Idle/overlay prevents display, mark visible row **FAIL/UNVERIFIED**, preserve raw frames/snapshots and report inherited-media-only proof separately; do not navigate/reload child before documenting initial outcome. | **NO CAPTURES YET** |
| **R5 — saved default changes only new tabs** | Yes; mandatory **recording**, same viewport/host. | In one mounted app API-seed **Dark**, **+ → Browser** and load fixture (A). Change persisted default to **Light** using the real API; show saved setting/readback, return to A without closing/recreating it, then **+ → Browser** and load fixture (B). Change default back to **Dark**, add/load C. Switch A/B/C and show IDs, document tokens and Preview menu Appearance. | Baseline opens omit seeds and normally remain System/Light. Candidate A stays Dark after both changes; B is Light and stays Light after second change; C is Dark. Newly opened IDs differ; revisiting A/B keeps their original document tokens. Do not test by navigating an existing tab, closing/reopening, or changing its per-tab menu. This proves repeated saved-default reads and no retroactive retheming. | **NO CAPTURES YET** |

Popup Light/System and the other successful caller aliases need no mandatory extra videos: the copy branch has no Light/Dark/System split, and all three values are covered by R1–R3, with explicit Light/Dark popup behavior additionally covered by the focused tests. Do not claim those aliases or popup modes were visually exercised unless actually captured. The matrix proves this scoped fix, not every unchanged feature of Preview.

## Width and optional extensions

**390px means the viewer's browser viewport, unless explicitly called the page viewport.** At the pinned head, Preview is not categorically hidden at 390. `ChatView` switches to `ChatRightPanel`'s sheet at `max-width:980px`; `RightPanelSheet` uses `min(88vw,24rem)` below 760px, so at 390 its outer width is about **343px**, before borders. Open/activate Browser in that sheet before capturing. A screenshot of the main chat with the sheet closed proves nothing about Preview. This is responsive web, not a claim about the native mobile client. Source: [layout breakpoint](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/web/src/rightPanelLayout.ts#L1), [sheet](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/web/src/components/RightPanelSheet.tsx#L25), [ChatView](https://github.com/pingdotgg/t3code/blob/04d02d50e48c9b713c5ae1d944df41a96ffabe7b/apps/web/src/components/ChatView.tsx#L3904).

| ID | Purpose / seed / capture | Scope and status |
|---|---|---|
| **N1 — 390-wide viewer, Dark Browser sheet** | Optional **recording**, viewer **390×844**, viewer theme Light, saved default Dark, renderer native Light. Repeat R1 and deliberately open the Browser sheet; show page probe and Appearance menu. If the narrow toolbar obscures the probe, preserve that observation and also provide the standard 1440×1000 recording/crop, without CSS changes or presenting the wide crop as a 390 capture. | Framing control; no width-dependent dispatch changed. **NOT REQUIRED / NO CAPTURES YET**. |
| **N2 — page itself exactly 390 wide** | Optional **recording**, only if requested as responsive *page* proof: keep viewer 1440×1000 and use Preview's existing device/responsive toolbar to size the browser page to 390 wide; record actual page `innerWidth` with its probe. This is different from N1, and does not establish a 390-wide T3 client. | Unchanged viewport control. **NOT REQUIRED / NO CAPTURES YET**. |
| **S-native — native Dark System, first CDP attachment / live transition** | Optional stronger **recording**. Standalone `T3CODE_DEV_SEED=0 vp run dev` offers no dark native-renderer selection: ServerBrowserContexts launches headless. Cannot honestly seed this by changing the viewer theme. If the lead has an isolated real Electron primary host serving the SAME `runtime:'server'` tab to the web client, calibrate its native Dark page before the first actual Playwright/CDP attachment, set Browser default System, record opening/attachment and, for a live-follow claim, change the actual host theme Light→Dark→Light while keeping the tab/document alive. Record renderer/host, tab ID and matching native probe. A fixture with a mocked desktop attachment or artificial dark-host launch is a **labelled harness capture**, not standalone real-web proof. | Conditional extension for native-host or zero-flash claims; do not block the five-row headless-default proof on unavailable Electron. **NOT AVAILABLE FROM STANDALONE WEB SETUP / NO CAPTURES YET**. |

No Electron legacy/runtime-transfer, secondary environment, remote/tunnel transport, native mobile, macOS/Windows, provider-by-provider, counts/long-name, download/error-page, or native OS appearance claims are made by the minimum set. Their relevant code branches are inventoried above; none adds a new client-local-default branch in this diff.

## Evidence handoff rules

- Each row records exact revision, viewer dimensions/theme, renderer kind and native scheme, persisted settings readback, seed method, route/environment/thread/tab IDs, request/snapshot preference, page probe, artifact URL and recording timestamps. Redact credentials, cookies and startup pairing URLs. A bare `colorScheme` RPC override is not proof that a saved default crossed the real opening path.
- Baseline and candidate use equivalent fixtures in isolated fresh state. Name recordings `R1-base` / `R1-head`, etc.; combined recordings must index each row's time range. Supplement R1 with labelled Before/After stills for the PR if useful. Do not reuse older SHA evidence.
- Status after capture is `PASS`, `FAIL`, or `UNVERIFIED (specific prerequisite)`, never “passed by code inspection.” Record empty/loading/error outcomes honestly. Optional native/harness records must name the setup and cannot fill a standalone real-app row.
- Minimum first-reset support can be the already-added focused mock test transcript at the exact head; it verifies the null sentinel and retained equality guard. Real System video plus that transcript supports the scoped behavior, but does not prove a real native Dark host or no initial flash. Do not add tests or edit the audited source for this proof pass.
- Stop only owned processes by captured PID. Keep all evidence/raw logs until uploaded. If `df -BG --output=avail /` drops below 5 GB, stop and report available space as infra friction. This pass started no persistent processes and deleted nothing.

## Friction

FRICTION:
- infra | Shared audited checkout changed revision during source reads; exact pinned Git objects were used to finish derivation | Keep the capture checkout pinned/exclusively owned or read immutable objects and verify SHAs immediately before capture.
- tool | Web fetch could not open the older matrix URL; a read-only curl fetch succeeded | Fetch raw downloadable Markdown directly when the web parser cannot access it.
- forge | Initial GitCafe status request exited 7; a later read succeeded and reported the requested head | Preserve/retry a transient read failure; do not substitute another forge or infer current capture proof from CI.

VERDICT: INSUFFICIENT <all affected rows: no captures yet>
