Summary
A /builder comment on GitHub is not bound to a specific plan. githubPlanAcceptance (services/api/src/github/router.ts:641-674) picks the newest plan_acceptance proposal on the issue whose Architect run succeeded, and the approval is written against whatever came back:
.where(and(... eq(actionTypes.name, "plan_acceptance"), eq(runs.status, "succeeded"), ...))
.orderBy(desc(proposals.createdAt))
.limit(1)
COMMAND_RE (router.ts:60-61) carries nothing that could disambiguate, and nothing supersedes or expires an older open proposal when a newer Architect run succeeds on the same issue. The API lane is proposal-id-bound (POST /v1/proposals/:id/decide), so this is GitHub-only.
Scenario
A reviewer reads plan P1 (proposal A, published with the facility:architect-plan:<run>:<proposalA> marker). A teammate re-runs /architect; run 2 succeeds and opens proposal B seconds before the reviewer's /builder webhook is processed — or the reviewer simply never noticed the second publication comment. Facility approves B, dispatches the Builder on P2, and the audit trail records that the reviewer approved P2 with a valid plan hash and fresh evidence. Every downstream check in builder-plan-policy.ts passes, because they verify that the approved proposal is consistent, not that it is the one the human read.
Why it matters
Gate 1 is the product invariant ("a person accepts the plan"). Here the person accepted a plan; which one is decided by ordering.
Suggested fix
Either require the proposal id (or a plan-hash prefix) in the command — /builder <proposalA> — with the bare form accepted only when exactly one live proposal exists on the issue; or supersede older open plan_acceptance proposals when a new Architect run succeeds on the same issue, and refuse /builder while more than one is live. Both keep the current UX for the common case.
Adjacent but different from #222/#223 (comments invalidating an approved plan) and #147 (base commit not recorded).
Summary
A
/buildercomment on GitHub is not bound to a specific plan.githubPlanAcceptance(services/api/src/github/router.ts:641-674) picks the newestplan_acceptanceproposal on the issue whose Architect run succeeded, and the approval is written against whatever came back:COMMAND_RE(router.ts:60-61) carries nothing that could disambiguate, and nothing supersedes or expires an older open proposal when a newer Architect run succeeds on the same issue. The API lane is proposal-id-bound (POST /v1/proposals/:id/decide), so this is GitHub-only.Scenario
A reviewer reads plan P1 (proposal A, published with the
facility:architect-plan:<run>:<proposalA>marker). A teammate re-runs/architect; run 2 succeeds and opens proposal B seconds before the reviewer's/builderwebhook is processed — or the reviewer simply never noticed the second publication comment. Facility approves B, dispatches the Builder on P2, and the audit trail records that the reviewer approved P2 with a valid plan hash and fresh evidence. Every downstream check inbuilder-plan-policy.tspasses, because they verify that the approved proposal is consistent, not that it is the one the human read.Why it matters
Gate 1 is the product invariant ("a person accepts the plan"). Here the person accepted a plan; which one is decided by ordering.
Suggested fix
Either require the proposal id (or a plan-hash prefix) in the command —
/builder <proposalA>— with the bare form accepted only when exactly one live proposal exists on the issue; or supersede older openplan_acceptanceproposals when a new Architect run succeeds on the same issue, and refuse/builderwhile more than one is live. Both keep the current UX for the common case.Adjacent but different from #222/#223 (comments invalidating an approved plan) and #147 (base commit not recorded).