Planning bug: an acceptance criterion keyed on a COUNT that the code's own idempotency argument forbids — the falsifying comment sat one file from the card's own context refs
The record. MOTIR-3051's AC 2 read "the auto-plan cadence is not paused by a plan carrying zero items … a test covers a project whose only undecided plan is empty and asserts cadence still fires." Built as written, that ships a regression.
lib/jobs/definitions/autoPlanCadenceTick.ts sets retryPolicy: 'idempotent' and states the whole argument for it in its header: "a project that already fired now HAS an undecided plan, so the gate skips it on the re-run. Re-running the whole sweep therefore converges rather than double-firing." Between submitExpand's createPlan and motir-ai's first addProposals that plan holds zero items — so a gate keyed on the COUNT lets an Inngest retry of the tick fire a second expand_item at the same stub, replacing a permanent pause with the stacked proposal the gate exists to prevent. And nothing in the suite would have said so: every plan tests/integration/ai/autoPlanCadence.test.ts seeds carries a sourceJobId, and no test exercises the retry path.
Caught at motir run before a line was written; the criterion was AMENDED on the card with its evidence (sourceJobId IS NULL — the PRODUCER, not the count) rather than narrowed silently, and both tests now exist.
The shape, generalized. The card's Context refs named autoPlanCadenceService.ts — the SWEEP. The falsifying sentence was in autoPlanCadenceTick.ts — the JOB DEFINITION that schedules it, one file away, never opened at plan time. A predicate's correctness is not a property of the predicate: it is a property of everything that RE-EVALUATES it, and a retry policy is a caller that re-evaluates it on purpose. The three-option menu was otherwise well-verified — the mechanism was correct, CLI_TOKEN_GRANT was quoted correctly, the gate was read correctly — which is exactly why the one unchecked clause read as safe.
Nearest existing lesson: notes.html #306 (a bug card stated its MECHANISM as settled and enumerated the permitted repairs — the enumeration bounded the next run's search). This is the softer form: the mechanism WAS right and only the repair's PREDICATE was under-specified. #288 is the other half (every existing test synchronised between steps, so the one bug that appears only when they are not was invisible to the whole suite) — here, every seeded plan had a job.
Acceptance criteria
- A
notes.htmlmistake entry exists carrying this lesson, with the counter bumped and the.mistakemarkup matched: a criterion that gates on a COUNT / an emptiness owes a read of every caller that RE-EVALUATES that gate — the retry policy, the cron, the poller — not only the service that owns it. - The entry names the concrete tell: the falsifying comment lived in the JOB DEFINITION beside the service the card's context refs did name, so "I read the file the card pointed at" was true and insufficient.
- Whether this rises to a RULE (
plan-rules/phase-deepen.md's precondition-verification limb, which already owns verify the claim against shipped reality) is judged, and the judgement is stated — a single occurrence is a LESSON by default perplan-rules/CORPUS-MAINTENANCE.md.
Not blocked by MOTIR-3051 — stated rather than left silent
This card's deliverable is a notes.html entry in motir-meta. It reads on the AMENDMENT already applied to MOTIR-3051 in the live tenant, which exists now, and on comments already on origin/main (autoPlanCadenceTick.ts). It consumes nothing from MOTIR-3051's unmerged branch, so no blocked_by is owed — relates_to carries the provenance.