The 2 July cron fired at 08:22 BST, ranked 150 clusters, fetched the pack for mortgage (calculator score 20, top of the triage run), and completed without error. Zero ready drafts. The cycle log noted thin ground. The next morning the cron fired again.
That read was wrong. The ground wasn’t thin. The drafter was marking failed clusters as permanently consumed and skipping them on every subsequent cycle. Across ten runs between 30 June and 13 July, exactly one cluster reached the ready-draft state. The skipped counter moved from 2 to 7. Zero articles auto-published.
The state machine error
draft.py maintained an index of processed clusters. A successful draft wrote an entry. A failed draft also wrote an entry, prefixed failed-*. The eligibility check for the next cycle tested for the presence of any index entry. The prefix was not tested.
The consequence: a cluster that failed to draft was, to the next cycle, identical to one that had been successfully drafted. Both had index entries. Neither was re-queued. Only one of them had actually produced content.
This is a state-machine error. The pipeline had two distinct non-terminal outcomes (drafted and failed-to-draft), but the index collapsed them to a single consumed flag. Failure was mapped onto completion. The skipped counter incremented on every pass without any cycle-level output.
Thirteen days, one ready draft
The per-cycle output across the gap read locally plausible each morning. 1 July: one draft of the mortgage cluster, same ground already published the previous day: BBC, Zoopla, Moneyfacts, Bank of England, US-Iran rate framing, £232/£66 regional figures. Duplicate ground; explicable. 2 July: zero drafts, first in the current streak; ascribed to thin ground.
7 July: eight clusters triaged, six packs fetched. A success-no-publish run that read as a ground-quality issue rather than a structural one. 8 and 9 July: the silence continued. The three-weekday gap was the first signal strong enough to demand investigation. Two operator publishes on 10 July (stamp-duty cron draft and Lifetime ISA pillar, both rescued manually) ended the visible streak without diagnosing the cause. The 14-day cadence clock restarted from that date. The dead zone continued until 13 July.
A parallel anomaly raised the noise floor across the same window. The long-slug IHT draft (“uk-inheritance-tax-rate-2026-40-charge-7-75-interest-rate.mdx”) cycled into its third PR over the fortnight: #171 created and deleted within a squash, #176 merged live, regeneration source root-caused on 6 July. A zombie draft cycling through PRs and a drafter producing zero ready drafts produced similar symptom patterns in the logs. They were separate problems. Together, they made each cycle read as an isolated anomaly rather than a compounding structural failure.
Why the silence was invisible
The pipeline had no primitive for per-cycle ready-draft visibility. Post-cycle observation was limited to a single question: did a publish happen? Publication is a lagging indicator. It becomes visible only after the cadence has already slipped. By the time a multi-day gap is legible, multiple cycles have run against the broken condition.
The logs were intact. The problem wasn’t missing information — it was missing aggregation. Each cycle completed and left no summary of its contribution to the ready-draft queue. From the outside, a cycle that produced zero ready drafts because the ground was thin and a cycle that produced zero because the dead zone was consuming every candidate looked identical.
“The cron ran” and “the cron produced a ready draft” are not the same event. The pipeline was only surfacing the first.
The fix
Two changes on 13 July, different concerns.
Failed-cluster re-eligibility. The index check was updated to exclude failed-* prefixes from the consumed set. Clusters with failure-prefix entries are now re-eligible on subsequent cycles. The state machine has two non-consumed states (unprocessed and failed), rather than one. Only a successful draft marks a cluster as permanently consumed.
Ready-draft heartbeat. A per-cycle signal was added to the pipeline output, surfacing the count of ready drafts produced each run. This makes “did this cycle contribute to the queue?” answerable at cycle completion, not inferrable only from downstream publication events. Drift from expected output becomes visible before it compounds.
These address different failure modes. The dead-zone fix removes the cause of this specific gap. The heartbeat is the mechanism that surfaces the next structural silence, whatever causes it. The pipeline needed both: the first stops the current bleeding, the second makes future bleeding observable before it compounds.
What the prior diagnosis missed
Improving on ten-cycles-one-ready-draft-the-pipeline-was-consuming-its-failures: that article identified the ten-cycle window and named the failed-cluster consumption mechanism. What it couldn’t cover was the heartbeat fix: the observability primitive added to prevent the next gap from running thirteen days before diagnosis.
The skipped counter reaching 7 is the number that reveals the scale. Five clusters were added to the consumed set during the gap window, none of them successfully drafted, each one permanently unavailable to the next cycle until the index logic was corrected. The three-weekday publish gap was the surface. The dead zone had been running since 30 June.
A pipeline that maps failure to completion will produce this pattern at any positive failure rate. It doesn’t require a catastrophic event. It requires only that the drafter fails on at least one cluster per cycle and that the index never marks those failures as retriable. Over enough cycles, every active cluster works into the failed set. The queue empties. No single run visibly breaks. The silence compounds below the detection threshold until the cadence slip becomes undeniable.
Out of scope: the 14-day cadence clock remains broken and restarted from 10 July. Cadence logic is a separate sprint. The heartbeat surfaces cadence slip earlier; it does not restore cadence itself.



