Prose doesn't execute
Two documents said the daily heartbeat surfaced our blog staleness, citing a spec section that did not exist. History showed why: the wiring was deliberately deferred behind a sign-off gate, and that reasoning was honestly recorded — in the commit message. The documentation described the same work in the present tense. 59 branches then landed on main with nothing flagging it. Deferred work written in the present tense stops being a plan and becomes a claim.
The problem
This blog is supposed to publish every couple of days. It went six days without a post. That is not interesting. What is interesting is that the repo's own documentation said the daily heartbeat was surfacing exactly this drift — and had said so, in the present tense, since the day that wiring was deliberately not built.
The cadence system had two halves, and both were real:
- A miner.
scripts/blog/blog-candidates.mjsreads the post registry, computes days-since-last-post, and — when stale — ranks candidate topics out of the recent merges. It worked. It still works. - An authoring pipeline. A skill that takes a topic and produces a finished bilingual post: mine the material, write it, fact-check it against the repo, build-verify, review, land it. That worked too.
The connector between them — the thing that would run the miner on a schedule and act on the answer — did not exist. Not "was broken". Did not exist.
It was, however, thoroughly documented. Both halves pointed at it. The skill said:
One post every ~2 days. node scripts/blog/blog-candidates.mjs reports days-since-last-post and, when stale, ranked candidate topics mined from merges. The daily heartbeat surfaces the same (HEARTBEAT-SOP §7.1).
The miner's own header repeated it, listing its consumers: the authoring skill, and "the daily AI-heartbeat (HEARTBEAT-SOP §7.1)". Two files, agreeing with each other, describing a working integration in the present tense.
Here is what was true of the document they both cited, at the commit just before the fix:
In docs/ops/HEARTBEAT-SOP.md | Occurrences |
|---|---|
The string 7.1 | 0 |
The word blog, in any case | 0 |
| Numbered headings of any kind | 0 |
And the executor those files named? scripts/ai-team-manager.mjs — the script the recurring agent patrol actually runs each cycle — contained the word "blog" zero times. It ran exactly two standing duty checks, and neither was this one.
Both halves were real and both worked. Only the arrows between them were fictional — and the arrows were the part written in prose.
After the previous post landed, 59 branches landed on main — 248 commits — before the gap prompted an investigation. Not one of them tripped anything, because there was nothing to trip.
The part that makes this worth writing down
My first instinct was that someone had fat-fingered a section number. That would be a boring bug. The truth is more useful, and I only found it because a reviewer refused to accept my first explanation and went digging through history.
One commit created the miner, created the authoring skill, and wrote the §7.1 reference into both. That same commit's message says, in plain words:
Heartbeat SOP §7.1 wiring is a privileged-file edit (gated) — left for explicit human sign-off; the tools work standalone (/blog auto, --nudge) meanwhile.
So the reference was never a mistake. It was a deliberate deferral, correctly reasoned and honestly recorded. The SOP is a protected file — the kind that defines what the automation's gates mean — so wiring the duty into it needed a human signature that had not been obtained yet. The author knew. The author wrote it down.
The author wrote it down in the commit message.
And in the documentation, the same work was described in the present tense: the daily heartbeat surfaces the same. A month later, one of those two records was still sitting in the normal workflow; the other needed someone to go and ask for it. In fairness, the routing does exist — git blame on that exact line lands on that exact commit. It just requires you to already suspect the sentence — and a confident-sounding sentence is the kind least likely to provoke that suspicion.
Two more things then happened, quietly. The signature never came. And the SOP was restructured — at the time the reference was written it really did use numbered headings, including a ## 7. §7 Emit ONE bilingual digest, so §7.1 named a plausible slot underneath a section that existed. Six days later a rewrite replaced those with named headings (## Operating Principle, ## Entry Points, ## State Machine), dissolving the anchor too. By the time anyone looked, the pointer was orphaned twice over.
The general shape, and the reason this outlives our particular repo:
Deferred work described in the present tense stops being a plan and becomes a claim. The reason it was deferred sits in a commit message, reachable only if someone thinks to look. The claim sits in the documentation, which is what the normal workflow reads.
What to actually do about it:
- Write deferred integrations in the future tense, in the document itself, with the blocker named. "The heartbeat will surface this once the SOP entry is signed off" is a sentence that ages into an obvious question. "The heartbeat surfaces this" ages into a lie. Changing the tense would not have built the missing consumer — but it is the cheapest correction available, and it keeps the document honest until someone does.
- Never let a commit message be the only place a caveat lives. If a limitation still matters a month later, it belongs where the behaviour is documented — or in a test. History is reachable, but nothing in the normal workflow routes a reader there unless they already suspect the line and run blame.
- Cite symbols, not section numbers. Both are mechanically checkable in principle — the 0-occurrence table above is itself a mechanical check. The difference is what already exists:
checkBlogCadenceresolves to a definition or a call site using the language tooling any language-aware editor already ships. A plain-text§7.1needs a checker somebody has to write, and it breaks silently under renumbering — which is precisely what happened. - When you inherit a two-half system, search for the call site, not the description. "Producer exists, consumer documented" is a high-risk shape, because everything looks present in review.
What shipped
The patrol script already ran two standing duty checks each cycle, each returning one of three things — satisfied, a finding to assign, or blocked. The blog cadence became the third. It keys on the newest date in the post registry and files a finding when that date goes stale:
function checkBlogCadence({ postsFile, today = localDate(), staleDays = BLOG_STALE_DAYS } = {}) {
if (!postsFile) return { blocked: 'blog posts registry path unresolved' }
let src
try {
src = fs.readFileSync(postsFile, 'utf8')
} catch (err) {
return { blocked: `blog posts registry unreadable: ${err.code || err.message}` }
}
const dates = parsePosts(src).map((p) => p.date).filter(isRealIsoDate)
if (dates.length === 0) {
// A registry that parses to zero dated posts means the regex drifted from
// posts.ts, not that the blog is current — refuse to report the duty green.
return { blocked: `blog posts registry parsed 0 dated entries: ${postsFile}` }
}
const latest = dates.sort().at(-1)
const ageDays = daysBetweenUTC(latest, today)
if (!isBlogStale(ageDays, staleDays)) return { ok: true }
return { finding: /* … assigned, with the miner named as the topic source … */ }
}
The blocked branches matter more than the happy path. A duty check that can't read its input must report unknown, never satisfied — otherwise the first filesystem hiccup silently converts "I don't know" into "all good". The zero-entries branch is the sharper one: if the registry ever gets restructured past the parser, the honest answer is "my parser is broken", not "the blog is current".
Then the fix reproduced the bug. Three times.
The patch went through three finding-bearing rounds of independent review before a fourth round approved it with an empty list. Three of the findings were the same shape as the original — a document asserting a relationship that nothing enforced — wearing different costumes.
1. I deferred it behind the same gate, and wrote the same present tense
My first patch replaced the dead §7.1 with a live-sounding see HEARTBEAT-SOP "Standing Proactive Duties" item 3. There was no item 3 — because the SOP is a protected file and I had not obtained the signature either. I had just reproduced the original mistake, for the identical reason, in a patch whose entire premise was that this mistake had been made. The reviewer's words: "the patch recreates the exact dangling-reference failure mode it claims to fix."
It never reached main — it was caught on the branch and rewritten before landing. The fix was to point at the code that actually enforces the duty and state plainly, in the document, that the SOP entry is still pending a signature. If a step of your change is blocked, the change is not smaller — it is incomplete, and the incompleteness belongs in the docs, not just in your head.
2. Shared helpers are not shared behaviour
To stop the duty and the miner disagreeing about "stale", the duty imported the miner's parser and date arithmetic. The import even carries a comment saying it therefore cannot drift. It could drift. It did.
| Definition of "stale" | Fires on day | Honours override |
|---|---|---|
Miner: daysSince >= STALE_DAYS | 2 | yes |
Duty, as first written: ageDays <= 2 | 3 | no |
So on the day a post turned exactly two days old, running the miner printed 🟡 STALE while the same hour's patrol logged standing duties current (… + blog cadence). Two components, one importing from the other, sharing a parser and contradicting each other about the only question that mattered. The environment override the miner honoured, the duty ignored entirely.
The fix was to export the predicate rather than the parts, and have both callers use it. Sharing the helpers a decision is built from does not share the decision. If two components must agree on a judgement, export the judgement.
3. An owner whose default contract would have refused the work
The duty was first assigned to the role that already owns our other content duty. That role's charter says it does not write production code, and its output contract is enforced by a hook requiring funnel and experiment evidence that a blog handoff does not produce. Had that version landed, every finding would have routed by default to a role whose own contract could reject the work.
An exemption was available: the hook has a documented per-invocation skip. Reaching for it would have meant every blog handoff riding an escape hatch built for a different purpose, so the owner changed instead, to a role permitted to do the work. An assignment your own guardrails refuse by default is the same failure as a pointer to a missing section — an unenforced claim about how the system behaves.
The honest limits
- This does not write the post. It converts silent drift into a visible, assigned, hourly finding. Someone — a person or an agent — still has to do the work, and the cadence can still slip if that assignment is ignored. Detection is not production.
- The SOP entry was pending when this was drafted; it has since been signed off. For about a day, the duty ran while the protected operating document still did not describe it — the same gap section 4 is about. The difference this time is where the caveat lived: in the documentation, in the reader's path, rather than in a commit message. That is the whole recommendation, and it is the reason this sentence could be updated instead of quietly rotting.
- We did not build the general fix. The new tests cover the duty's behaviour — thresholds, routing, blocked states, registry parsing. None of them reads the documentation and asserts that the symbol it names exists. The doc-to-code reference checker this post argues for is not shipped, and we have not audited the repo's other cross-references.
- Review caught these, not tests. Two separate reviews did the work: an independent review of the duty patch found the three defects in section 4 before it landed, and a later fact review of this post went digging through history and overturned my first explanation of the root cause — I had written it up as a fat-fingered section number. Mandatory independent review is a real process and it worked twice. What was missing, both times, was anything mechanical.
Recap
- The bug: two working halves of a system, an integration between them described in two documents, and no code connecting them.
- The root cause, which was not carelessness: the integration was deliberately deferred behind a human sign-off gate, and that reasoning was recorded in the commit message. The documentation described the same work in the present tense. A month later, only one of those was still in the normal workflow's reading path.
- The cheapest correction: future tense, in the document, with the blocker named. "Will surface, once X is signed off" ages into a question; "surfaces" ages into a lie. It does not build the missing consumer — it just stops the docs lying about it while you wait.
- The durable habit: cite symbols, not section numbers. Both can be checked mechanically, but only one comes with tooling that already does it, and only one survives a renumbering. Ours was orphaned twice: the wiring never landed, and six days later the numbered headings it referenced were replaced with names.
- The rule from the same incident: if two components must agree on a judgement, export the judgement, not the helpers it is built from.