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 · Roadmap

MOTIR-4093Done

(motir-core) The acceptance-lane lifecycle guard has NEVER checked a receipt in CI — no job sets its credential, so MOTIR-2770 always passes and the lane has grown back to 23 specs

Opened by Zhu Yue ·

Found 2026-09-01 while asking why main runs the acceptance lane on every merge. It does, because the guard that was supposed to empty the lane has been a no-op since it shipped.

⚠️ AMENDED (1) 2026-09-01 by a halted motir run (run.md guard #4) — read this before AC 1

The finding below is correct and reconfirmed on origin/main at e5346ff9c. Two things the card assumed underneath it are false, and both were verified against shipped reality rather than reasoned about:

  1. There is no read path to spend a credential on. fetchApprovedStories issues GET {base}/api/work-items/{key}/acceptance-evidence. That path exports only POST; no GET handler exists anywhere in the tree, git log --all -S'export async function GET' on it returns nothing (it has never existed), and the DEPLOYED product answers HTTP 405 to a real bearer. Because the function fails OPEN by design, 405 is indistinguishable from "no receipt is approved" — so the approved set is empty on every call whatever credential is supplied. This card is now blocked_by the sibling that builds that route.DISCHARGED: MOTIR-4144 merged as #2515 and the route now exists.
  2. MOTIR_UPLOAD_TOKEN is not a configured secret. gh secret list on moooon-B-V/motir-core returns ten secrets and it is not among them. The publish step's working credential is keyless GitHub OIDC (MOTIR-1650); the secret is an unconfigured PAT fallback. So AC 1's "the same secret/vars the publish step used" names an empty string, and shipping AC 1 + AC 2 together as written would turn main red on every merge while still measuring nothing.

AC 1 is therefore re-pointed (below) at whatever credential the blocking card settles on. AC 4 is discharged: MOTIR-4094 merged as #2500 at 2026-09-01T20:53:57Z and drained the lane, so the count is now 0, not 23 — git ls-tree -r --name-only origin/main tests/e2e/ | grep -E '^tests/e2e/acceptance[^/]*\.spec\.ts$' | wc -l. Sizing moved 2→3 points / 40→60 minutes for AC 3's workflow predicate, which is a new guard file rather than an env: edit.

⚠️ AMENDED (2) 2026-09-02 by a second halted motir run (run.md guard #4) — THE REPLACEMENT CREDENTIAL IS NOT CONFIGURED EITHER

Amendment (1) retired MOTIR_UPLOAD_TOKEN for not being a configured secret, and the 2026-09-02 11:58 handover comment re-pointed AC 1 at MOTIR_GUARD_TOKEN. Checked against the PROVIDER this time, on origin/main at cd236496d:

  • gh secret list --repo moooon-B-V/motir-core12 repository secrets (BETTER_AUTH_SECRET, FLY_API_TOKEN, NEXT_PUBLIC_SENTRY_DSN, NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, NPM_TOKEN, RELEASE_APP_ID, RELEASE_APP_PRIVATE_KEY, SEED_DATABASE_URL, SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT, VERCEL_TOKEN). MOTIR_GUARD_TOKEN is not among them.
  • gh api repos/moooon-B-V/motir-core/actions/organization-secretstotal_count: 0. Both environments (Preview, Production) → total_count: 0 secrets each. There is nowhere else it could be.
  • gh api repos/moooon-B-V/motir-core/actions/variables → one variable, FLY_EXPECTED_MACHINE_COUNT. MOTIR_BASE_URL is not a variable, and after MOTIR-4096 retired the publisher the string MOTIR_BASE_URL appears nowhere in .github/** — so the handover comment's "already vars.MOTIR_BASE_URL || 'https://app.motir.co' in the acceptance lane" is stale. Harmless: the literal fallback can simply be written fresh. The TOKEN half is not harmless.

So this card is now blocked_by a manual / human sibling that mints and stores the credential. Wiring an empty token and shipping AC 2's fail-closed assertion in the same pull request makes ci.yml's test job — twelve legs, on every pull request and every merge-queue entry — fail on a value no pull request can supply: a repository-wide merge outage, and exactly the outcome amendment (1) predicted for the previous credential.

The keyless arm was considered and rejected. MOTIR_GUARD_AUTH=github-oidc needs no secret and MOTIR-4144 tested it — but authenticateGithubOidc (lib/github/oidcAuth.ts) returns the workspace OWNER's identity and authenticateCiPublisher then skips the permission check entirely, so id-token: write on the twelve-leg Vitest job would let any file in the suite, on any non-fork pull request, authenticate to production Motir as the workspace owner. A PAT scoped to project:browse is both smaller and the arm the handover comment names.

One more state to re-check before building, not a blocker: at 2026-09-02T14:33Z the DEPLOYED origin still answered 405 to GET https://app.motir.co/api/work-items/MOTIR-1627/acceptance-evidence with a real bearer — MOTIR-4144 merged at 14:21:53Z and the deploy of cd236496d was still in flight. Since MOTIR-4144 made a route-level status THROW (LaneGuardReadError) instead of folding into "not approved", a credentialled guard pointed at an origin without the route is now RED rather than silently green. Re-run that curl and read 200 before wiring anything.

The mechanism

tests/e2e-acceptance-lane-membership.test.ts is MOTIR-2770's guard: no spec in the lane may have an approved receipt, and every member must declare its story. Its functions now live in tests/helpers/acceptanceLaneGuard.ts (MOTIR-4144 — one implementation, two callers; the route test needs a database and a spec cannot import a spec). The receipt half needs to ask the product:

export function resolveStatusSource(env: Record<string, string | undefined>): StatusSource | null {
  const baseUrl = env['MOTIR_GUARD_BASE_URL'] ?? env['MOTIR_BASE_URL'] ?? '';
  const token = env['MOTIR_GUARD_TOKEN'] ?? env['MOTIR_UPLOAD_TOKEN'] ?? '';
  const authMode = env['MOTIR_GUARD_AUTH'] === 'github-oidc' ? 'github-oidc' : 'bearer';
  return baseUrl && token ? { baseUrl: baseUrl.replace(/\/$/, ''), token, authMode } : null;
}

With no source it checks only the undeclared-story half and prints its own degradation notice, which ends:

(The approved-receipt half of this guard was SKIPPED: no MOTIR_GUARD_TOKEN/MOTIR_UPLOAD_TOKEN + MOTIR_BASE_URL in this environment, so the product could not be asked which receipts are approved. It runs in CI.)

It does not run in CI. On origin/main at cd236496d those names appear in no workflow file at allMOTIR-4096 retired the publish step that was their last home. The Vitest lane that executes this test (ci.yml's test job, 12 legs, vitest.collect.config.ts) sets none of them, so the guard has taken the degraded branch on every run it has ever had, and reported green.

And note the second degradation nobody has to reach a credential to hit: with the lane at 0 members the spec returns at members.length === 0 before resolveStatusSource is ever called, so an empty lane passes with no source and no notice. Whatever AC 2 asserts has to bind independently of lane membership, or the hatch is shut only on the days it would not have mattered.

What it cost

find tests/e2e -maxdepth 1 -name 'acceptance*.spec.ts' | wc -l on origin/main = 23 at filing time — now 0, drained by MOTIR-4094 (see amendment (1)). The workflow header states the opposite as settled fact, and every cost argument in the file is priced on it:

  • "after the MOTIR-2769 triage the lane's steady-state membership is ZERO"
  • MOTIR-2760's gate: "an empty lane costs one ~10s checkout per merge"
  • MOTIR-2908's derived fan-out: min(specs, 4) — at 23 members that is permanently the capped 4.

So every merge paid build + 4 shards instead of a 10-second gate. Measured on run 33502392371 (merge of #2495): gate 0.35 + build 3.8 + shards 13.2 / 13.3 / 10.9 / 8.1 = ~50 machine-minutes, ~17 min wall, on ~20 executed baselines a day. The drain has since removed the immediate cost; the HOLE this card closes is what stops it coming back.

Acceptance criteria

  1. The job that runs tests/e2e-acceptance-lane-membership.test.tsci.yml's test job — has BOTH an origin and a token in its env:: MOTIR_GUARD_TOKEN: ${{ secrets.MOTIR_GUARD_TOKEN }} and MOTIR_BASE_URL: https://app.motir.co (a literal — there is no vars.MOTIR_BASE_URL and no other workflow left to copy an expression from). ⚠️ AMENDED TWICE. The original wording named secrets.MOTIR_UPLOAD_TOKEN; amendment (1) retired it as unconfigured; MOTIR_GUARD_TOKEN is unconfigured too, so this criterion is discharged only once its provisioning sibling is done — that card is this one's blocked_by, and its AC 3 records the 200 that proves the value behind the name. Prefer the guard-specific MOTIR_GUARD_* names over MOTIR_BASE_URL where a choice exists, so a job-level env cannot change what any other test in the lane sees.
  2. The guard refuses to pass silently where it is meant to bind. A test asserts that in an environment that is SUPPOSED to bind and has no credential resolvable, the run FAILS instead of printing the degradation notice — and that assertion does not depend on the lane having members, since the spec returns early at members.length === 0 and the lane's steady state is 0. The laptop / fork-PR degradation stays exactly as it is — that is the stated design, and it is only the CI claim that was false. Note the discriminator cannot be CI alone: a fork PR also sets CI and gets no secrets, so the workflow has to say which environment is SUPPOSED to bind (e.g. required unless github.event.pull_request.head.repo.fork).
  3. Self-recounting predicate, not a count: no job in .github/workflows/** runs this test file without a resolvable source. A new job that reintroduces the hole fails the assertion. Model it on tests/ci-job-timeouts.test.ts, which derives its subjects from the directory rather than listing them; keep every helper INLINE in the spec, because a new non-spec module under tests/ that calls readdirSync becomes a "scanner module" and drags its importers into tests/ci-structural-guards-lane.test.ts's candidate set.
  4. The PR body carries the guard's real output against the lane as it stands — each member spec with its story key and whether that receipt is approved. That list is the input MOTIR-4094 works from. DISCHARGED: MOTIR-4094 is done and merged (#2500), and the lane it drained now holds 0 specs, so there is no disposition list left to produce and no consumer waiting for one.

Sequencing — read this before starting

Landing this fix RED-LIGHTS the lane immediately for every discharged spec, and main stays red until the triage lands. So this card is blocked_by MOTIR-4094: drain first, then close the hole behind it. MOTIR-4094 has landed, so that hazard is gone. The card is now blocked on the missing read path insteadMOTIR-4144 has landed too (#2515).

What remains is the CREDENTIAL, and it is the last blocker. The order is forced: mint and store MOTIR_GUARD_TOKEN, prove it reads 200 against the deployed origin, and only then wire the env: and the fail-closed assertion. Reversing it turns the twelve-leg Vitest lane red on every pull request, on a value no pull request can supply.

Context refs

  • tests/helpers/acceptanceLaneGuard.tsresolveStatusSource, fetchApprovedStories, LaneGuardReadError, judgeLane (moved here by MOTIR-4144).
  • tests/e2e-acceptance-lane-membership.test.ts — the check itself and its two describe blocks; still where the guard's own tests live.
  • .github/workflows/ci.yml — the test job's env: block, where the credential lands. Note its if:needs.changes.outputs.app == 'true' && github.event_name != 'push' — so it runs on pull requests and the merge queue, never on push-to-main.
  • tests/ci-job-timeouts.test.ts — the self-recounting whole-directory workflow guard to model AC 3 on.
  • tests/helpers/structuralGuardLane.tsBOUNDED_SCAN_MODULES already declares acceptanceLaneGuard.ts; read it before adding a helper module that walks a directory.
  • docs/decisions/acceptance-receipt-lifecycle.md §3 — the lifecycle this guard enforces.
  • MOTIR-4096 retires the publish step and with it today's only MOTIR_UPLOAD_TOKEN consumer: whichever of the two lands second must leave the guard's credential wired. MOTIR-4096 landed first, and took every one of these variable names out of .github/**. There is nothing left to preserve — this card writes them fresh.

Discussion

No comments yet.

Adding to this discussion signs you in on app.motir.co and brings you back to this request.

Add a comment