Skip to content

[Spec 250] t3code is the front end: nested Workspace > Architect > Builders, porch gates, and approval from t3code - #266

Merged
pseudoseed merged 177 commits into
mainfrom
builder/spir-250
Aug 31, 2026
Merged

[Spec 250] t3code is the front end: nested Workspace > Architect > Builders, porch gates, and approval from t3code#266
pseudoseed merged 177 commits into
mainfrom
builder/spir-250

Conversation

@pseudoseed

@pseudoseed pseudoseed commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Closes #250. Refs #146.

t3code is now the front end for Codev. Workspace → Architect → Builders renders as a tree in
t3code's own web app, porch gates render as their own thing rather than being smuggled into thread
titles, six builders tile at 1440x900 and seven tile 4x2 at 1920, and a gate can be approved from t3code over a same-origin proxy
with porch recording the session, machine and timestamp.

This PR is codev-side only. The customization itself lives in a private fork,
pseudoseed/t3code@codev, pinned at 2f64a1b0ee2b. What lands here is the harness that vendors it:
the pin, the regenerated contract, 33 exported patches, the tools, and the tests.

106 commits carry [Spec 250]; the rest of the branch's ~170 are porch bookkeeping
(chore(porch): ...). 11 plan phases, builder/spir-250. GitHub's own header is the live count of
commits and files — the figures here are the shape of the work, not a checksum of the branch.

What a human can see or do now

Where
An architect and its builders as a nested tree, with the project as a heading t3code sidebar
Two architects in one workspace as two subtrees, each owning its builders t3code sidebar
A gated builder's gate name, #128's question and its choices gate panel above the composer
Six builders at 1440x900, seven tiling 4x2 at 1920 /codev-builders
Approving a gate without leaving t3code gate panel → porch status.yaml

Two blocking defects the review found, and what changed

The codex lane reviewed the finished PR after claude and opencode had approved it, and found two
things eleven rounds had not. Both are fixed here; the fork commit is 2f64a1b0e.

The approval path had no answer for a dead network. send in apps/web/src/codev/approval.ts
awaited fetch bare, and four of its five call sites had no catch; GateApproval.tsx had a
finally and no catch. A proxy disconnect while opening the session, issuing the capability,
minting the nonce, or taking the synchronous fallback stopped the spinner and produced nothing
no error, no unconfirmed state. On the approval surface that is indistinguishable from having
pressed nothing, and the builder does not advance either way.

It survived every earlier round because the file's outcome vocabulary is careful — four outcomes,
unconfirmed kept apart from refusal in five places — and nobody asked what happens when fetch
itself rejects. A rich taxonomy of answers is not the same as answering.

send now returns transport failure as a value, ({reached:true} & Json) | {reached:false, error},
so the compiler asks at all five call sites. The outcomes are deliberately different: the three
pre-submit steps report a definite AGENT_UNREACHABLE_* because nothing was submitted and the
gate provably did not move, while both submit routes report unconfirmed because the request
may have arrived and only the reply been lost. Six tests, one per outcome; restoring the throw fails
all six, by rejection.

The proxy buffered the upstream response with no limit — the same defect as the request-body
bound fixed during phase 10, on the return path, where the bytes come from configuration rather than
from an authenticated caller. MAX_PROXIED_RESPONSE_BYTES at 1 MiB, a new oversized outcome that
is neither unreachable nor a truncated answered, and an upstream abort. The first version of the
fix settled after destroy(), which emits error synchronously — so it reported a reachable host
as unreachable until the test caught it.

Criteria 6 and 9 are unchanged: codex is right that a runbook is not acceptance evidence, and this
body says so below rather than claiming otherwise.

Acceptance

10 of 11 criteria met. Criterion 6 is UNMET and says why. Criterion 9 is met under the plan's
amended reading and not under a literal one.
Full table with per-criterion evidence in
codev/resources/250-acceptance-evidence.md; the numbers in it are regenerated from the runs by
tools/t3-server/collect-spec-250-evidence.mjs, not typed.

Criterion 9 wants reading before you accept it. Its wording asks for a clean rebase and a
shape-check on the rebased tree. One of those is now run and one is not.

