Fix #264: address the gate notification by worktree, not by project id - #270
Merged
Conversation
A gate is where a human's authority enters the system. A message saying a gate was approved, delivered to a project that approved nothing, is a channel around that authority. Two hops composed to make one. `porch approve` sent `afx send <projectId>`, and `afx send` resolved the workspace from the SENDER's session — its CODEV_THREAD_ID / CODEV_BUILDER_ID, else cwd — then matched the agent by tail with leading zeros stripped. So `250` reached `builder-spir-250` in whichever workspace the sending process belonged to, and an approval for a throwaway project in a temp workspace woke a live builder somewhere else. Neither hop carried the identity of the project whose status.yaml had actually been written. The cluesmith#1094 guard did not fire because it checks that the sender's identity is VERIFIABLE, not that the recipient is CORRECT. - porch addresses the project's own worktree: `notifyGateApproved` sends `afx send --worktree <artifactRoot> --exact`. artifactRoot is the directory whose status.yaml was written, so it names one builder in one workspace or it names nothing. `notifyProtocolComplete` is pinned the same way — it prompts `afx cleanup`, which is destructive. - `afx send --worktree <path>` resolves the recipient AND the resolution workspace from that path, scoped to the workspace that owns it. A worktree no builder owns throws, naming the worktree and listing who is registered. `fromWorkspace` still comes from the sender; those were one value, now two. - `afx send --exact` turns off the builder tail match in both Tower resolvers, live and the offline-hold registry. The miss names the address, the workspace, and the builders actually there. - The message names the project and workspace, says outright that it is not an approval and carries no authority, and says what to check it against and what a mismatch means. Verified against the original reproducer: a `porch approve` for a project in a temp workspace now delivers nothing and logs the named miss. A send addressed to a real builder's worktree still delivers. Regression tests confirmed to fail with each half of the fix backed out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CMAP feedback (claude, HIGH). Two non-blocking items. `exact` short-circuited before the SHELL lookup as well as the tail match, so `afx send --exact <shell-id>` would have failed with "is not a builder in workspace". Unreachable today — porch is the only --exact caller and always resolves to a builder id — but the flag is public CLI surface and its name promises less than it did. The tail match is the only fuzziness being refused; a shell is matched by its full id already. Pinned by a test that resolves both a shell and an architect under `exact: true`. The JSDoc for resolveRecipientWorktree was orphaned above workspaceForWorktree when the two were split, leaving resolveRecipientWorktree undocumented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pseudoseed
added a commit
that referenced
this pull request
Aug 31, 2026
3 tasks
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.
Summary
A gate-approval notification could be delivered to a project that approved nothing.
porch approveaddressed the recipient by bare project id, andafx sendresolved the workspace from the sending process's session and then matched the agent by tail — so250reachedbuilder-spir-250in whatever workspace the sender belonged to. An approval for a throwaway project in a temp workspace woke a live builder somewhere else, with a message indistinguishable from a real one.This is a safety defect rather than a noisy one. A gate is where a human's authority enters the system; a notification that arrives without it is a channel around the
--a-human-explicitly-approved-thisapparatus.Fixes #264
Root Cause
Two hops composed, neither carrying the identity of the project whose
status.yamlwas actually written.Hop 1 — the recipient was a bare project id.
commands/porch/index.ts:1267senttarget: state.id, whichnotify.tsturned intoafx send <projectId> "<msg>" --raw.Hop 2 — the workspace came from the sender, and the agent was tail-matched.
agent-farm/commands/send.tsresolves the workspace fromCODEV_THREAD_ID, elseCODEV_BUILDER_ID+CODEV_WORKTREE_ROOT, else a cwd walk-up. The issue attributes this to cwd; the launch-identity env actually wins over cwd, which is why a Playwright suite running inspir-250's pane made a temp-workspace approval resolve to/Users/chris/dev/codev-1455.servers/tower-messages.tsthen matchedbuilderId.endsWith('-' + stripLeadingZeros(agent)).The cluesmith#1094 guard did not fire because it checks that the sender's identity is verifiable, not that the recipient is correct. Both were perfectly verifiable.
Reproduced, both directions
Built a throwaway porch project in a temp workspace with a pending
prgate and ranporch approvefrom it. Before the fix the message landed inbuilder-bugfix-264's pane while that project's ownprgate stayedpending. Withenv -u CODEV_BUILDER_ID -u CODEV_WORKTREE_ROOT -u CODEV_THREAD_ID, nothing was delivered — the delta between those two runs is the whole mechanism.Fix
notifyGateApprovedsendsafx send --worktree <artifactRoot> --exact.artifactRootis the directory whosestatus.yamlwas just written, so it names one builder in one workspace or it names nothing.notifyProtocolCompleteis pinned the same way — it promptsafx cleanup, which is destructive.afx send --worktree <path>resolves the recipient and the resolution workspace from that path, scoped to the workspace that owns it. A worktree no builder owns throws, naming the worktree and listing who is registered.fromWorkspacestill comes from the sender's session; those were one value and are now two. When the caller names its own target (architect), the path pins only the scope and no DB read happens, so an orphaned worktree cannot suppress the cleanup trigger.afx send --exactturns off the builder tail match in both Tower resolvers — live and the offline-hold registry. The miss names the address, the workspace, and the builders actually there.Note the safety property does not depend on Tower being upgraded first: porch now sends a canonical builder id, which cannot tail-match anything, so an older Tower that ignores
exactstill delivers correctly.176 lines of production code across 8 files.
Test Plan
bugfix-264-gate-notification-addressing.test.ts(resolver + worktree resolution, two-workspace fixture) andbugfix-264-gate-notify-addressing.test.ts(the argv and the message text)afx send --worktree <my worktree> --exactdelivered tobuilder-bugfix-264Not in scope
Issue #185's "On it" false positive in the spawn render gate, per the architect — separate issue.
🤖 Generated with Claude Code