(motir-core) The acceptance-receipt STATUS has no read path — `GET /api/work-items/<key>/acceptance-evidence` is 405, so MOTIR-2770's approved-receipt guard fails open and can never fire, credential or not
Found 2026-09-01 by a halted motir run MOTIR-4093, under run.md guard #4. MOTIR-4093 set out to give the lane-membership guard the credential it never had in CI; the credential turns out to be the second of two missing things, and this is the first.
The mechanism
tests/e2e-acceptance-lane-membership.test.ts asks the product which stories have an approved receipt:
const res = await fetchImpl(`${source.baseUrl}/api/work-items/${key}/acceptance-evidence`, {
headers: { authorization: `Bearer ${source.token}` },
});
if (!res.ok) continue;
That GET has no handler. app/api/work-items/[id]/acceptance-evidence/route.ts exports POST and nothing else; its sibling upload-token/route.ts likewise. Measured on origin/main at e5346ff9c:
git grep -n 'acceptance-evidence' origin/main— the only two route files are the two POSTs above.git log --all --oneline -S'export async function GET' -- 'app/api/work-items/[id]/acceptance-evidence/route.ts'returns nothing: a GET has never existed on that path, so this is not a regression to bisect.- Against the DEPLOYED product, with a real workspace bearer:
GET https://app.motir.co/api/work-items/MOTIR-1627/acceptance-evidence→ HTTP 405. Same forMOTIR-2765.
Why that is worse than a 404 would be
fetchApprovedStories fails OPEN, deliberately and correctly — its own comment says "A story the read cannot resolve is treated as NOT approved — the guard must never fail a spec on the strength of a 404 or a flaky hop." That is the right policy for a flaky hop. Applied to a route that does not exist it means the approved set is empty on every call, for ever, and the guard reports green for a structural reason no amount of credential fixes.
So MOTIR-2770's approved-receipt half has two independent reasons to have never fired: no credential in CI (MOTIR-4093's finding) and no endpoint to spend one on (this card's). Closing only the first produces a guard that looks wired, is authenticated, and still measures nothing — which is the exact defect MOTIR-4093 exists to end.
What to build
A read path for the receipt STATUS, authenticated for the same CI caller the publishers already authenticate, and then the guard pointed at it.
- The route.
GET /api/work-items/[id]/acceptance-evidencereturning the story's CURRENT receipt status. It only has to answer the one question the guard asks — is this story's receiptapproved? — so return the evidence's status and nothing the caller has no business reading. Thin route perCLAUDE.md§4-layer: gate → one service call. - The auth. Reuse
authenticateCiPublisher(lib/publishAuth/ciPublishAuth.ts), which already accepts keyless GitHub OIDC first and a bearer PAT holding the required permission second — the same doorauthorizeAcceptancePublishuses. A READ should not needACCEPTANCE_PUBLISH_PERMISSION; decide the permission it does ask for and say why. Do NOT reuseauthorizeAcceptancePublishwholesale: its eligibility gate (org toggle / plan) is about whether a publish is allowed, which is not a question a status read should be able to fail on. - The guard's call.
fetchApprovedStoriessends onlyauthorization: Bearer <token>. If the credential this lands on is OIDC, it must also sendx-motir-auth: github-oidc, which it does not today — so the guard's fetch changes with the route or the two will not meet. - Keep the fail-open policy, and make it distinguishable. A flaky hop must still not fail a spec. But a 404/405 from the route itself is a defect in the guard's own wiring rather than a fact about a story, so those must be LOUD — this is the same rule MOTIR-4093 applies one layer up. A guard whose read path returns a method error should fail the run, not quietly conclude that no receipt is approved.
Acceptance criteria
GET /api/work-items/<key>/acceptance-evidenceexists and answers a CI caller with the story's current receipt status. Proven by an integration test against real Postgres in the shape oftests/acceptance-evidence-publish-route.test.ts: an approved receipt readsapproved, a pending one does not, and a story in another workspace reads 404 rather than 403 (finding #44's rule, which the POST path already follows).- Both auth arms are exercised — the keyless OIDC arm and the bearer-PAT arm — and an unauthenticated call is refused.
tests/github/oidc-auth.test.tsalready drives this shape against the POST. fetchApprovedStoriesreaches the shipped route: a test drives the guard's own function against the real handler (not a hand-written fake) and gets a non-empty approved set for an approved story. This is the criterion that would have caught the defect — every existing test of that function passes it afetchImplstub, so no test has ever asked whether the URL it builds resolves.- A route-level error the guard cannot interpret — 404, 405, 401 — is reported rather than silently folded into "not approved". A transport error (
ECONNRESET, a timeout) keeps today's tolerant behaviour, and a test pins the two apart.
Sequencing
MOTIR-4093 is blocked_by this card and keeps its own scope: the lane's env:, the fail-closed assertion, and the self-recounting predicate over .github/workflows/**. Land this first, then MOTIR-4093 can wire a credential that buys something. Whichever credential this card settles on is the one MOTIR-4093's AC 1 must name — its current wording ("the same secret/vars the publish step used") points at secrets.MOTIR_UPLOAD_TOKEN, which is not a configured secret in this repository.
Context refs
app/api/work-items/[id]/acceptance-evidence/route.ts— the POST-only route.lib/publishAuth/ciPublishAuth.ts—authenticateCiPublisher, the OIDC-then-PAT door.lib/acceptanceEvidence/publishAuth.ts—authorizeAcceptancePublish, and the eligibility gate a read should NOT inherit.tests/e2e-acceptance-lane-membership.test.ts—fetchApprovedStories, and the fail-open comment.scripts/upload-acceptance-video.mjs:254-276—requestGithubOidcTokenand thex-motir-auth: github-oidcmarker.docs/decisions/acceptance-receipt-lifecycle.md§3 — the lifecycle the guard enforces.