The documentation site and the milestone pages
Context
The docs repo holds the ADRs and, later, the runbooks. Its charter so
far is to describe what is landed, never what is coming. The delivery of
each ADR is a GitLab group milestone with issues across the repos, and the
only view of a milestone is GitLab’s flat issue list: the order, the
blockers, what the operator can start today and why the plan drifted live
in the operator’s head.
The operator wants a public static site at the.corvus.observer that
renders the ADRs, one page per milestone in a readable narrative form
(decision, what to do next, the path with blockers, open questions,
deviations, history), and the relation between the two in both directions.
The site is built when main of docs changes, and the milestone state
is refreshed by the operator, by hand, about once a day.
Decisions
The charter includes milestone pages
A milestone page is the current state of a milestone as of its last sync. Current state is documentation. The “what is landed” rule keeps applying to ADRs and runbooks; a milestone page describes what is landed and what is in flight, and is never a place to design.
Data and narrative, split
Each milestone has two files in docs:
- the data, a YAML file generated by the sync from GitLab: issues, states, labels, MRs, dependencies, the computed path, the unblocked set, closed items. Never edited by hand. Issue and MR metadata only (titles, states, labels, links), never bodies or comments, because the site is public.
- the narrative, a markdown file owned by the operator: frontmatter (title, slug, GitLab milestone id, the ADR it delivers), then the decision in one paragraph, the phases as a story, open questions, deviations and noise, out of scope, timeline.
Regenerating the narrative with a language model on every sync was considered but rejected: the prose would drift with no change in the facts, and the MR diff would stop showing what moved. Instead the sync reports drift between narrative and data in the MR description, and the operator edits the narrative like any other docs change.
The sync is an MR, once a day
A skill in agents, sync, fetches every open milestone, precomputes the
derived data, writes the data files in a worktree of docs, and opens one
MR per run with the drift report. The operator’s merge publishes. Pushing
to main directly was considered but rejected: a branch is an MR
everywhere in corvus, and the diff of the data file is the daily review.
The fetch and the precomputation are a shell and jq script, reproducible
without a language model; the model writes only the drift report.
Order comes from the “Depends on” section
Every issue body ends with a “Depends on” section: a bullet list of
issue references, one per line, written by the plan skill from the
titles in the plan. It is the single source of order; milestone and
the sync parse it and nothing else. A reference survives a rename, and a
line per entry means a title with a comma never splits. The skill also
creates relates_to links, so GitLab’s “Linked items” panel shows the
relation in both directions, but those carry no direction and are only
cosmetic. Phases are scoped labels set by plan.
GitLab’s blocking issue links were the first choice, structured and queryable, and were rejected when the API answered that blocked issues are not available on the group’s plan: they are a paid feature, and the site is not worth a subscription. Parsing titles out of the section was considered and rejected: a renamed title breaks it silently.
A dependency on an issue of another milestone blocks like any other and is rendered with that milestone named.
The relation to the ADR lives in docs
The narrative frontmatter names the ADR the milestone delivers, single valued; the site computes “delivered by” on the ADR page from it. The GitLab milestone description still names the ADR once, and the sync flags a mismatch. Taking the relation from GitLab was considered but rejected: it is a docs fact, and the build stays a pure function of the repo.
The narrative is born with the milestone
plan scaffolds the narrative in a docs worktree when it creates the
milestone: frontmatter, the decision paragraph from the ADR, empty
sections, and opens the docs MR. The sync flags a narrative that is still
a scaffold. The milestones that predate this record get their narrative
and their “Depends on” sections backfilled once.
Hugo, with precomputed data
The site is built with Hugo: a single binary in CI, YAML data files read
natively, no toolchain in docs. Templates only lay out; everything
derived (path order, unblocked set, timeline of states) is precomputed by
the sync into the data file, so the data file is a complete snapshot and
the build has no logic. Astro was considered for how naturally it would
express the page, but rejected for the Node toolchain it brings into a
repo of markdown. A hand-written generator was considered and rejected as
one more thing to maintain.
The theme is hand-written inside docs (layouts and one stylesheet, no
JavaScript, no external theme). Light and dark follow the system. The
home page lists ADRs and open milestones; each ADR page carries a
“delivered by” box; closed milestones stay reachable from their ADR and
from an archive list. The ADRs gain frontmatter (date, status) so the
build never depends on the layout of their prose.
Published to R2, public
The site lives in a Cloudflare R2 bucket with the.corvus.observer as its
custom domain. Bucket, custom domain, the API token scoped to the bucket
and that token as a CI variable of docs are declared in platform/infra,
the first R2 resource and the first CI variable in tofu. The docs
pipeline builds on MRs and, on main, builds and syncs the output to the
bucket with deletion, so the bucket is exactly the build. The site is
public, consistent with the public GitLab group; Cloudflare Access was
considered and rejected since the site exposes nothing the repos do not.
Consequences
docsgets its first pipeline, including the shared no-references lint. The data file stores repo, issue number and URL as separate fields so that lint never meets arepo#nstring outside markdown.- The operator’s Cloudflare token needs R2 permissions before the first apply; it is scoped to the zone today.
- A milestone page is only as current as its last sync. The sync time is on the page.
- Counters that come from grepping a codebase (how much of the old setup remains) are not in this record. They can join the narrative frontmatter later as declared commands.
planandmilestonechange: references and phase labels at creation, the “Depends on” section instead of guesswork when ordering.