Regeneration is proved. The generator runs to completion against the rebased tree and the
regenerated contract is compared byte for byte to what is vendored here: schema.json, schema.ts
and types.d.ts all move. generate.mjs refuses any checkout whose HEAD is not pin.commit, so
this happens in a second throwaway — git merge-tree gives the merged tree an identity inside the
scratch clone, and a scratch copy of the codegen tool reads a scratch pin naming it. The guard is
satisfied rather than bypassed, and the real pin.json is neither read nor written.

The rebase does not complete cleanly. It stops at commit 6, which the plan states is the
result the drill exists to produce, not a failure. The plan's phase 11 amendment settles the rest:
criterion 9 is met by the procedure completing and reporting. The evidence document sets the four
clauses against what was actually run so the reading is yours, not inferred.

Criterion 6 needs an iPad on the tailnet and no device was available. It closes unmet with an
executable 16-step runbook (codev/resources/250-ipad-acceptance-runbook.md), not as passed and not
left open. The Playwright suite is not recorded as a substitute: it drives the same proxy in a
desktop browser, so it covers the approval path, and covers neither tailnet reach nor touch targets.

Runs behind the table:

  • 32 Playwright tests against t3code's real web app, at the pinned fork head
  • 8 vitest e2e through the real fork server's proxy
  • 7396 + 180 codev tests, 58 skipped, 0 failed
  • fork: 2984 web, 2873 server, typecheck clean

Reviewing this

Start with tools/t3-fork/FORK.md — every fork commit, the rebase risk table with measured beside
predicted, and a "do not tidy these" section covering two deliberate anti-rebase-debt choices.

codev/reviews/250-t3code-front-end-customization.md carries the retrospective — spec compliance
per criterion, deviations, and the disposition of every consultation finding across 21 review rounds
(20 implementation, 1 plan) — over the per-phase findings log it was written as, which is kept below it unmodified.

The agy/Gemini lane produced no output for this project. It is recorded as absent from every
round rather than as an approval; Claude and opencode ran on every phase, plus codex on the plan.

The rebase drill (tools/t3-fork/rebase-drill.mjs) is worth a look on its own: it measures the
rebase job against a live upstream 104 commits ahead, in a throwaway clone, and discards its own
result if either real checkout moved. A sequential rebase stops at commit 6, the whole conflict
surface is 3 files of the 35 we modify, 4 of the 9 contract-closure files come out of the merge with
different bytes, and the contract regenerated from that tree moves 3 shape artifacts.

Its header carries the reasoning for each of those choices, including two versions of one defect it
was itself caught in: a documented outcome no code assigned, and a measurement that would have
compared the fork to itself had it been taken one line later.

Known state, stated rather than waved at

  • apps/client is kept as the fallback and frozen. Nothing from phases 7-10 is backported. One
    test fix landed (a session-status enum moved _6_7 when the contract was regenerated in phase
    5), because a fallback whose suite is red is not a fallback. Gap filed as The frozen fallback client's suite runs only in CI: root npm test filters to @cluesmith/codev #265: root npm test
    filters to @cluesmith/codev, so nothing local runs it.
  • apps/server/src/entrypoint.test.ts > matches through a symlinked entrypoint fails in the
    fork.
    Pre-existing, byte-identical to the base commit, macOS /var/private/var. Not
    skipped and not modified.
  • A page-level CSP for t3code is not done. Recorded as a follow-up in the plan; the same-origin
    proxy already gives the guarantee structurally.
  • The proxy's idle timeout does not bound a trickling upstream. Stated in agentProxy.ts rather
    than closed: a total-duration bound would have to fit the slowest legitimate answer, and the
    origin allowlist narrows the threat to the operator's own hosts.
  • SAFETY: a gate-approval notification crosses workspaces and can tell a builder its gate was approved when it was not #264 is filed and not fixed here. porch approve sends the bare project id as the afx send
    target and the recipient is tail-matched with leading zeros stripped, so a gate approval can reach
    a same-tailed project in another workspace. Folding it in would put an unrelated change in this
    PR.

Migrations

Codev's two columns stay out of upstream's numbered migration registry, via
apps/server/src/codev/schemaGuard.ts rather than a migration. FORK.md says why. The drill
re-checked the watermark against a real new upstream migration: upstream added 043, above the
042 our base leaves, so it runs.

🤖 Generated with Claude Code

