childStatusCascadeService exempts `bug` children — delete the carve-out; MOTIR-3229's incident was a parent declared done over unfinished children, not a kind problem
Repo · motir-core. childStatusCascadeService exempts every bug child from the downward cascade. It should not. A bug that is a CHILD of a parent run is fixed BY that run — its commit is in the very pull request whose merge woke the cascade — so leaving it open contradicts the merge that closed it.
⚠️ Two things this card does NOT propose (Yue, 2026-08-21)
- The event-driven cascade is RIGHT and stays. An earlier draft proposed settling the whole subtree in one transaction; that is withdrawn. A merge is one fact, the subtree it settles can be large, and a background reconciliation is the honest shape. The write-per-child and the delay are not the defect.
- The CHILD'S KIND IS NOT THE DISCRIMINATOR, and no replacement discriminator belongs in the cascade. The containment belongs upstream — see the root cause below.
isCascadeExemptshould be deleted, not made cleverer.
What the exemption costs, measured on a live merge (2026-08-21)
MOTIR-3232 — thirteen children — merged as #2237. From get_work_item_activity:
10:00:40 PR #2237 merged
10:00:42 MOTIR-3232 In Review → Done
10:02:34…36 eleven SUBTASK children Implemented → Done
MOTIR-3236 (bug) — skipped by the exemption
MOTIR-3240 (bug) — skipped by the exemption
10:03:30 MOTIR-3232 Done → IMPLEMENTED ← the rollup counts what the cascade may not complete
Both skipped cards were planned children, claimed and built by the run: MOTIR-3236's fix is commit 6d1c3340, MOTIR-3240's is c3e2b5c4, both inside #2237. They were left open because of their kind, against the evidence of their own merged code.
And the story did not merely lag — it was pulled back out of done, because parentStatusRollupService states in its header "No work item is exempt: there is no per-item or per-kind carve-out." The upward half counts children the downward half is forbidden to complete, so the end state is stable and self-perpetuating. It sat at implemented until both bugs were closed by hand, after which the rollup pulled it up on its own. Deleting the exemption fixes both halves at once: there is then nothing left for the two services to disagree about.
⚠️ MOTIR-3229's incident was diagnosed at the WRONG LAYER — and the kinds were never the issue
The exemption exists because of a real loss: MOTIR-1343's run filed two defect reports under the story while shipping it, and both were closed out from under a session three minutes into investigating them. MOTIR-3229 concluded that a bug must be exempt. That is a fix to the symptom. The measured history says so:
11:24:59 MOTIR-1343 In Progress → Implemented
11:31:18 MOTIR-1343 Implemented → In Review ← the parent run opened its pull request
13:27:15 MOTIR-1343 In Review → Done ← merged
13:41:45 MOTIR-3218 To Do → In Progress ← FILED AND CLAIMED 14 MINUTES LATER,
13:41:56 MOTIR-3219 To Do → In Progress under a parent that was ALREADY done
13:44:35 MOTIR-3218 In Progress → Done ⎫ swept, 36ms apart
13:44:35 MOTIR-3219 In Progress → Done ⎭
14:06:57 MOTIR-3219 Done → In Progress ⎫ the recovery, by hand
14:07:10 MOTIR-1343 Done → In Progress ⎭
14:57:19 MOTIR-1343 In Review → Done ← redone properly
The children were in_progress when they were swept, and kind is not what made them vulnerable. filterNotDone keeps every child not in a done-category status and the cascade completes them all — so an in_progress subtask in the same position would have been swept identically, with no exemption to save it. The exemption protects one kind-shaped slice of a kind-agnostic hole.
The hole is that a parent's done-ness was declared over children that were not finished. Two forms of it, and this card asserts only what the history above shows:
- A parent run must not open its pull request and move the parent to
in_reviewwhile any child is unimplemented — regardless of kind. (Yue's statement of the rule. MOTIR-1343 satisfied it at 11:31, when neither child existed yet, so this timeline is not its instance — but the rule is the containment for the general case and belongs with this card's decision.) - A child must not be filed under a parent that is already finished — which IS what this timeline shows, and is where MOTIR-1343 actually lost its findings.
⚠️ What is NOT established: why the cascade fired at 13:44:35 when the parent's own in_review → done was at 13:27:15 and no parent transition is recorded at 13:44. Whoever takes this should pull that Inngest event rather than infer it — three mechanisms have already been asserted from partial evidence in this lineage and corrected.
Acceptance criteria
isCascadeExemptand its kind carve-out are DELETED. A bug child that the run built is completed by the parent's merge; a test covers the MOTIR-3232 shape and fails against today's code.- The parent settles at
doneand STAYS there. The regression test asserts the state after the rollup has run, not just after the cascade — the incident above passed the cascade and failed 54 seconds later. - The containment is placed upstream, not in the cascade: a parent may not be moved to
in_review(nor its pull request opened by a parent run) while a child is unimplemented, and the guard is kind-agnostic. If that guard already exists forimplemented(lib/workItems/statusLadder.tsis shared with a gate that refuses a container's claim to be built while a child is not), this card says why it did not coverin_review. - A child created under an already-done parent does not inherit that parent's doneness — the MOTIR-1343 shape, tested from its own evidence.
childStatusCascadeService's header andisCascadeExempt's doc block are rewritten rather than deleted wholesale: they carry MOTIR-3229's measurement, which stays true, and the new text should say why the conclusion drawn from it moved layers.- The rollup's "No work item is exempt" claim is re-read against the result — with the exemption gone it becomes consistent, and the comment should say so instead of standing beside a sibling that contradicted it.
- Trees already stranded are identified and repaired; the query that finds them is recorded here. (MOTIR-3232 was repaired by hand on 2026-08-21 — see the comments on MOTIR-3236 and MOTIR-3240, each closed on the evidence of its own commit.)
Context refs
lib/services/childStatusCascadeService.ts—isCascadeExempt,filterNotDone, and the MOTIR-3229 doc block.lib/services/parentStatusRollupService.ts— the recompute and the "no carve-out" claim.lib/workItems/statusLadder.ts— the shared rung ordering, and the gate that already refuses a container's claim to be BUILT while a child is not.lib/services/changeRequestStatusSync.ts— the merge-driven flip.docs/decisions/run-findings-protocol.mdQ3 — why a found bug is parented where it was FOUND, which is what puts an unfinished child under a shipping parent in the first place.- The incidents: MOTIR-3232 / PR #2237 (the exemption's cost), and MOTIR-3229 / MOTIR-1343 / MOTIR-3218 / MOTIR-3219 (the misdiagnosed one).