MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3266

Planning bug: an acceptance criterion named TWO triggers for one event-driven seam, and one of them is a non-terminating loop — the card's own explanation names only the other

Done
Description

Found by motir run MOTIR-2852, 2026-08-20, before the first line of code. Repo of the fix: motir-meta — the correction is to the authoring rule, not to product code. MOTIR-2852 itself shipped, with the criterion amended on the record.

The defect

MOTIR-2852's acceptance criterion 1 read:

When the admission gate returns gate_deferred, and when an in-flight runner reaches a terminal state and frees a slot for its project, the next runnable pending intent for that project is dispatched directly…

Two triggers, conjoined, for one new seam. The second is correct and shipped. The first does not terminate, and one code-path trace at origin/main b820c979 shows it:

  1. a deferral takes no claimbootOnce returns on verdict.outcome === 'deferred' before reaching claimPending — so the deferring intent is still pending;
  2. "the next runnable pending intent for that project" resolves through a queuedAt asc read, so it is that same intent — always when it is the only queued one, and ordinarily otherwise;
  3. the dispatched boot re-enters a gate whose caps have not moved, defers again, dispatches again — with no wait between turns. Excluding the trigger does not rescue it: two queued intents then trade the dispatch, which is the same loop one step longer.

Shipped literally, the criterion would have replaced one SELECT per minute with an unbounded event storm against the same database — inverting the card's entire purpose.

Why it got past authoring, and it is not carelessness

The card's explanation is right, and names only one trigger: "the worst-case wait between a CI slot freeing and a deferred job being admitted." The criterion then generalised one step further than the reasoning did, by adding the state where the need is FELT (a deferral) alongside the event that RESOLVES it (a slot freeing). Those two read as a natural pair — the deferral is what proves something is queued — and the AC's own conjunction ("When A, and when B, …") is grammatically ambiguous between two triggers and one trigger under a precondition. AC 5 disambiguated it the wrong way ("a deferral dispatches, a completion dispatches"), which is what made the loop the card's literal instruction rather than a misreading of it.

The general shape: a criterion for an event-driven seam that names a STATE as a trigger. A state is not an event and does not clear on its own, so a dispatch fired on one re-fires on the next observation of the same state. Only a TRANSITION out of the state is safe to hang a dispatch off.

Acceptance criteria

  1. motir-meta/prompts/plan-rules/type-code.md carries a check for a card that introduces an event-driven trigger: every named trigger is a state TRANSITION, and the transition's own effect must remove the condition that fired it. A trigger that leaves the world in the state it fired on is a loop, and the card names which transition clears it.
  2. The check states the tell in a form an author can apply without tracing code: a trigger phrased as "when X is deferred / blocked / still waiting / at capacity" is a STATE; "when X completes / is released / frees a slot" is a transition. Where a card wants both, the state is the PRECONDITION and only the transition dispatches.
  3. motir-meta/notes.html carries the lesson entry, and the rule cites it by number. The motir run MOTIR-2852 session wrote that entry in its own unnumbered docs/* lesson PR (a notes.html lesson logged by a run working a motir-core card carries no MOTIR-<id>, so there is no card to depend on). If that PR has already merged, this criterion is a read; if it has not, this card writes the entry — either way the work is in this repo and in this diff.
  4. Ordering: every criterion reads on this card's own diff, pre-merge. Nothing is blocked.

Dependency disposition — why there is no blocked_by on MOTIR-2852

validate_work_item returns a likely-missing-edge advisory naming MOTIR-2852 (implemented, so not done). This is a deliberate exclusion, recorded here because silence would be indistinguishable from an oversight.

This card CONSUMES nothing from that one. Its whole deliverable is a rule in motir-meta/prompts/plan-rules/type-code.md plus a notes.html entry — no motir-core substrate is read, and none of MOTIR-2852's unmerged branch is referenced by any criterion. Where it appears above it is provenance: the incident the rule generalises. And the one fact this card leans on — that MOTIR-2852's AC 1 and AC 5 carry an amendment on the record — is true of the live card now, written by update_work_item during that run; it does not wait for PR #2221 to merge. The relates_to edge carries the discovery trace, which is the edge that is actually owed.

Context refs

  • MOTIR-2852 — the card, its amended AC 1 and AC 5, and the Resolution section.
  • PR moooon-B-V/motir-core#2221 — the falsification with the trace, in the body.
  • tests/ciFleet/ciRunnerAdmissionWake.test.ts @ motir-corethe DEFERRAL limb of the card — why it is NOT implemented, which demonstrates turn one of the loop against the real queue; and the ⚠️ NO WAKE ON A DEFERRAL note at lib/services/ciRunnerBootService.ts's deferral branch.
  • MOTIR-3224 — the motir-ai twin, whose wake hangs off a COMMITTED ROW (a transition) and therefore had no equivalent limb to get wrong.