pseudoseed and others added 30 commits August 30, 2026 13:30
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 1 claude lane: REQUEST_CHANGES. Migration ids 900/901 would have
silently shadowed every future upstream migration under Effect's watermark
migrator (Migrator.js:121). Codev's columns now apply via a guarded
idempotent startup ALTER outside migrationEntries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verified 042_ProjectionThreadLinkedPullRequest.ts and 7 others already use
PRAGMA table_info + conditional ADD COLUMN. Our guard is that code verbatim,
invoked from a layer after MigrationsLive rather than from migrationEntries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Architect ruled: Codev's columns stay out of upstream's numbered migration
registry, applied from a layer after MigrationsLive with a named startup
signal. Spec risk row amended under the approver's authority.

Codex round: gate revision made optional to reconcile allocation with
stale-rejection; codev:gate-write moved to its own RPC method since
RpcAuthorization scopes methods not commands; phase 6 extends
thread-backend.ts rather than adding a dead parallel module; four
persistence modules named; proxy target moved to a server-held allowlist
to close an SSRF path.

Also corrected a false claim of my own: t3code sets no page-level CSP, so
connect-src 'self' was never there to keep closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
t3code has no Playwright; apps/web is vitest-only. Criteria 5 and 5b are
browser measurements, so phases 7-9 needed a harness that did not exist
where the plan put it. It now lives in this repo, which already has
@playwright/test, and drives the fork's dev server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A per-command env prefix never reaches the child porch spawns, so the lane
fails and porch re-issues the same task indefinitely with every individual
step reporting success. Issue #261.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eck the closure

sidebarProjectGrouping.ts groups environments, not threads, so Codev's
hierarchy composes with it rather than extending it. ThreadId is already in
the pinned closure via baseSchemas.ts, so phase 5's closure guard will not
trip on the new fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 'exit 0' in every report was tail's exit code, not consult's; run
without the pipe the lane returns 1 and names the external_directory
rejection. Porch does not spawn consult either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ective

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third lane, additive to both others. Phase 1 now rewires acquire/start/status
to upstreamBase - acquire would otherwise checkout the fork SHA into the
read-only upstream clone from an ordinary test run. Phase 4 names all four
RPC registration points and keeps gate commands out of the client command
unions. Phase 5 vendors the method via pin.methods.

Also corrects my own measurement error: the 'exit 0' reported for every
failed opencode run was tail's, not consult's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three lanes, all REQUEST_CHANGES, all findings accepted - nothing in the
disagree column. Plus two findings of my own: the CSP claim was false in
both plan and spec, and three phases planned Playwright tests the fork
cannot run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A GitHub fork inherits the source repo's visibility, so forking a public
repo cannot produce the private customization this spec rules for. Phase 1
now uses gh repo create --private with upstream as a remote, asserts the
created repo's visibility, keeps the MIT licence and attribution, and
checks the read-only upstream clone is untouched at phase end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t refresh

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two checkouts with two meanings, built while the fork head still equals
upstreamBase so every new assertion has a known answer.

