Fix #272: every workspace is a named top-level node in the sidebar tree - #305
Merged
Conversation
…its directory Two things were wrong on top of a workspace-to-project mapping that works. **The title was a path with a prefix.** `codev:/Users/chris/dev/codev-1455` is what the sidebar heading renders, verbatim — a single-member project group's label IS its title, through `projectGrouping.ts` and `sidebarProjectGrouping.ts` with no cleanup step between them. A column of absolute paths does not scan, and at a narrow width every workspace truncates to the same leading characters. The connect path writes the leaf name now. **A workspace nobody had spawned into had no project row at all.** Not "created on first thread creation", which is what the issue says: it is created by `initialiseThreadBackend`, the thread-backend CONNECT, and only for the workspace being connected. Tower's existing sweep does not close the gap either — it enumerates `architect UNION builders`, and a workspace with neither is exactly the one that was missing. So: a reconciler over `getKnownWorkspacePaths()`, at startup and every 30 s. **One connection per SERVER, not per workspace.** The obvious implementation is `ensureThreadBackendReady(root)` for every known root — it already creates the project. It also installs a live engine and holds a WebSocket for the life of the process, so it would open roughly one socket per known workspace, against what is usually one server, to write one row each. Roots group by the server they name; reads are plain HTTP and the socket opens lazily, so a pass with nothing to do — every pass after the first — never opens one. **Three filters on the enumeration, one per case the real table contains.** Today `known_workspaces` holds `/Users/chris/dev` (a parent directory a terminal was opened in), several deleted checkouts, and `.builders/` worktrees. A project minted for any of them is a permanent sidebar heading for something no Codev command would accept. **Only two titles are ever rewritten, and this codebase wrote both.** A sweep that enforced a computed title would undo a rename made in the t3code UI, silently, every 30 s. So: the legacy `codev:<the project's own workspace root>` (compared canonically, so a project stored under `/private/var` is still recognised as ours while `codev:` in front of an unrelated path is not), and the workspace's own leaf name. The second is what makes a project created by a spawn converge on the set-wide unique name — the connect path knows one workspace and cannot see a collision, so `api` and `api` need the sweep to deepen them to `backend/api` and `mobile/api`. `readProjectRows` is split out of `activeProjectForWorkspace` rather than copied: the reconciler needs the whole list and the title on it, and the previous reader kept only `id` and `workspaceRoot` — a reconciler reading that can tell a project exists and can never tell what it is called. A second copy of the request would also be a second place for the transport rules to drift, which is how that call skipped `assertTransportSafe` once already. The sweep never throws. A server that is down is a "not yet", and one unreachable server must not hide every workspace behind every other one. Refs #272, #250. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The unit suite drives the reconciler with a fake gateway, which proves the DECISION and nothing about the wire. The gateway is exactly the part a fake substitutes away — a bootstrap exchange, an HTTP snapshot read, a lazily-opened socket carrying `project.create` and `project.meta.update` — and spec 250's own review has a name for asserting a value at both ends and never carrying it through the code a human runs. So `tools/t3-fork/issue-272-projection.mjs` runs the real gateway against a live fork server and reads the rows back out with sqlite rather than through the code under test. It asserts the rename keeps the project id (a second `project.create` for one workspace root is refused by the server, so a "rename" that re-created would have failed the sweep — but the id says so rather than leaving it inferred), that a human's title survives, that two `api` workspaces come out distinguishable, and that a second pass writes nothing. Exit 3 when it could not tell — a missing checkout, a server that would not start, an unreadable database — and never spelled like a pass. The unit fixtures use the paths the real `known_workspaces` table holds today: a parent directory a terminal was once opened in, deleted checkouts, and `.builders/` worktrees. Every filter exists because that table contains a row it would let through. The connect-path title is a source guard, in the pattern `issue-227-thread-seams.test.ts` uses for CLI wiring that cannot be driven without a live server. It sits past a token exchange, a ticket and an upgrade; the live run above is the behavioural proof and this is the durable half. Every new assertion was confirmed to fail with its change reverted. Refs #272. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`pin.contractSource` is `fork`, so a fork HEAD ahead of `pin.commit` makes `t3-server.mjs verify` exit 1 and turns the suite red. The Sidebar commit is a fork commit, so REFRESH.md steps 3-8 are part of this work rather than a follow-up. `pin.commit` → `26b4c2dc09f0`. The regenerated artifacts are byte-identical apart from the sha, which is what a Sidebar-only commit should produce — the closure is `packages/contracts/src` and nothing there moved. Checked rather than assumed; 0 unrepresented, so nothing Codev consumes lost its schema. 35 patches now, one more than before. Four evidence runs re-collected against the new head, because REFRESH.md is explicit that this is the step that gets forgotten: a fork commit touching no closure file changes nothing but a sha, so regeneration looks like the whole job while the acceptance evidence goes on describing the previous fork. - criterion 8b: passed - hierarchy wire: 8 of 8 - rebase drill: still `conflicts`, still the same 3 files, 44 commits carried - upstream movement: 2 undecidable, 3 source-only - `collect-spec-250-evidence.mjs --check`: exit 0 `tools/t3-fork/issue-272-projection.mjs` is the live proof of the Codev half, and it passed all 8 claims against a fork server: three projects created and named after their directories, one legacy `codev:<path>` title rewritten in place keeping its id, `Entriq (do not rename)` untouched, `backend/api` and `mobile/api` distinguishable, nothing minted for a builder worktree or a deleted checkout, and a second pass writing nothing. The acceptance-evidence header now says which rows describe an earlier fork head and were NOT re-run. Rewriting the commit in a historical row without re-running it would turn a record into a claim. Refs #272, #250. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h stops shipping a username `bugfix-214-publish-scrub` bans `/Users/<real>` and `/home/<real>` from every shipped file, and `workspaceDisplayNames`' worked example was `/Users/chris/dev/codev-1455` — which compiles into `dist` and would have gone out in the package. The guard accepts `<anything>` as a placeholder, so it reads `/Users/<user>/dev/codev-1455` now. My first fix was `/home/dev/...`, which is the same violation with a different prefix, and I only noticed because I re-ran the guard rather than trusting a grep I had written for the wrong pattern. **The empty heading, in a browser.** The unit test proves the ORDER contains the entry; this proves something draws it. `seedHierarchy` gains a second project with no threads at all — the ordinary state of a workspace nobody has spawned into, now that Codev registers one per workspace — and the spec asserts its heading exists, carries `data-codev-project-empty`, sits below the populated project's run, and has every architect subtree above it. The existing heading assertion is scoped to the populated project's title, because counting both would make it an assertion about the wrong thing. The idle project's workspace root is a real temp directory: the server refuses a project whose root does not exist, and it cannot live under the fork, because `start-fork` refuses a dirty fork checkout and a fixture that wrote there would poison the next run. 10 of 10 in `spec-250-hierarchy.spec.ts`, and the re-shot sidebar screenshot shows `idle-workspace` with nothing under it. Refs #272. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
porch runs nvm node 20; my shell is Homebrew node 26. better_sqlite3.node was ABI 147 and node 20 needs 115, so 729 tests across 49 files failed on one binary while every run I drove passed. pnpm install, pnpm rebuild and build_from_source were all silent no-ops -- the mtime stayed Jul 24 through all three -- and there is no node-20 prebuild for darwin arm64, so only node-gyp rebuild fixes it. Recorded with the repair and the ruled-out hypotheses, because porch truncates a failed check to 5 lines and the cause names itself nowhere. Refs #272, #304. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The retrospective, plus two facts routed to the COLD tier and neither displacing a hot entry: arch.md gains the workspace-to-project projection under Integration Points and a Node-20/ABI invariant, and lessons-learned.md gains the two debugging lessons the nine failed gate attempts paid for. Nothing promoted to the hot files. Both are at their 10-entry cap, the hot tier already carries the t3code-fork fact this builds on, and the Node/ABI fact is repo-specific rather than a cross-cutting decision rule — which is the routing the update-arch-docs skill prescribes. Refs #272. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n oversight docs/codev/spec-250/phase-7/*.png in the fork predate this change and do not show the empty-workspace heading. Re-shooting them costs a second fork commit and the whole REFRESH tail again — pin move, regeneration, patch re-export, four evidence re-runs — for pictures that still accurately depict what spec 250 delivered. Recorded in Things to Look At so a reviewer finds the reasoning rather than the gap. The behaviour is pinned by spec-250-hierarchy.spec.ts, which asserts the heading's presence, position and data-codev-project-empty attribute. Refs #272.
…ituted The review consultation caught that isCodevWorkspaceDirectory is injected away by the unit tests AND by the live harness, so the filter that actually decides 'deleted checkout' and 'not a workspace' in Tower had no coverage — the repo's own lesson about a test supplying the boundary it means to check, in the code I wrote the day I quoted it. Testing it for the first time surfaced a real defect rather than confirming the code: existsSync(join(path, '.codev')) accepts a .codev that is a FILE. codev init creates a directory; the check now requires one. The assertion fails with the guard reverted. Five tests against a real filesystem: .codev present, absent, path missing, path is a file, and .codev is a file. Refs #272.
This was referenced Aug 31, 2026
pseudoseed
added a commit
that referenced
this pull request
Aug 31, 2026
pseudoseed
added a commit
that referenced
this pull request
Aug 31, 2026
chore(porch): pir-272 state commits stranded after #305 merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PIR Review: Every workspace is a named top-level node in the sidebar tree
Fixes #272
Summary
The sidebar tree showed one project per path string — titled
codev:/Users/chris/dev/codev-1455— and only for workspaces something had already been spawned into. This names each project after
its own directory and adds a Tower sweep that reconciles a project row for every workspace Codev
knows about, so a workspace nobody has started work in appears as a heading with nothing under it
rather than not appearing at all. The fork change is what makes that heading render: the tree's
project level was derived entirely from architects, so a project with none drew nothing.
Files Changed
packages/codev/src/agent-farm/workspace-projection.ts(+382 / -0) — new; the decisionpackages/codev/src/agent-farm/workspace-projection-sweep.ts(+144 / -0) — new; the wiringpackages/codev/src/agent-farm/thread-backend.ts(+151 / -12) — leaf-name title,readProjectRows,openProjectGatewaypackages/codev/src/agent-farm/servers/tower-server.ts(+37 / -0) — sweep lifecyclepackages/porch-driver/src/thread.ts(+25 / -0) —updateProjectMetapackages/codev/src/agent-farm/__tests__/issue-272-workspace-projection.test.ts(+411 / -0) — newpackages/codev/src/agent-farm/__tests__/issue-272-project-title.test.ts(+163 / -0) — newpackages/codev/src/__tests__/e2e/spec-250-fork-stack.ts(+43 / -0) — seeds a project with no threadspackages/codev/src/__tests__/e2e/spec-250-hierarchy.spec.ts(+53 / -0) — the empty-heading testtools/t3-fork/issue-272-projection.mjs(+297 / -0) — new; live verification against a fork servercodev/research/272-workspace-projection-evidence.json(+90 / -0) — its recorded outputcodev/plans/272-the-sidebar-tree-needs-every-w.md(+251 / -0)codev/state/pir-272_thread.md(+248 / -0)In the fork (
pseudoseed/t3code@codev), commit26b4c2dc0:Sidebar.logic.ts,Sidebar.logic.test.ts,Sidebar.tsx. Readable intools/t3-fork/patches/0035-*.patchwithoutaccess to the private repository.
The pin move and regenerated contract that commit obliged shipped separately as PR #291, because
four builders were blocked on
criterion 8bwhilepin.jsonnamed a fork commit that was no longerchecked out. That is why they are not in this diff.
Commits
59020b24d[PIR The sidebar tree needs every workspace as a top-level node, named, not one project per path string #272] feat: every known workspace is a project row, named after its directoryd07b066b1[PIR The sidebar tree needs every workspace as a top-level node, named, not one project per path string #272] test: the rules without a server, and the wire with one3c25fe8b5[PIR The sidebar tree needs every workspace as a top-level node, named, not one project per path string #272] chore: move pin.commit, regenerate, and re-run the evidencecb513dfd4[PIR The sidebar tree needs every workspace as a top-level node, named, not one project per path string #272] test: the browser draws the empty heading, and the doc path stops shipping a username8074a0317[PIR The sidebar tree needs every workspace as a top-level node, named, not one project per path string #272] docs: the ABI mismatch that failed the gate 8 timesTest Results
npm run build: ✓ passnpm test: ✓ pass — 7499 passed / 58 skipped (388 files), plus 180 incodev-v2. 35 new.porch check 272under Node 20, the runtime porch actually uses: ✓ build 17.6s, ✓ tests 227.1sSidebar.logic.test.ts125 passed (8 new)spec-250-hierarchy.spec.ts10 passed, including the empty-workspace headingrendered in a browser
tools/t3-fork/issue-272-projection.mjs): 8 of 8 claims—
codev-1455created and named;codev:<path>rewritten in place keeping its project id;Entriq (do not rename)untouched;backend/apiandmobile/apidistinguishable; nothing mintedfor a builder worktree, a deleted checkout, or a non-workspace; a second pass writes nothing
Manual verification at the
dev-approvalgate: build 17.6s, tests 216.7s, both green.Architecture Updates
Two facts routed to the COLD tier; neither displaced a hot entry.
codev/resources/arch.md§ Integration Points — a new subsection on the workspace→projectprojection: the two writers (connect path and sweep), why the sweep enumerates
getKnownWorkspacePaths()rather thanarchitect ∪ builders, that the project title is thesidebar heading verbatim, that it opens one connection per server, and which two titles it will
rewrite.
codev/resources/arch.md§ Invariants & Constraints, new Architects deregister on any session death except a graceful workspace stop (main + main2 lost on pseudoapps) #10 — porch and afx run nvm Node 20,so every native module in a worktree must match ABI 115;
better-sqlite3ships no Node-20prebuild for darwin/arm64, so only a from-source rebuild produces a loadable one.
Nothing was promoted to
arch-critical.md: it is at its 10-fact cap, the hot tier already carriesthe t3code-fork fact this builds on, and neither addition is worth displacing an existing entry.
The Node/ABI fact is repo-specific rather than a cross-cutting decision rule, which the
update-arch-docsskill routes to cold.Lessons Learned Updates
Two entries added to
codev/resources/lessons-learned.md§ Debugging and Root Cause Analysis,both COLD:
passes for another, stop diffing the inputs and diff the loader — capture live process ancestry
rather than trusting either party's belief about how it runs. And when hundreds of tests across
dozens of unrelated files fail at once, the cause is one shared dependency, not hundreds of
defects.
code. Three build commands exited 0, printed nothing, and left the binary untouched.
Nothing promoted to
lessons-critical.md, for the same cap-and-displacement reason.Things to Look At During PR Review
isMachineWrittenTitleis the safety boundary of a sweep that runs every 30s. It permitsexactly two strings — the legacy
codev:<root>form and the workspace's leaf name. If it wereloosened, the sweep would start overwriting titles humans chose. The leaf-name case is deliberate
and is what lets a project created by a spawn converge on a set-unique name.
CodevSidebarEntrygained a case that carries no thread. Six call sites didentry.thread,including
orderedActiveThreads, which is what shift-range-select and jump-hint labels areassigned from —
undefinedthere is a crash one row away.codevOrderedThreads/codevEntryThreadare the answer. An optionalthread?: Twas the alternative and is worse:entry.thread.idwould keep compiling and fail at runtime on the one kind that has no thread.projectGroups, notprojectKeys. The sidebar's list is logical projects whileprojectKeyOfreturns a physicalenvironmentId:projectId. A flat key list draws a second emptyheading beside a group whose other member holds the architects, same name on both.
readProjectRowswas split out ofactiveProjectForWorkspacerather than copied. The oldreader dropped
title, so a reconciler could tell a project existed and never what it was called.A second copy of that request would also be a second place for the transport rules to drift —
which is how that call skipped
assertTransportSafeonce already.The fork's committed screenshots are now one case out of date, deliberately.
docs/codev/spec-250/phase-7/*.pngin the fork depict the tree before this change and do notshow the empty-workspace heading. Re-shooting them means another fork commit, which obliges the
whole
REFRESH.mdtail again — a second pin move, regeneration, patch re-export and fourevidence re-runs — for pictures. They still accurately depict what spec 250 delivered. The
current render is in this branch at
packages/codev/test-results/spec-250-screenshots/phase-7/(untracked), andspec-250-hierarchy.spec.tsasserts the heading's presence, position anddata-codev-project-emptyattribute, so the behaviour is pinned by a test rather than by an image.
Consultation findings and what I did with them
Both reviewers returned APPROVE (claude, opencode — this repo runs a 2-way pass per
.codev/config.json, not the 3-way the protocol prompt describes). NoREQUEST_CHANGES.Claude raised four non-blocking items:
isCodevWorkspaceDirectorywas substituted by every test and by the live harness — FIXED.The unit tests inject
isCodevWorkspaceandissue-272-projection.mjs:211injects its ownpredicate, so the filter that actually decides "deleted checkout" and "not a workspace" in Tower
was never executed by anything claiming to cover it. That is exactly the repo's own hot-tier
lesson about a test supplying the boundary it means to check. Five tests added against a real
filesystem — and writing them surfaced a second, real defect:
existsSync(join(path, '.codev'))accepted a
.codevthat is a file.codev initcreates a directory; the check now requiresone. Confirmed to fail with the guard reverted.
createWorkspaceProjectionSweeperhas no test, andoptions.depsexists for tests notwritten — acknowledged, not fixed here. The sweeper is interval plumbing over
reconcileWorkspaceProjects, which is covered; the untested part isstart/stop/overlapbehaviour. Worth a follow-up rather than widening this PR.
change worth knowing. A pairing-issued one-time token is now spent on the first tick, before
anyone spawns. The constraint is pre-documented on
ThreadBackendConfig.bootstrapToken, but thismakes an unbounded desktop seed effectively mandatory for any workspace carrying a
threadsconfig. Not changed here because caching a credential across processes is a storage decision.
fix; deferred as a follow-up rather than folded in.
Items 2-4 need issues; I did not open them because that call is the architect's.
How to Test Locally
pir-272→ Review Diffnode tools/t3-server/t3-server.mjs start-fork, then from the fork'sapps/web:T3CODE_SINGLE_ORIGIN_DEV=1 T3CODE_PORT=3811 PORT=5733 npx vp devWhat to verify:
codev-1455, notcodev:/Users/chris/dev/codev-1455sqlite3 <db> "select title, workspace_root from projection_projects;"— one row per real Codevworkspace, none for a
.builders/path or a deleted checkoutwhy the name is a name and not a path
Reproduce the live check directly:
Run anything in this worktree with nvm Node 20 first in
PATH. A green under a newer Node saysnothing about what porch will do — see the thread log.
Flaky Tests
None skipped. One documented, not skipped:
packages/codev/src/__tests__/e2e/spec-250-approval.spec.ts— intermittent 30s timeout in theopenThreadhelper waiting for asidebar-row-card. Across five runs it failed at fourdifferent tests (lines 180, 213, 261, 305) and passed 6/6 once. Confirmed not caused by this
change: an A/B with this branch's fixture edits reverted to HEAD still failed, at a third
different test. Left enabled rather than skipped — skipping would remove coverage of the approval
path to hide a timing artifact this PR did not introduce.