I fixed four pipeline bugs in three days. The success rate was 20%.

The queue had three problems visible before 09:00 on the 13th. Queue id=36 (“Scheduling articles through Telegram taught me cron is the wrong model”) was blocked. Id=35 had cancelled at 08:00 UTC, the third cancellation in 72 hours. The 09:00 BST slot missed; the 12:00 BST slot also missed. Four distinct failure events before noon. Autonomous success rate across the previous five scheduled slots: 20%.

Working backwards from that number is where the postmortem started.

Four fixes, in order

Em-dash density, 2026-06-12. Three captainrandom articles auto-published on 2026-06-10 and 2026-06-11 shipped at 4.5×, 8.6×, and 12.3× the style guide’s em-dash budget. The ceiling is three per thousand words, and the scrubber raised zero flags. The scrubber was checking density per paragraph. An article that spread its em-dashes evenly across paragraphs would never trip the flag even at ten times the budget. The fix: move the check to article scope, compute against total word count. Shipped 2026-06-12.

Duplicate gate, 2026-06-11. PR #97, “Two bots, one token, one very confusing 409 — fixing BBBrain’s polling conflict,” opened at 12:00 BST as a duplicate of the already-live 2026-05-28 article covering the same incident. The G-L3 duplicate gate had not caught it. The gate was querying against articles shipped via cmd_ship; the live target had been shipped by a different path and was invisible to the query. Fix: check live target regardless of ship path. Shipped 2026-06-11.

G-L3 alias-map, 2026-06-13. The Friday morning integration test exposed an over-aggregation in G-L3’s alias map: platform-tag matches were being ignored for topic_key derivation. The derivation was using the primary tag match only, not the full match set. This is why id=36 was blocked: the article’s topic_key had been derived without its platform-tag component, and the downstream uniqueness check flagged a false collision. Fix: use the full match set, including platform-tag hits. Shipped 2026-06-13.

Substitute pool eligibility, 2026-06-13. The 12:00 BST walkthrough for queue id=37: schedule_sweep fired the publisher at 11:00:08Z; the publisher cancelled the row (internal note: publisher_cancelled_row). Fix C alarmed twice across the morning. The substitute pool was pulling from future-scheduled rows and recursing naturally, correct by its own logic, but “eligible” was resolving to rows that hadn’t been planned for today. Combined with the SLA recovery layer catching id=35’s cancellation at 08:00 UTC, the failure mode B-016 that prior SLA work had addressed was re-surfacing on the recursion path. Fix: narrow eligibility to exclude future-scheduled rows; extend the SLA recovery to cover the recursion path. Shipped 2026-06-13.

What each fix did to the system

Each fix was correctly scoped. Em-dash: the scrubber’s scope. Duplicate-gate: the query’s blind spot. The G-L3 alias-map fix corrected topic_key derivation that had been ignoring platform-tag input. The substitute-pool fix narrowed eligibility that was resolving too broadly.

None of those fixes caused the next failure. The cascade reading is wrong if it implies causation. Each fix closed one hole and left the next one reachable. The em-dash fix changed what the scrubber flagged. It did not change how candidates were derived or how the queue was populated. The duplicate-gate fix changed G-L3’s query. The alias-map aggregation sat in the same module but was unrelated to the gate’s logic. The alias-map fix corrected topic_key derivation without touching substitute-pool eligibility.

The failures look sequential because the fixes were applied in sequence. The underlying vulnerabilities were already present in parallel. The sequence of fixes happened to walk into them one at a time. This is worth naming precisely because the natural reaction to a postmortem like this one is to conclude the fixes were incomplete. They weren’t. Each one is correct. The cascade was a property of the system’s surface area, not of the individual patches.

What single-row diagnosis misses

Each of the four fix sessions started from one blocked or failing row. That’s the right scope for a fix session: you have a symptom, you trace the cause, you close the gap. The problem is that starting from a single row produces a diagnosis shaped like a single row.

The em-dash violation across three consecutive published articles is not visible when you start from a blocked row. It’s visible when you look at the published output as a set and run the style-guide numbers across all three. The substitute pool consuming future-scheduled rows is not visible from one missed slot. It’s visible when two consecutive slots miss and you trace what schedule_sweep did each time.

The postmortem on Friday morning started from the success rate: 20% autonomous across five slots. That number reframes everything. Five separate blocked or cancelled rows look like five incidents. The 20% view says: whatever the individual causes, the pipeline as a system is failing four times in five. Individual diagnosis finds individual bugs. System-level diagnosis finds the class of problem those bugs belong to.

The individual fix sessions each produced a correct patch. The postmortem produced a question: what failure modes are currently present but not yet triggered? That question doesn’t surface when you’re looking at a single blocked row. The Friday morning integration test ran across a batch of candidates, including platform-tagged ones, rather than a single candidate. That’s why it surfaced the alias-map aggregation when the single-row sessions had not.

What changed

The four fixes that shipped by end of day 2026-06-13:

  • Em-dash check scope: paragraph → article
  • Duplicate gate query: cmd_ship path only → all ship paths
  • G-L3 topic_key derivation: primary tag only → full match set including platform tags
  • Substitute pool eligibility: future-scheduled rows excluded; SLA recovery extended to the recursion path

None of those is a structural fix. The structural question is why it took a postmortem to compute the 20% number rather than having it as a standing metric. The signal was always derivable from article_queue; the question is where it sits in the monitoring surface. A per-week slot success rate visible on the dashboard would have surfaced the pattern earlier and might have prompted the batch integration test before the fourth fix was needed.

Out of scope for this sprint: rebuilding the monitoring surface to include standing success-rate metrics. The immediate state: four fixes shipped, the cascade mode is closed, and the success-rate view goes on the backlog as the structural item the postmortem surfaced.

All writing