Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-4144Done

(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-evidenceHTTP 405. Same for MOTIR-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.

  1. The route. GET /api/work-items/[id]/acceptance-evidence returning the story's CURRENT receipt status. It only has to answer the one question the guard asks — is this story's receipt approved? — so return the evidence's status and nothing the caller has no business reading. Thin route per CLAUDE.md §4-layer: gate → one service call.
  2. 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 door authorizeAcceptancePublish uses. A READ should not need ACCEPTANCE_PUBLISH_PERMISSION; decide the permission it does ask for and say why. Do NOT reuse authorizeAcceptancePublish wholesale: 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.
  3. The guard's call. fetchApprovedStories sends only authorization: Bearer <token>. If the credential this lands on is OIDC, it must also send x-motir-auth: github-oidc, which it does not today — so the guard's fetch changes with the route or the two will not meet.
  4. 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

  1. GET /api/work-items/<key>/acceptance-evidence exists and answers a CI caller with the story's current receipt status. Proven by an integration test against real Postgres in the shape of tests/acceptance-evidence-publish-route.test.ts: an approved receipt reads approved, 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).
  2. 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.ts already drives this shape against the POST.
  3. fetchApprovedStories reaches 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 a fetchImpl stub, so no test has ever asked whether the URL it builds resolves.
  4. 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.tsauthenticateCiPublisher, the OIDC-then-PAT door.
  • lib/acceptanceEvidence/publishAuth.tsauthorizeAcceptancePublish, and the eligibility gate a read should NOT inherit.
  • tests/e2e-acceptance-lane-membership.test.tsfetchApprovedStories, and the fail-open comment.
  • scripts/upload-acceptance-video.mjs:254-276requestGithubOidcToken and the x-motir-auth: github-oidc marker.
  • docs/decisions/acceptance-receipt-lifecycle.md §3 — the lifecycle the guard enforces.