# Read-only proof audit, pass 1

Scope: [#19738](https://git.cafe/pingdotgg/t3code/pulls/19738), [GitHub mirror #19738](https://github.com/pingdotgg/t3code/pull/19738). Only `/home/ubuntu/wt/pr-19738` inspected. No prior findings. No other PR audited.

Reviewed replacement `takeover/pr-19738` at [9da6ba5f1f](https://github.com/pingdotgg/t3code/commit/9da6ba5f1fe6d66ecc4e734d38276451f0b9ab78), against local `origin/main` [c4fa4629eb](https://github.com/pingdotgg/t3code/commit/c4fa4629eb645b95aae2beef56c70f705b736049). Original supplied context: `aravhawk/t3code`, `fix/persisted-pr-inactivity`, [7cce90c](https://github.com/aravhawk/t3code/commit/7cce90c). Local refs verified; no fetch or live forge metadata refresh performed for this narrowly scoped proof plan.

Read worktree `AGENTS.md`. Read the entire `git diff origin/main...HEAD`: exactly two files, `apps/server/src/orchestration-v2/ThreadSettlementService.ts` and its existing `.test.ts`; 109 insertions, 3 deletions. Followed worker scheduling, persisted-link sync, schemas, RPC handlers, forge summary implementation, dev runner, clock, and auto-settle commit guard.

This is a proposed proof matrix, not completed proof. No tests, servers, browser sessions, captures, code edits, pushes, PR comments, or watches were run. No developer live userdata was read, copied, or written. This document is the only written audit artifact and is outside the worktree.

## Behavior and minimum fixture matrix

All rows use a candidate with no unrelated blockers unless named. `T` is server time; `A` is latest activity (max of user message/run request/run start/run completion, falling back to creation); `D` is configured days. “Settle” means a persisted `thread.settled` event from a `server:auto-settle:` command and projection `settledOverride="settled"`, with `settledAt` equal to the activity/wake anchor, not sweep time. Tests should assert returned timestamps and committed behavior, not merely counts.

| ID / affected path | Fixture and controls | Minimum proof / acceptance | Current coverage from source |
| --- | --- | --- | --- |
| P1 core regression | Only PR representation is `pullRequests=[known-open persisted link]`; legacy `linkedPullRequest` and `branchPullRequest` null; A older than D; retention false | Resolver returns A; worker dispatches auto-settle; real isolated server transitions unsettled → settled without user/run activity, link remains open | New resolver test and startup-sweep worker test; runtime transcript still required |
| P2 retention | Same fixture as P1 with retention true, including only persisted link (no legacy alias) | Resolver null and worker no settle through completed sweep; live control stays unsettled at same age | New resolver test covers true; worker/live control still needed |
| P3 unknown conservative | Visible link snapshot null, alone and mixed with known-open/terminal links; retention false | No settle even if old or supplied lookup says merged/closed; live unknown fixture stays unsettled | New single-null resolver test; mixed priority and worker/live control absent in added diff |
| P4 no premature terminal shortcut | Open persisted link plus supplied merged **and closed** lookup; merge setting on/off; inactivity null or not yet elapsed | No merge/close shortcut; retention false settles only after inactivity; retention true protects at old age | New test covers merged + inactivity null only; extend parameterization for closed and recent/old cases |
| P5 mixed persisted links / stack | At least one visible open plus terminal link linked before terminal time; vary array order | Open outranks terminal-link selection. Null D: no settle; old + retention false: settle; old + retention true: no settle. Unknown anywhere still blocks | Not directly covered by new tests; use resolver table, no separate live recording needed |
| P6 time boundary and anchor | Known-open link; A just newer than, exactly at, and just older than T−D days; run timestamps newer than message; empty thread; wake timestamp newer than A | Strict `<`: equality stays unsettled; only older settles; latest activity or wake controls timestamp; PR sync time does not restart inactivity | Existing generic activity, empty-thread, manual/timer wake tests; add open-link boundary rows to tie new path to these rules |
| P7 inactivity disabled | Known-open link, D=null, onMerge true/false, retention false/true | Never settle from open PR alone; changing settings can later enable inactivity | New merged-conflict null-D assertion partly covers; standalone null-D control useful |
| P8 existing candidate protection | Known-open, old, retention false; individually archived, settled/active override, pinned, auto-settle disabled, pending runtime request, live run, completion-holding background work, recent queued turn, effective snooze | No auto-settle while each blocker holds. Clear blocker → normal inactivity decision, accounting for wake anchor. Background shell that does not hold completion remains eligible | Existing candidate/worker coverage handles these guards; use compact resolver/candidate table where absent, not a full cross-product |
| P9 unaffected terminal/no-link behavior | No links; eligible persisted merged/closed only; terminal predates link; tombstoned `stack-dismissed` open/null; legacy references | Existing merge/close timing and merge setting remain; newest eligible terminal chosen; recorded historical terminal ignored; dismissed links invisible; legacy/no-link inactivity unchanged | Existing terminal, old-merge, legacy retention tests. Inspect/extend focused table for dismissed/mixed ordering rather than demanding duplicate suites |
| P10 worker entry points | Known-open persisted fixture through initial activation, periodic minute sweep, link/sync event, settings change; project override of inactivity/retention | Same P1/P2 decision from effective project settings; known visible links never fall into branch/legacy forge lookup; configuration change enqueues immediately | Added worker case proves initial sweep only; existing project settings and merge notification tests cover shared machinery; focused TestClock settings/event/periodic case needed for newly reachable path |
| P11 committed state / race / consequences | Auto-settle dispatch with snapshotAt; intervening thread update or explicit override; successful settlement re-emitted | Stale auto-settle rejected; successful event/projection durable across restart; terminal cleanup and settle scripts retain existing once-per-settlement behavior; no script in shared checkout | Existing orchestrator guard and cleanup tests are controls. Runtime P1 should use no worktree/scripts so proof cannot invoke project code |

Smallest automated set: run the existing `ThreadSettlementService.test.ts`, with focused additions in that file for P3–P7/P10 gaps if the implementer needs full claimed coverage. Do not add a new broad harness or UI tests. Existing added P1 resolver and worker assertions should fail against base implementation because base returns null for any visible open link. Demonstrate that red result on the base implementation without changing this review worktree; then green on reviewed replacement. Run targeted checks only; no repo-wide checks.

Suggested command for the later execution pass (not run here):

```sh
cd /home/ubuntu/wt/pr-19738
vp test run apps/server/src/orchestration-v2/ThreadSettlementService.test.ts
```

Use `TestClock.adjust` and service `drain`, Queues/Deferred or exact persisted-event milestones. The new worker test uses a mocked orchestrator and asserts dispatched IDs; it does not substitute for the actual server commit transcript. Do not prove negative controls with arbitrary sleeps or retry loops: establish completed sweep/settings processing, then inspect their state.

## Real server seeding route, without live data or real PRs

The following are instructions for the later authorized execution pass, not actions performed in this audit. Start with `/tmp/ev-19738` absent or verified empty; do not delete an unknown directory. Keep fixture code, offset files, transcript, and stub logs beneath that new home. Never run `migrate-dev-db` for this proof.

Launch from the reviewed worktree with explicit home and auto-seeding disabled:

```sh
T3CODE_DEV_SEED=0 vp run dev --home-dir /tmp/ev-19738 --no-seed --no-browser --no-auto-bootstrap-project-from-cwd
```

For the time/forge preload described below, add `NODE_OPTIONS='--import=/tmp/ev-19738/proof-preload.mjs'` and a dummy, non-secret `GH_TOKEN` scoped to this process. Confirm the runner's actual ports and runtime home in startup output; ambient `T3CODE_HOME` is insufficient in a worktree. Do not set VITE_HTTP_URL or VITE_WS_URL. Record spawned PID and stop only the process tree owned by this fixture. Redact pairing URLs/auth credentials before upload.

Use a small terminal RPC driver built from the existing typed `WsRpcGroup` and `RpcClient.make` with `RpcClient.makeProtocolSocket`, `Socket.layerWebSocket`, `RpcSerialization.layerJson` (pattern: `packages/client-runtime/src/rpc/session.ts`). Authenticate through the fresh dev server's own dev credential/pairing flow, keeping credentials out of the transcript; do not use the current thread's live MCP environment as the seeding destination. Use `/ws?orchestrationProtocol=2` with that fixture's authorized session. A raw ad-hoc `{method,params}` JSON-RPC envelope is incorrect: this is Effect RPC (`_tag:"Request", id, tag, payload, headers`, plus its response/stream acknowledgment protocol). Prefer the typed client so dates are encoded correctly.

Call these actual app routes in order, waiting for committed replies/events:

1. `server.updateSettings({patch:{sidebarAutoSettleAfterDays:null,sidebarAutoSettleOnMerge:false,sidebarAutoSettleKeepPullRequests:false}})`. Begin disabled so seeding cannot race a sweep. Verify effective settings, and no project overrides initially.
2. `projects.mutate({type:"project.create",commandId:unique,projectId:"proof-19738",title:"PR inactivity proof",workspaceRoot:"/tmp/ev-19738/workspace",createWorkspaceRootIfMissing:true,scripts:[]})`. A non-repository project is supported for explicit-host linked PRs, so no remote fetch, checkout, or real forge object is required.
3. `orchestration.dispatchCommand` with `{type:"thread.create",commandId:unique,threadId:"proof-open",projectId:"proof-19738",title:"Idle open link",createdBy:"user",creationSource:"web",modelSelection:{instanceId:<configured instance>,model:<its model>},runtimeMode:"full-access",interactionMode:"default",branch:null,worktreePath:null}`. Select an available configured model from fresh server config; creating an empty thread does not start a provider. Create separate `proof-retained` and `proof-unknown` threads the same way. Do not send messages, start runs, schedule tasks, or use `orchestration.launchThread` with a prompt.
4. For each, `orchestration.dispatchCommand` with `{type:"thread.pull-request.link",commandId:unique,threadId,host:"github.com",repository:"fixture-owner/fixture-repo",number:<101|102|103>,url:<synthetic GitHub-shaped URL for that number>,source:"manual"}`. These are fabricated references resolved only by the stub; never create or contact an actual PR. Linking alone gives snapshot null, which is not the regression fixture.
5. Let the real `PullRequestSyncReactor` read the stub summary and commit `thread.pull-request-link-updated` containing `snapshot.state:"open"` for the first two. For unknown, return not-found for its summary and verify snapshot remains null. The alternative deterministic seeding command is `thread.pull-request-link.sync` with same key, `snapshot:{state:"open",title:"Fixture open",headBranch:"fixture",baseBranch:"main",isDraft:false,updatedAt:<ISO>,syncedAt:<ISO>},stack:null`; this command is in the public command schema. If using it, still stub the real sync reactor so it cannot contact a forge or overwrite the fixture. Mark the transcript honestly as command-seeded rather than forge-seeded.
6. Read `orchestration.getThreadProjection({threadId})` and subscribe via `orchestration.subscribeThread({threadId,...})` before enabling settlement. Confirm stored link and snapshot, no messages/runs/runtime requests/holding tasks, no pin/snooze/override/archival, legacy PR fields null, `settledAt:null`, `settledOverride:null`. Empty thread age is its real persisted createdAt. Read-only SQL of **only** `/tmp/ev-19738/userdata/statev2.sqlite` may supplement RPC/event evidence, never replace creation commands or manually write projections.

## Stub forge and time hook details

A `gh pr view` executable stub is insufficient. `PullRequestSyncReactor.syncGroup` calls `PullRequestService.summary`, which reaches `GitHubPullRequestApi.getPullRequestSummary` using an aliased HTTP GraphQL resolver. `apps/server/src/server.ts` supplies `FetchHttpClient.layer`. A fixture-only preload can wrap `globalThis.fetch` before server modules load, answer GitHub GraphQL calls locally, and reject any unexpected external fetch instead of forwarding it. Log method, operation, synthetic key, response state, and logical time; never authorization headers. Permit local RPC traffic if it uses fetch. Ensure any CLI credential fallback is avoided with the process-local dummy GH_TOKEN; reject unexpected forge CLI invocations rather than falling through to the real binary.

Implement the response against `packages/source-control-github/src/server/pullRequestBatches.graphql`, including actual generated batch aliases, not a hard-coded unaliased `data.repository`. For each requested repository alias return a pullRequest with number, title, synthetic url, `state:"OPEN"`, `isDraft:false`, `mergeable:"MERGEABLE"`, `reviewDecision:null`, zero additions/deletions/changedFiles, updatedAt, null mergedAt/closedAt, headRefName/baseRefName, null author, empty latestReviews.nodes and commits.nodes, `stack:null`, `stackEntry:null`. Recognize stack membership reads too and return null stack so no siblings appear. Unknown control returns a per-alias NOT_FOUND GraphQL error with matching path. Support any routing identity discovery request with a fixed synthetic viewer, or use direct sync seeding and fail unrelated requests explicitly. Validate the response through the actual decoder: a logged stub response alone does not prove persisted known-open state. Handle all aliases/fixture keys, including batched first sync. No watch is needed or allowed.

There is no discovered runtime clock flag. The settings schema permits 1–90 days, so a fractional-day or zero-day patch is rejected. A fixture preload can bind original Date.now and replace `Date.now` with `originalNow()+Number(readFileSync('/tmp/ev-19738/offset-ms','utf8'))`, with an initial offset of 0. This is a proposed external test hook, not an existing project feature and not tested in this audit. The installed Effect Clock implementation (`effect/src/internal/effect.ts`, ClockImpl.currentTimeMillisUnsafe) reads Date.now; DateTime.now follows that clock. Keep timers and monotonic clocks unchanged. Emit hook-loaded PID and logical timestamp diagnostics, and confirm the actual server child inherited the preload. Do not change the host clock or patch repository files.

At T0, seed with D disabled, then configure D=3. Record a completed pre-threshold decision: empty known-open threads remain unsettled. Advance offset by four days using the external file; this crosses the one-day minimum without altering thread activity. Since the periodic sweep timer is still real time, trigger an immediate settings sweep by changing D from 3 to 2 via `server.updateSettings`. This both crosses the effective inactivity threshold and exercises the real settings-change path. Record T0/T1, both settings patches, original persisted createdAt/activity anchors, and the offset change in the transcript. If preload proves unavailable, report that prerequisite; do not pretend Vitest TestClock applies to a separately spawned server.

For simultaneous controls, set `projectSettingsOverrides` on a second synthetic project so its retention is true while global retention is false, or run P2 as a separate fresh thread with global retention true before enabling P1. Prefer separate projects in the same fresh home to compare all three at one logical time; read back merged settings and use the actual returned project IDs. Do not try to reopen by pinning active: explicit active overrides independently prevent auto-settlement and would invalidate P1.

## Required runtime transcript, not screenshots

Capture terminal text/JSONL with server PID/head/home, hook diagnostics, sanitized RPC inputs/replies, event sequence and command IDs, stub summary responses, before projections, offset/settings change, resulting server-generated auto-settle event, and after projections. Label planned versus actual observations. The acceptance sequence is:

- BEFORE: known-open link is durably stored, no thread activity, below threshold, unsettled. The retention and unknown controls are also unsettled.
- ACTION: advance logical server time beyond threshold using documented test hook/config sequence; do not modify projection timestamps or invoke `thread.settle`/`thread.auto-settle` manually.
- AFTER: retention-off known-open fixture settles through the real worker and orchestrator. Its link still says open. Retention-on fixture and null-snapshot fixture remain unsettled through a demonstrably completed sweep. Capture the decision/sweep completion with trace instrumentation or an explicit fixture diagnostic; receipt of settings update alone is not a worker completion barrier.
- DURABILITY: restart the same isolated dev server at the same logical offset, read projection, and confirm stored link and settled state remain. No provider runs started throughout. Verify fixture events contain no user-message/run activity between BEFORE and AFTER.

Unit tests handle exact threshold equality, array ordering, and guard permutations; the live transcript handles actual persistence, worker activation, config trigger, and no-activity transition. No movie is necessary for a server-only flow; a timestamped terminal recording is optional if it makes sequencing easier to judge. Screenshots, viewport, theme, browser rendering, and web/desktop/mobile render paths: N/A. All clients consume the same server projection; no wire schema, provider adapter, transport, or client files changed. Local/remote/tunnel behavior shares this service and needs no separate visual pass. Existing docs do not require changes for this targeted behavior fix.

## Proof status and friction

No merge-readiness claim: required runtime proof and test execution are outstanding by audit scope, not demonstrated failures. No confirmed new correctness defect found from this read-only pass; the fixture matrix identifies evidence gaps, especially persisted-link-only worker controls and mixed-link precedence.

FRICTION: Runtime minimum is one day; no existing clock/config acceleration hook found. Proposed preload must be validated before claiming a real threshold crossing.

FRICTION: Forge summaries use batched HTTP GraphQL, not gh pr view. A CLI-only stub can leave snapshot null and accidentally test the conservative path instead of the bug.

FRICTION: Fresh worktree dev starts can auto-copy live data. Both explicit /tmp/ev-19738 home and --no-seed/T3CODE_DEV_SEED=0 are mandatory; never run migration seeding here.

FRICTION: Settings RPC completion is not settlement-worker completion. Negative controls need a completed sweep milestone; mocked dispatch IDs and an arbitrary wait do not prove committed live behavior.

FRICTION: This pass deliberately executed no tests or runtime captures. The matrix is review planning, not a substitute for the dispatcher-required test plus live transcript.
