Skip to content

Fix #264: address the gate notification by worktree, not by project id - #270

Merged
pseudoseed merged 9 commits into
mainfrom
builder/bugfix-264
Aug 31, 2026
Merged

Fix #264: address the gate notification by worktree, not by project id#270
pseudoseed merged 9 commits into
mainfrom
builder/bugfix-264

Conversation

@pseudoseed

Copy link
Copy Markdown
Owner

Summary

A gate-approval notification could be delivered to a project that approved nothing. porch approve addressed the recipient by bare project id, and afx send resolved the workspace from the sending process's session and then matched the agent by tail — so 250 reached builder-spir-250 in 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-this apparatus.

Fixes #264

Root Cause

Two hops composed, neither carrying the identity of the project whose status.yaml was actually written.

Hop 1 — the recipient was a bare project id. commands/porch/index.ts:1267 sent target: state.id, which notify.ts turned into afx send <projectId> "<msg>" --raw.

Hop 2 — the workspace came from the sender, and the agent was tail-matched. agent-farm/commands/send.ts resolves the workspace from CODEV_THREAD_ID, else CODEV_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 in spir-250's pane made a temp-workspace approval resolve to /Users/chris/dev/codev-1455. servers/tower-messages.ts then matched builderId.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 pr gate and ran porch approve from it. Before the fix the message landed in builder-bugfix-264's pane while that project's own pr gate stayed pending. With env -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

  • porch addresses the project's own worktree. notifyGateApproved sends afx send --worktree <artifactRoot> --exact. artifactRoot is the directory whose status.yaml was just 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'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 --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 is checkable rather than an instruction. It names the project and workspace, says outright that it is not an approval and carries no authority, and says what to confirm against and what a mismatch means.

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 exact still delivers correctly.

176 lines of production code across 8 files.

Test Plan

  • Regression test added — bugfix-264-gate-notification-addressing.test.ts (resolver + worktree resolution, two-workspace fixture) and bugfix-264-gate-notify-addressing.test.ts (the argv and the message text)
  • Both confirmed to fail with the corresponding half of the fix backed out (3 failures each), then restored
  • Build passes
  • All tests pass — 7413 passed, 58 skipped, 0 failed
  • Original reproducer re-run against the fix: nothing delivered, named miss logged
  • Live positive: afx send --worktree <my worktree> --exact delivered to builder-bugfix-264

Not in scope

Issue #185's "On it" false positive in the spawn render gate, per the architect — separate issue.

🤖 Generated with Claude Code

pseudoseed and others added 9 commits August 31, 2026 07:58
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
pseudoseed merged commit 55efb75 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
main carries #274's own merge plus bugfix-264 (#270); without this the state-commit
PR would have read as a revert of all eleven.
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.

SAFETY: a gate-approval notification crosses workspaces and can tell a builder its gate was approved when it was not

1 participant