- pseudoseed/t3code created with `gh repo create --private`, never
  `gh repo fork` (a fork inherits the source repo's visibility). Branch
  `codev` at 082e6ea5, checked out at /Users/chris/dev/t3code-codev.
  Visibility asserted via `gh repo view`, not inferred.
- pin.json gains upstreamBase, forkRepo, forkBranch. `commit` keeps its
  spec 146 meaning and becomes the fork head from phase 5.
- tools/t3-fork/identities.mjs resolves the mapping once; all seven
  T3CODE_ROOT readers ask it rather than re-deriving a literal path.
- t3-server.mjs: acquire, start and status pinned to upstreamBase, not
  pin.commit. acquire() writes to the read-only upstream clone and both
  smoke.mjs and live/integration.mjs call it, so pin.commit there would
  have moved that clone onto a fork sha from an ordinary test run.
  verify asserts both identities plus merge-base(commit, upstreamBase).
- classify-churn requires --upstream-movement or --fork-drift; neither or
  both exits 1. Empty ranges report NO_UPSTREAM_MOVEMENT / NO_FORK_DRIFT
  at exit 0, distinct from 1 and from 3.
- generate.mjs reads the fork; source-hash.json grows an `upstream`
  section at upstreamBase and a measured forkDrift block.
- Cold-start evidence re-collected (t3-server.mjs changed). Port 3799 was
  held by another session's server, so the run used T3_HARNESS_PORT=3811.
- /Users/chris/dev/t3code unchanged: branch main at 082e6ea5, clean.

47 new tests. pnpm -w test green, including spec-146-t3-contract with
both live suites executing rather than skipping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eption

Review nits from the claude lane. FORK.md said "nothing re-derives it"
while live/integration.mjs deliberately reads T3CODE_ROOT directly and
keeps it required (#214); the sentence was stronger than the code. Test
section heading said "seventh readers" over a list of six.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lowed git status

Both findings from the opencode/grok review.

ready() called the both-identity verify one call after start() had
deliberately dropped the fork requirement, so the exemption bought
nothing: smoke.mjs runs acquire, verify, start, ready in sequence, and
live/integration.mjs had the same shape. pin.commit does not move to the
fork head until phase 5, so on phase 2's first fork commit a correct
upstream server would have failed ready with CHECKOUT_MOVED_DURING_RUN
for a checkout the server never touches.

Adds verify-upstream and verify-fork. ready, smoke.mjs and
live/integration.mjs use verify-upstream; bare verify still asserts both,
which is the phase acceptance criterion.

verifyCheckout caught a failed `git status` and fell through to the empty
string, which is how "clean" is spelled. The comment said undetermined;
the code said fine. Inherited from the spec 146 version. Now exits 3 with
NO_<IDENTITY>_STATUS. The test triggers it with chmod 000 on .git/index,
which leaves rev-parse HEAD working and lands the failure exactly between
the two checks, and refuses to pass vacuously if the mode is ignored.

Cold-start evidence re-collected after the smoke.mjs change. 52 tests in
the spec 250 suite; pnpm -w test green at 7263 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseed and others added 11 commits August 31, 2026 05:45
…rect zero-movement drill

claude APPROVE/HIGH, opencode COMMENT/HIGH. Nothing blocking; four notes, all
actioned.

**The suite hard-asserted one of the drill's two shapes.** A `NO_UPSTREAM_MOVEMENT`
run is a pass — it legitimately carries `watermark.checked: false`,
`contractClosure.checked: false`, zero churn and no `preserved` block, because
nothing was cloned. Three assertions would have failed on it and a fourth would
have thrown. That is a test failing on a right answer, which is the mirror of the
defect this phase already spent two iterations on.

The shape is now named once and each branch asserts its OWN contract. The
zero-movement block was verified by running the suite against a synthetic
zero-movement evidence file: 6 passed, 7 skipped, where the old suite failed 3 and
threw on 1. The `if (...) return` in the whole-surface test became `it.runIf` for
the same reason a return is not a skip — vitest records it as a pass with zero
assertions.

**A comment outlived the test it described by one commit.** The drill-closure test
header still said the order of the two checks was asserted below, one commit after
that test was deleted for being unfalsifiable. opencode caught it. Same shape as
iteration 1's finding, one file over.

**The churn classification is no longer prose.** "3 source-only, 2
consumed-change-undecidable" sat outside the marker block in two documents — the
rot the collector exists to stop. `classify-churn --upstream-movement` is now
persisted to `codev/research/250-upstream-movement.json` and printed by the
collector, with two new refusals: a classification covering a different range than
the drill (exit 3 — two ranges in one table is worse than one range and a gap),
and one run against the fork rather than upstream (exit 3 — the fork answering
"what changed upstream" is a tautology). Deleting either guard fails its test.

**"contractRegeneration in every result" was not true** of the three
`could-not-run` paths. Corrected in three documents rather than padded into those
paths: `could-not-run` means nothing was learned, and a measurement-shaped field
on it is the first thing a reader would mistake for a finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
APPROVE + COMMENT, nothing blocking, four notes all actioned. The sharpest is the
one that inverts the phase's own defect: my tests asserted one of the drill's two
shapes unconditionally, so a correct NO_UPSTREAM_MOVEMENT run would have failed
three assertions and thrown on a fourth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The defect inverted: two iterations on tests that pass when they should fail, then
a test that fails when it should pass. Same missing question in both directions,
and I did not think to ask it of the tests I had just written to fix asking it of
the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he rebase, not deferred to the first real one

The architect's question, and it was worth the hour. The drill proved that the
rebase MEASURES; it did not prove the contract still REGENERATES after one. Those
are different claims, and the second is the one that matters on the day a new base
is adopted — the worst possible moment to find out it does not hold.

`generate.mjs` refuses any checkout whose HEAD is not `pin.commit`, so pointed at
this repository "regenerate from the rebased tree" means moving the real pin. The
answer is not to loosen the guard. It is to satisfy it somewhere disposable:

  1. `git merge-tree --write-tree` plus `commit-tree` give the merged tree an
     identity inside the throwaway clone. The sequential rebase stops at commit 6,
     so there is no rebased HEAD — but the generator reads only the closure, and
     the closure merges clean.
  2. A scratch codegen root is assembled beside it. generate.mjs resolves
     pin.json, its output dir and its staging area from its own file location, so
     a copy of the tool under a scratch directory reads a scratch pin naming the
     merged commit and writes to a scratch generated/. The guard is satisfied
     honestly, not bypassed: the artifacts really are reproducible from the commit
     they name.
  3. The output is compared byte for byte to what is vendored HERE — never to what
     the scratch run itself just wrote, which is the tautology this file spends
     three sections refusing.

Result: the contract regenerates, and `schema.json`, `schema.ts` and `types.d.ts`
all move. `ATTRIBUTION.md` and `source-hash.json` also differ and are listed
separately as `embedsCommitId`, because they name their own commit and would
differ after any rebase.

A regenerated contract that differs is a RESULT, not a failure — the same argument
as `conflicts` — so the outcome vocabulary stays three words and the artifact list
carries the finding.

The generator needs Node >= 22; the drill runs under 20. An interpreter that
cannot run it reports `attempted: false` with NO_INTERPRETER, never "the contract
does not regenerate" — that would be a claim about the fork made from a fact about
this machine. `T3_CODEGEN_NODE` overrides.

pin.json untouched, both real checkouts unmoved and clean, re-checked afterwards
as on every run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ew and the thread

The architect's question closed a gap two review iterations had passed over,
because every sentence that made it sound settled was true. What none of us asked
was whether the constraint had a way around it that was not 'loosen the guard'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…that backs it

7396 + 180 passed, 58 skipped, 0 failed, exit 0, after the regeneration change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… was not

The review file was 1,559 lines of per-phase findings and carried none of the
REVIEW template's sections. It answered "what went wrong in phase N" and "did
this spec get built" not at all.

Adds Summary, Spec Compliance (11 criteria with phase attribution and evidence
pointers), Deviations from Plan, Consultation Feedback, Lessons Learned,
Architecture Updates, Lessons Learned Updates and Follow-up Items. The phase log
is demoted unmodified to `## Phase-by-phase record` below them — compressing
1,500 lines of findings into four headings would lose the part worth keeping.

Consultation Feedback is reconstructed from the committed artifacts: every
verdict grepped out of the 44 raw lane files, every disposition from the
rebuttals written at the time. Two lane files carry two verdict blocks; the last
is the binding one, checked rather than assumed — taking the first would have
recorded phase 11 iteration 1 as an APPROVE when it was the round that found the
drill's `ok` tautology. The agy/Gemini lane produced no output for this project
and is recorded as absent, not as an approval.

Governance routing, with displacement:

- cold, arch.md: a `### The t3code Fork (Spec 250)` section under the existing
  `## Integration Points` — two checkouts, two pin identities, why the upstream
  clone is never checked out, why the private repo is created rather than
  `gh repo fork`ed, what `contractSource: "fork"` changes, the schemaGuard
  watermark. Under an existing top-level section deliberately: a new one would
  have forced a thirteenth entry into the hot map, capped at 12.
- hot, arch-critical.md: one fact — t3code is the front end via the private
  fork, the upstream clone is read-only, never `gh repo fork`, and apps/client
  is the FROZEN fallback. Hot because the mistake it prevents is made before
  anyone opens a reference doc.
- displacement: the file was at 10/10, so the two-tier-governance one-liner is
  demoted into arch.md's Governance section, which already stated it in full.
  Caps after: 10 facts, 12 map topics, 32 lines.
- cold, lessons-learned.md: "looks flaky, is actually under-budgeted" from phase
  11, and harness runs poisoning the following suite (#263).

35 governance tests pass; the evidence collector's --check still exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… raw lane outputs actually live

Every number in the review is meant to be checkable against the PR a reader is
looking at. Three were not:

- 105 -> 106 `[Spec 250]` commits, 128 -> 130 files. The old figures were taken
  against the local `main`, which was behind. `gh pr view 266 --json changedFiles`
  reports 130, which is what a reader sees.
- The consultation total is 21 rounds (20 implementation, 1 plan), counted from
  the artifacts. The PR body said 22.
- 43 raw lane files, not 44.

And a claim that would have sent a reader looking for files that are not there:
the raw `.txt` lane outputs are gitignored (`.gitignore:69`, `codev/projects/*/*.txt`).
They live in the builder worktree and do not travel with the PR, so the
`*-rebuttals.md` are the durable record. Said plainly rather than implied by
"alongside them".

Adds #267 to Follow-up Items: `consult --type pr` cannot review a PR over
GitHub's 20,000-line diff cap and exits 0 when it refuses. Hit on this PR at
43,714 diff lines; both lanes refused correctly and both returned 0.

Also verified rather than trusted while checking the Spec Compliance table: all
four cited `spec-250-*.spec.ts` line numbers land on the test they name, and the
"9 Playwright tests" counts in the evidence document are runtime counts — six
static tests plus a three-viewport loop — not the static declarations a grep
finds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseed and others added 6 commits August 31, 2026 06:22
…e repository it tests

PR review round 1 — claude APPROVE, opencode COMMENT, no blocking findings.

**The phase 11 rebuttal was too broad, and being too broad hid a race.** Its
argument — the test's value is that it drives the collector against its REAL
committed inputs, so fixture copies would test a copy — is true of ONE test in a
file of six. The other five work by DAMAGING an input, and a damaged input has no
reason to be the committed one.

What that concealed: `spec-250-vendoring-identities.test.ts` reads
`codev/research/250-criterion-8b-evidence.json` in its MODULE BODY. Vitest runs
files in parallel workers, so a worker collecting it while this file held the
mutation fails on corrupted data — in a file unrelated to the collector, with
nothing in its own output to explain why. I had not looked for other readers of
the paths being mutated.

The five damage cases now run a COPY of the collector under a `mkdtempSync` root.
The collector resolves its root from `import.meta.url`, so the copy reads that
tree's inputs — the same technique `rebase-drill.mjs` uses to regenerate the
contract without moving the pin. No flag is added to the tool to suit a test,
nothing tracked is written, and a killed run leaves a temp directory instead of a
mutated repository plus a stray `.spec250-test-backup`. `spawnSync('rm', ...)`
goes with the backup file it deleted.

`agrees with the committed evidence` still runs the real collector against the
real repository. That is the assertion whose value depends on the committed
files, and it is the one the rebuttal was actually about.

**Verified capable of failing, which this needed.** Three of the five assert exit
3, and MISSING_RUN — a scratch root missing an input — is also exit 3, so an
incomplete fixture would have passed them for the wrong reason. Removing all five
mutations: 5 failed, 1 passed. Restored: 6 passed, and 83 with
spec-250-vendoring-identities.test.ts alongside it.

Also from the round:

- Two commit counts reconciled and labelled. The branch is 167 commits, 106 of
  them `[Spec 250]`; both figures were stale against a local `main` behind
  `origin/main`.
- #268 filed. `status.yaml` history records 9 rounds against 20 that ran, and the
  pattern is exact rather than a gap: a round is recorded if and only if at least
  one lane did not approve. Phases 7, 8 and 9 are absent entirely — the three
  where both lanes approved on round 1. A phase reviewed cleanly reads identically
  to one never reviewed. Not hand-edited.
- #267 filed before the round could run: `gh pr diff` returns HTTP 406 over
  GitHub's 20,000-line cap and both lanes refused correctly, then exited 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n, and two counts that read as contradictions

Three things a reader of PR #266 would have got wrong.

**Cited paths cross two repositories with nothing saying so.** Criterion 8 and
11 point at `apps/server/src/codev/schemaGuard.test.ts` and
`threadHierarchy.test.ts`. Neither is in this repository — they are in the fork
at `pseudoseed/t3code@codev`, and a reader chasing them from the PR finds
nothing. Spec Compliance now opens with the split: `packages/`, `tools/`,
`codev/` are here and travel with the PR; `apps/server/` and `apps/web/` are the
fork's; `apps/client` is the exception that IS here, frozen; and the Playwright
`.spec.ts` files are here and run against the fork's app.

**"10 of 11 criteria" over 13 checkboxes.** Both are right — the spec numbers 11
criteria and adds two sub-criteria, 5b and 8b. Said as 10 of 11 numbered, 12 of
13 counting sub-criteria, so the table and the sentence stop disagreeing.

**Phase 11's "Rebutted" line contradicted the review round that overturned it.**
It now says so and points at both documents: the argument held for one of the
file's six tests, was applied to all six, and concealed a cross-file race.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y commit invalidates

The review's summary carried an exact commit and file count. Both were corrected
once this round already, and adding the round's own rebuttals moved the file
count again — 130 to 131. A figure that changes when you write about it is a
figure that will be wrong in the version anyone reads.

The tagged-commit count stays, because it describes the work. The branch and file
totals become approximate and point at `gh pr view 266 --json commits,changedFiles`
for the live number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t commit

`spec-1280-measurement-instrument.test.ts > T12 — determinism` failed in the
full `src/__tests__/` run (1 failed, 1859 passed). I was committing review fixes
while that suite ran; T12 invokes the instrument twice and compares bytes, so a
commit landing between the two invocations changes what it reads.

Re-run alone: 24 passed. Not flaky, not skipped — the test caught precisely what
it exists to catch. Recorded with the corollary: do not commit while a suite that
reads git state is running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pseudoseed

Copy link
Copy Markdown
Owner Author

3-way integration review

Lane Verdict
claude APPROVE (HIGH)
codex (gpt-5.6-sol) REQUEST_CHANGES (HIGH)
opencode COMMENT (HIGH)

Note on how this was reviewed. The first attempt produced nothing: gh pr diff returns HTTP
406 over GitHub's 20,000-line API cap and all three lanes refused with "produced a 0-byte diff —
refusing to run a review on nothing."
That refusal is the only reason this was reviewed at all
rather than assumed. Re-run with --base main, which computes the diff locally. Filed as #267.

Two blocking, both from codex, both real

1. Pre-submit network failures escape as rejected promises — on the approval path

approval.ts:102-123 does not catch fetch, and GateApproval.tsx:95-130 has only a finally.
So a proxy disconnect during session creation, capability issuance, nonce minting or the
synchronous fallback gives the user no error and no unconfirmed state.

This is the defect class this project has spent 11 phases eliminating, landing on the highest-stakes
surface in the product. A gate approval that silently does nothing is worse than one that fails
loudly: the human believes they approved and the builder never advances, and nothing anywhere says
which happened.

Wanted: an explicit named outcome for each pre-submit failure, and a test per outcome.

2. The proxy bounds request bodies and not responses

agentProxy.ts:469-478 buffers the upstream response with no limit. You found and fixed the
unbounded request-body buffer before review; this is the same defect on the return path. A faulty
or compromised configured agent exhausts server memory. Bound it and abort the upstream response
when exceeded.

Not changes — my rulings, already disclosed in the body

  • Criterion 6 unmet. Codex is right that a runbook is not acceptance evidence, and the PR says
    exactly that. No iPad was available. It ships unmet with a reason, which is the honest outcome.
  • Criterion 9 met under the amended reading. The amendment is recorded in the plan with my
    ruling behind it: the drill runs on a throwaway clone and the real pin does not advance, because
    moving it makes every spec 146 and 236 result untestable. The body tabulates the literal gap
    rather than claiming the literal reading. That is disclosure, not a change to make.

Follow-up, do not fix here

Claude's point, and it pairs with #265: the vendored contract now sources from a private repo, and
40 e2e tests live permanently outside CI. Both are "the only thing that proves this works runs
nowhere automated." File it.

Also noted: arch-critical.md is at its 10-fact cap, so the next hot-tier addition needs an
explicit demotion.

Verified myself rather than read

  • pseudoseed/t3codevisibility=PRIVATE, isFork=false
  • /Users/chris/dev/t3code — HEAD 082e6ea52186, clean tree
  • pin.json — both identities present, contractSource: fork

pseudoseed and others added 3 commits August 31, 2026 06:53
…rk, the proxy bounds the way back

Round 2 of the PR review. codex REQUEST_CHANGES with two blocking findings,
after claude APPROVE and opencode COMMENT had cleared the same code. Both real,
both fixed. Fork commit `2f64a1b0e`, pushed; `pin.commit` moved with it.

The fork-side change and its reasoning are in that commit and in
tools/t3-fork/FORK.md. What lands here is the pin, the regenerated contract, the
re-exported patches, the re-run evidence, and the record.

## What the pin move actually cost, which REFRESH.md understated

A fork commit invalidates every evidence run that names a fork commit — four of
them — and `collect-spec-250-evidence.mjs` refuses with STALE_RUN rather than
publishing numbers about a fork nobody is looking at. So criterion-8b, the
hierarchy wire evidence, the churn classification and the rebase drill all had to
be re-run before the evidence would regenerate. All green.

That step did not exist in REFRESH.md, which said to regenerate and commit. It is
the step that gets forgotten precisely because a fork commit touching no closure
file changes nothing in the contract except a sha — so regeneration looks like the
whole job while the acceptance document quietly goes on describing the previous
fork. Now step 8, with the port guidance the re-runs needed.

## A hand-typed number, outside the block that exists to prevent hand-typed numbers

The drill carries 43 commits now, not 42. "Stops at commit 6 of 42" appeared twice
in prose OUTSIDE the generated block, so the collector could not reach it and it
was wrong the moment the fork moved. Both now name the stop point and point at the
generated row for the total.

## Regeneration

Only the provenance sha moved: `schema.json`, `LOSSY.md` and `UNREPRESENTED.md`
are byte-identical, because the change touches `apps/` and no closure file.
`verify` clean on both identities, merge-base intact, 39 contract tests pass, and
`--check` exits 0.

Fork suites: web 2990 passed, server 2875 passed with only the pre-existing
entrypoint symlink failure, contracts + server + web typecheck clean.

## Also

- #269 filed at the architect's direction: nothing automated runs the 40 e2e tests
  or the contract check, and the vendored contract's source is a private repo.
- #264 fired a third time — a message claiming the pr gate was approved arrived
  while this builder's own Playwright fixture was running. `porch status` said
  WAITING FOR HUMAN APPROVAL. Not acted on; evidence added to the issue. What
  stopped it was a note in an untracked state file, which is a habit, not a
  control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ribes

32 passed at fork head 2f64a1b0ee2b. The previous run described 3786b840e1a4,
and an e2e result that names a fork nobody ships is the same defect as a stale
evidence number — round 1 had already flagged that staleness once.

The first attempt came back 4 failed, 28 did not run, and it was self-inflicted.
`T3_HARNESS_PORT=3830` is required by the evidence runs, because other sessions
hold 3799 and 3823 and the harness rightly refuses to kill what it does not own.
I carried it into the Playwright command, whose documented form omits it: the
fixture's fork server went to 3830 while the dev server on 5733 proxied to a port
with nothing behind it, so the page served 200 with no threads in it and every
locator timed out against a working server.

A variable that fixes one tool can break the next one in the same shell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pseudoseed
pseudoseed merged commit 357a364 into main Aug 31, 2026
9 checks passed
pseudoseed added a commit that referenced this pull request Aug 31, 2026
pseudoseed added a commit that referenced this pull request Aug 31, 2026
…asurement that nearly outlived what it measured

PR #266 merged as 357a364; issue #250
auto-closed. Worktree merged to the integrated state.

Verification on the merged tree:
- codev suite 7396 + 180 passed, 58 skipped, 0 failed, under the default node
- `t3-server.mjs verify` clean on BOTH identities: upstream at 082e6ea52186,
  fork at 2f64a1b0ee2b on that base
- `collect-spec-250-evidence.mjs --check` exits 0, so the acceptance numbers
  still describe the fork that shipped
- patch 0034 and the moved pin confirmed present on origin/main

The merge brought in one spec edit from main: the CSP correction now stands in
the spec as well as the review — the same-origin guarantee is structural and a
test obligation, not a header, because upstream sends no page-level CSP.

Records the lesson from the merge itself. The approval said "CI is green 9 of 9";
`gh pr checks` said 9 pending. Both were true about different commits — the green
run was on the last commit carrying code, and porch had since committed a
status.yaml-only change that started a fresh run. Merged on green-on-the-code plus
a state-file-only delta and reported it as exactly that.

That is this project's own recurring defect aimed at a status report: a number
carried forward past the thing it measured stops being a measurement and becomes
a claim. Routed cold, with the note that it applies to reports, not only to code —
a stale green in prose is as misleading as a stale number in a document and harder
to catch, because nobody diffs prose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

t3code is the front end: private customization for nested Workspace > Architect > Builders, and porch gates in its UI

1 participant