v1.22.0 ·
The issue said nothing, and said it truthfully
The problem
One release earlier, this project shipped the ability to project its roadmap onto GitHub Issues. The first real output was read by the person who asked for it, and the verdict was two words: pretty lame.
Here is what an issue said, in full:
Projected from this repository's AIDLC roadmap.
Item: item-20260905-automation-verdicts-vs-reality File: .aidlc/roadmap/backlog/20260905-automation-verdicts-vs-reality.md
The roadmap file is the source of truth. Edit it there — this issue is a rendering of it, and its title and aidlc: labels are overwritten by aidlc roadmap sync. Comments here are never copied back into the repository, and this description is never rewritten after the issue is created.
Every word of that is true and none of it is useful. Three separate failures are stacked in nine lines.
It is a redirect, not a description. The file path is inline code, not a link, so even a reader with a checkout has to translate a path by hand. A reader without one — product, support, a contributor, the author on a phone — has nowhere to go at all. The entire point of putting the roadmap where the team already triages is that people who will not clone the repository can read it, and this body defeats that in its second line.
Every projected issue is interchangeable. The title carries the only item-specific words. On a board of thirty issues the bodies are thirty copies of the same paragraph, which makes search worthless, skim worthless, and email notifications worthless. The item quoted above is a genuinely interesting piece of work — four lifecycle automations whose verdicts do not match reality, three of them on the framework's own recommended path — and none of that reaches the tracker.
It cannot become right later. The last sentence is the sharp one: this description is never rewritten after the issue is created. So an item edited on disk leaves its issue permanently stale, and every issue created before a fix keeps the thin body forever. The projection converges on titles and on labels and on open-versus-closed, and on the one field a human actually reads it does not converge at all.
The thin body was not an oversight. It was argued for, in a docblock, on three grounds that were all real. The item↔issue link is an HTML comment, so an item that merely quotes a marker — that feature's own requirements document does — would stamp a second marker onto a different issue and make the identity map ambiguous. Mentions and issue references in item prose fire notifications and cross-links the instant they are published, which is the same class of harm the per-status eligibility rule exists to prevent. And the platform caps a body at 65,536 characters, which a generated body cannot exceed by construction and a projected one can.
Three real hazards, one conclusion, and the conclusion was wrong: it traded the feature's entire purpose for the three mechanisms it did not want to write.
How it could be solved
Four decisions. One of them was settled by a single command, and it inverted the design.
Enrich new bodies, or make bodies converge. The cheap fix is to write a rich body at creation and leave the freeze in place. Rejected, because it fixes nothing that matters: every issue created before the change stays thin forever, and every later edit to an item makes its issue quietly wrong again. So a fifth write outcome was added, and the stated objection to it turned out not to hold — the previous design said rewriting a body "would delete the context around [comments]", but issue comments are separate entities from the body and a body edit keeps its own revision history. The alternative considered was a one-off migration command; this project has a recorded lesson that a capability needing a manual post-deploy command will be un-run, and it cost five days of reporting zero dollars to learn.
What made the rewrite safe was drawing a boundary rather than claiming ownership of the whole field. The projection owns the bytes between two delimiters and nothing else, so a note somebody adds above or below them survives every sync. That also gave the drift check its shape: compare region to region, never body to body, or an issue somebody annotated would be rewritten on every run.
Sanitize code-blind, or code-aware. This is the one that measurement decided. The plan was to strip
comments and defuse mention-shaped and reference-shaped spans in item prose. Running that predicate over
all 76 real items first — 451,503 characters — returned a table nobody had predicted: 40 of the 41
hazard-shaped spans sit inside a fence or an inline code span, and exactly one is in prose. The in-code
spans are an npm scoped package name, a pinned GitHub Action, and CSS colour hexes that a #123 pattern
also matches. A code-blind sanitizer would have corrupted forty legitimate spans in order to defuse one.
Code-awareness therefore stopped being a refinement and became the primary correctness requirement, and one primitive now decides what counts as code — consulted by the sanitizer and by the marker scanner both, because if those two disagree about whether a quoted marker is a marker, the projection refuses runs over a hazard that is not there.
Tightening that same scanner corrected the requirements twice, through a recorded amendment. The first scan recognised single-backtick spans only. A stricter one found that one reference is in prose after all — a real pull-request number, which upgraded a synthetic positive control to a real-data one — and that all 32 item-id references are code spans whose entire content is the id. The original linking criterion had been written for prose and could never have fired once on real data. It was retired and replaced rather than quietly reinterpreted.
Invisible defusal, or visible. The well-known trick for breaking a mention without changing what a reader sees is an empty HTML comment between the sigil and the name. Rejected on two grounds. The rule immediately before it removes HTML comments, so the sanitizer would be adding back the exact construct it strips; and it puts another comment into a body whose identity marker is a comment. Backticks are visible, which is the cost, and they are literal by CommonMark itself rather than by a quirk of one renderer, which is the reason. One span per corpus renders in code style. That is the honest trade.
A branch name in permalinks, or HEAD. Reasoning says main. Measuring says otherwise: on the day
of the change, one well-known repository returned 200 for a file under blob/HEAD/ and 404 for the same
file under blob/main/, because its default branch is trunk. Hardcoding a branch would have shipped
broken links to every project that renamed one. HEAD also needs no extra API call and puts no remote
state in the body — which matters more than it sounds, because anything volatile in a body differs from
the stored copy on every run, and the projection would never converge again.
How AIDLC solves it
aidlc roadmap sync now publishes the roadmap item, not a pointer to it. Until this release
the issue body was a fixed six-line notice — the item id, the file path as inline code, and a
paragraph explaining that the file was the source of truth. That made every projected issue
interchangeable: thirty issues on a board carried thirty copies of one paragraph, and the only
item-specific words anywhere were in the title. For the person the feature exists for —
somebody triaging in the tracker who will not clone the repository — the issue was a redirect
to a file they could not open.
The body now carries a facts block, the item's whole prose, and links. The facts come straight
from the frontmatter: status, created date, author, source, dependencies and the instance the
item was promoted to, with not recorded printed for anything the item does not state rather
than a blank or a guess. The roadmap file is a working permalink. Item ids named in the prose
resolve to links to their own files, so an item that supersedes four parked predecessors now
publishes four clickable references instead of four opaque slugs.
The permalink form was measured rather than reasoned. On the day of the change, one well-known
repository returned 200 for its README under a blob/HEAD/ path and 404 for the same file
under blob/main/, because that repository's default branch is trunk. Hardcoding a branch name
would have shipped broken links to every project that renamed its default branch, so the
generated links use HEAD — which also needs no extra API call and puts no remote state in
the body.
The second half of the change is that the body is now kept in step. It used to be written once
at creation and never touched again, which meant every issue created before this release stayed
thin forever, and every later edit to an item left its issue quietly contradicting the
repository. A fifth write outcome, update-body, is planned whenever the stored region differs
from a freshly rendered one. The projection owns exactly the bytes between two delimiters;
anything a human writes above or below them survives a re-projection, so an issue description
remains usable as a place to add a note. Comments were never at risk — they are separate
entities from the body — and that was the stated reason the body had been frozen.
Publishing item prose was the thing the previous release deliberately refused to do, and its
three reasons were all real. Each is now answered rather than avoided. A quoted aidlc-item
marker can no longer inject itself, because HTML comments are stripped from prose — lossless,
since a comment renders as nothing. Mentions and issue cross-references in prose are wrapped in
backticks, so a sync notifies nobody and cross-links nothing. And the body is bounded by a
budget below the platform's own limit, truncating at a safe boundary and naming how much was
omitted.
The decision that made all three tractable is that every rule is code-aware, and that came from a measurement taken before any code was written. Across all 76 items in this repository's roadmap — 451,503 characters of body text — 40 of the 41 hazard-shaped spans sit inside a fence or an inline code span, and exactly one is in prose. The in-code spans are npm scoped names, a pinned GitHub Action, and CSS colour hexes that a naive issue-reference pattern also matches. A rule that ignored code would have corrupted forty spans in order to defuse one. So a single primitive decides what counts as code, and the sanitizer and the marker scanner both consult it — which is also what stops the two disagreeing about whether a quoted marker is a marker.
That measurement corrected the requirements twice, through a recorded amendment. The first corpus scan used a weaker inline-code pattern that recognised single-backtick spans only; a stricter one moved two figures. One issue reference turned out to be in prose rather than in code, which gave the cross-reference rule a real positive control instead of a synthetic one. And all 32 item-id references turned out to be code spans whose entire content is the id — so the original linking criterion, written for prose, could never have fired once on real data. It was retired and replaced.
Six defects were found by running things rather than by reading them, and two are worth naming because they would have shipped as the exact failure the design warned about. The renderer emitted a trailing newline that the extractor could never return, so every item would have planned a body refresh on every run, forever — non-convergence, introduced inside the mitigation for non-convergence, caught by a convergence test written for the previous release. And the code mask never marked the newlines between fenced lines, so the truncation point could land inside a code block and leave a fence open, rendering the rest of an issue as one block.
Verified end to end against this repository before release: a dry run planned a label update and a body refresh for the issue whose thin description prompted the work, plus a create for this instance's own item. The rendered body for that issue is 8,304 characters, and its four superseded predecessors resolve to working links.