Skip to content

docs(runbook): corrections from the v0.69 media pass - #833

Merged
edspencer merged 8 commits into
mainfrom
docs/runbook-media-corrections
Aug 11, 2026
Merged

docs(runbook): corrections from the v0.69 media pass#833
edspencer merged 8 commits into
mainfrom
docs/runbook-media-corrections

Conversation

@edspencer

Copy link
Copy Markdown
Owner

Four corrections to DOCS-UPDATE-RUNBOOK.md, each of which cost this pass real time. Runbook-only — no assets, no tooling (those are #828 and #829).

1. The 127.0.0.1 leak baseline was stale and did not add up

It claimed 38, attributed all of them to website/src/content/docs/**, then added "plus 6 in README.md" on top — summing to 44 against a stated 38. The correct split, recounted: 39 docs + 7 README = 46, now given as a table so the split can be rechecked rather than just the total.

This matters more than a number being old: the leak check is the only secret-adjacent gate on a docs PR (CI has three jobs and none scans for secrets; GitGuardian looks for credentials, not private hostnames), and a baseline that overcounts is exactly how a genuine new hit hides inside an expected one.

2. What a design release changes about a docs pass

New section. The hard judgement is not which images are old, it is which old images are wrong — and those are different sets. A What's New entry is a record of a release: an old-UI frame there is correct, and replacing it makes it false. So bucket by the tense of the surrounding prose, not by directory — directory is a proxy that fails on precisely the dual-use assets where the decision is hard.

Also records that age was never the criterion: the seven stills the previous pass committed were merged 41 minutes before the first design commit. A day old, and stale.

3. The stale-build trap, and how to actually check

The failure that costs an entire pass while every other check passes: rig up, seed passing, leak scan passing, all shots succeeding, md5sum showing no duplicates — and every frame the old UI. Rebuilding the checkout is necessary but not evidence; it proves the git state of a directory, not what the server serves. Name a UI element only the new build can paint and go look at it, with a negative control on any bundle grep.

Recorded honestly: this check passed first time on the v0.69 pass — but only because the rig's checkout had been rebuilt first. The checkout it would otherwise have served was stale, parked on its own branch at the previous release's baseline. So the section now says: verify the checkout ref before serving, and treat a rig clone parked on an old branch as the normal state rather than an anomaly.

4. Rig fragility: the projects root must be on a persistent volume

The rig kept its projects on a path that was not a mounted volume. A container restart destroyed every project.yaml and every .chats/*.jsonl while the data dir survived — which is the dangerous half, because booting then gives zero projects plus orphaned job records describing chats whose transcripts are gone, and that renders as a subtly broken instance in the screenshots. Hence: wipe the projects tree and the data dir together, or neither.

Plus three smaller ones: positive-control every verification grep (a clean result is either absence or a broken pattern, and they look identical); Cloudflare serves inconsistently mid-propagation, so fetch the asset URL rather than sampling the page harder; and a detection pattern is content — an early capture.mjs carried a private dev domain inside its own leak-detection regex, so the tool written to stop that string being published would have published it.

Docs-only: no changeset, no version bump.

HomeLab Agent added 2 commits August 10, 2026 14:13
- 127.0.0.1 baseline was stale and internally inconsistent: it read 38, and a
  later revision said 46 for the docs subtree then added "plus 6 in README.md",
  summing to 52. Recounted: 39 docs + 7 README = 46. A baseline that overcounts
  is how a genuine new hit hides inside an expected one.
- What a DESIGN release changes about a docs pass: every UI-bearing image goes
  stale at once, so recency stops being evidence. The previous pass's stills
  were merged 41 minutes before the first design commit.
- The stale-build trap and its one-navigation check. This is the failure that
  costs a whole pass silently: rig up, seed passing, leak scan passing, every
  shot succeeding, and the frames are the old UI.
- Rig fragility: the projects tree must be on persistent storage, and the
  projects tree and data dir must be wiped together or not at all.
- Positive-control every verification grep.
- Cloudflare serves inconsistently mid-propagation; fetch the asset URL.
- /proc and ss do not work from a keeper in a container.
- A detection pattern is content.
Four things this pass learned the hard way, none of which were in the runbook:

- The 127.0.0.1 leak baseline was stale AND internally inconsistent: it claimed
  38, attributed all of them to the docs subtree, then added "plus 6 in
  README.md" on top. Correct split is 39 + 7 = 46. A baseline that overcounts is
  how a genuine new hit hides inside an expected one.
- What a DESIGN release changes about a docs pass: bucket images by the TENSE of
  the surrounding prose, not by directory. A What's New entry is a record of a
  release, so an old-UI frame there is correct and replacing it makes it false.
- The stale-build trap, which passes every other check while it happens, and the
  in-browser check that proves the served bundle rather than a directory's git
  state — with a negative control, or you have only proved your grep works.
- Rig fragility: a projects root on a path that is not a persistent volume loses
  every project.yaml and transcript on a container restart while the data dir
  survives, leaving job records describing chats whose transcripts are gone.

Also records what happened when the stale-build check was actually run, since
"it passed first time" and "it fired" are both worth knowing — here it passed,
but only because the rig's checkout had been rebuilt first; the checkout it would
otherwise have served was genuinely stale.
edspencer pushed a commit that referenced this pull request Aug 10, 2026
The same 216-line diff is in #833. Two open PRs editing one 760-line file
conflict on whichever merges second, and this PR is tooling: tools/docs-media/**
is independently reviewable and independently revertable without it.
…d re-checking state

Both are about how this pass itself went wrong, which makes them the most
valuable lessons in it.

'Verify a survey agent's NEGATIVE findings before acting on them' is the sharper
one. A confident claim about what the code does NOT do reads as rigour, and is
the one class of finding whose consequence is deleting correct documentation —
nothing downstream catches it, because the edit makes the docs claim less, so it
builds, reads sensibly and survives review.

'Re-check PR and issue state at the moment you act on it' covers the other
concurrency hazard: several agents work this repo at once, so recon has a shelf
life measured in hours.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 10, 2026

Copy link
Copy Markdown

Deploying paddock with  Cloudflare Pages  Cloudflare Pages

Latest commit: 40d2141
Status: ✅  Deploy successful!
Preview URL: https://1368e3c3.paddock-7u2.pages.dev
Branch Preview URL: https://docs-runbook-media-correctio.paddock-7u2.pages.dev

View logs

HomeLab Agent added 4 commits August 10, 2026 14:18
The last-but-one figure was stale before it was pushed: the commit that recorded
it (#778) also closed two README gaps, one of which added the seventh 127.0.0.1.
That is not drift afterwards — it is the recording PR invalidating its own count.

Last item outstanding from the #830/#831/#832 reconciliation; everything else
those branches carried is already here.
…wn work

Two gaps from the v0.69 pass.

The 'prove you own the instance' recipe used ss(8) and /proc, neither of which
works from inside a container: it has its own network namespace and PID view, so
ss reports no listening socket on a port curl answers with a 200. The failure
points the wrong way — it reads as 'the rig isn't running' while the rig is
fine. Replaced with the check that actually worked: ask the API what it is
(dataDir + driveMode), assert in the seed script, and let it refuse.

And the re-verify rule now covers status claims about your own work, not just
findings about the code. Those are more dangerous, because a status claim is
what stands a worker down — being wrong cancels work rather than adding it.
The rule as written bound only the agent producing a negative finding. On the
v0.69 pass both errors happened within an hour, in opposite directions: the
accent picker's AA floor reported as unenforced (false — solve() never returns
below floor, and repairFill repairs --accent-solid to 4.5), and the correction
over-swinging into 'nothing is enforced' (false the other way). Neither party
had opened accent.ts, and the narrowest true form was available to both: the
floor is enforced, but nothing verifies it.

Aimed at reviewers, who are most likely to feel exempt — correcting a claim
feels like restoring the truth, which is why an unverified correction travels
further than the error it replaced.
A navigation check proves the build was current when someone looked; anyone
auditing later takes that on faith, which is the kind of claim this runbook
refuses elsewhere. The sidebar footer carries the running version, so the
committed screenshot proves it permanently — the v0.69 stills read v0.69.0.

Notes both, because they fail differently: the in-frame stamp does not exist on
a tight crop with no chrome, and a capture-time sidecar covers every shot but is
a separate file that can be lost.
… a check

The mechanism underneath the two rules already in this section, so it sits above
them. Three v0.69 errors shared one shape: a contrast finding accepted without
opening the file, a 'which PR carries the corrections' judgement made from
marker counts rather than a diff, and an attribution settled by a same-minute
mtime — which fits 'racing' and 'independent' equally well, so it could not have
come out the other way. A corroborating check feels like confirmation and costs
nothing, which is exactly why it gets run instead of the discriminating one.

Also names attribution as the expensive case: 'agent B did this' implicitly
asserts 'agent A did not', so it needs a branch, a worktree or a commit that
names an actor. Where every agent commits under one identity, the author field
cannot discriminate, and acting on a weak attribution cancels another agent's
work rather than merely producing wrong prose.
@edspencer
edspencer merged commit d634132 into main Aug 11, 2026
5 checks passed
@edspencer
edspencer deleted the docs/runbook-media-corrections branch August 11, 2026 02:22
edspencer added a commit that referenced this pull request Aug 11, 2026
…830)

* docs(docs-media): document the rig contract, sidecars and the leak-pattern rule

* chore(docs-media): commit the rig launcher, seed and fixtures

seed.mjs and serve.sh existed only on one box and were full of its paths, ports
and process-manager specifics, which made tools/docs-media/ unusable by anyone
else. Both are now driven by PADDOCK_RIG_{HOME,CLONE,PROJECTS,FIXTURES}.

serve.sh was RETYPED from a spec rather than copied: the original held a live
OAuth token in plaintext, and a copy keeps the credential in the editor buffer
and in shell history. It carries no credential and cannot: it re-execs under
env -i, and the rig runs a fake claude on the batch runtime.

capture.mjs gains theme pinning via addInitScript (the appearance keys are read
by a pre-paint script, so writing them after goto shoots mid-swap) and the
provenance sidecars.

* docs(website): re-shoot the stale stills on the 0.67+ UI

Every UI-bearing image on the site predated the redesign, including the seven
committed the previous pass — those were merged 41 minutes before the first
design commit, so age was never the criterion.

Foundation dark at the out-of-box default (DEFAULT_APPEARANCE; paddock:theme
defaults to dark), which is what a reader sees on first boot. The theme quartet
is the deliberate exception: there the choice itself is the subject.

* docs(runbook): correct the 127.0.0.1 baseline split (39 docs + 7 README = 46)

The previous paragraph attributed all 46 to the docs subtree and then added
"plus 6 in README.md", summing to 52. A baseline that overcounts is how a
genuine new hit hides inside an expected one.

* docs(runbook): what a DESIGN release changes about a docs pass

Four things this pass proved the runbook lacked, all in section 5:

- The stale-build trap gets its own warning. Serving dist/ from a checkout
  that predates the release means every re-shot frame is the old UI, and it
  fails SILENTLY: rig up, seed passes, leak scan passes, every shot succeeds,
  md5sum clean. Recorded as a positive check against the SERVED BUNDLE — name
  a UI element only the new build can paint, then go and look at it — rather
  than against the git state of a directory.

- Bucket by the TENSE of the surrounding prose, not the directory. A What's
  New entry records a release as it shipped, so an old-UI frame there is
  correct and re-shooting it falsifies the archive; a current-behaviour page
  showing obsolete UI is simply wrong. Directory is a proxy that fails on
  exactly the dual-use assets cited from both — fork those.

- The rig's own fragility. A projects root on a non-persisted volume loses the
  whole tree on a restart while the data dir survives, so the instance boots to
  zero projects plus orphaned job records: it does not fail, it lies. Wipe both
  or neither, and commit the launcher and seed so the rig is reproducible.

- Appearance is no longer determined by the URL. Pin the theme with
  addInitScript (the keys are read pre-paint), clear the solved-accent cache,
  assert it applied, and never verify a theme by grepping CSS.

Plus the seeding race (rename in a second pass or the transcript's own title
resolution clobbers it) and the md5sum/framing discipline for near-identical
shots. Box paths, ports and domains are written as placeholders.

* docs(runbook): pinning the appearance, and seeding a rig with texture

Runtime themes mean a screenshot is no longer determined by the URL. The keys
are read by a pre-paint inline script, so a capture must write them with
addInitScript rather than page.evaluate after goto (which yields a mid-swap
frame), must clear the solved-accent cache (keyed theme:mode — a stale entry
paints the previous theme's accent pre-boot), and should assert it applied.
Never verify a theme by grepping CSS: OKLCH serialises as oklch(...) and the
accent token is a bare RGB triple, so a regex reader scores a themed build zero.

Also the seeding race: a chat is the product of a turn, so an API-only seed
photographs as an empty app; and renaming a chat immediately after the turn
completes loses to the transcript's own title resolution.

* docs(website): remove the whats-new stills from the captures PR

git rm --cached was not enough: it left the files in the working tree, where the
next blanket `git add -A` re-staged them and silently undid the split. Removing
them from the tree on this branch is what actually holds.

They are committed on docs/media-v069 (#828), which is where #815 should take
them from — an asset and the reference to it belong in one PR, and whats-new.mdx
is on #815.

* chore: drop the captures from this PR — they ship in #829

The stills were pushed onto this branch by a concurrent worker. This PR is the
rig and the tooling, which is code-shaped and independently reviewable; the
frames it produces are a separate review with separate criteria.

* docs(runbook): negative findings must be re-verified by the relaying party

The most valuable thing this pass produced. A wrong positive claim is caught by
the next reader; a wrong negative is acted on by DELETING the evidence — correct
prose is removed and it reads as diligence.

Happened twice within an hour in opposite directions over one file: the accent
picker's floor was reported unenforced (wrong — repairFill does repair derived
tokens at runtime), then over-corrected into "nothing is enforced" (also wrong),
neither time by anyone who opened accent.ts. The rule therefore binds the
corrector too, and negatives must be scoped to their narrowest true form.

Records the control that actually worked: the issue-filing child was told to
re-verify against main before filing, did, and its issues carry the narrow true
claim while the broken version was still circulating above it.

* docs: drop the runbook from this PR — #833 owns it

The same 216-line diff is in #833. Two open PRs editing one 760-line file
conflict on whichever merges second, and this PR is tooling: tools/docs-media/**
is independently reviewable and independently revertable without it.

* chore(docs-media): drop the manifest entry for a shot that ships nowhere

docs-discover.png was captured and deliberately not committed: the rig has no
discoverable Claude Code history, so the route renders its empty state. A
manifest entry for a frame that exists in no PR invites someone to go looking
for it.

The twelve remaining entries all ship — seven in #829 and five in #815. Those
five are NOT pruned despite having moved out of #829: they still ship, and the
manifest is the only record of which theme and solved accent each was shot at,
which is exactly what a reviewer of #815 needs.

* docs(docs-media): restore the README the rig PR is supposed to carry

Building this branch with 'git checkout <other-branch> -- tools/docs-media'
took the README from that branch too, silently reverting the rig-contract and
sidecar sections to main's 69-line version — so the PR shipped the scripts with
none of the documentation that makes them usable, which was half the point.

Also adds the staleness note: because each sidecar records the app version,
'is this frame stale after a repaint?' is a file read rather than an inspection
of every PNG.

* docs(docs-media): note the uniform-timestamp tell as a known seed improvement

seed.mjs creates its chats in one run, so every row in a capture carries the
same relative age — the v0.69 Home frame reads '2h ago' nine times. That is the
hand-seeded-fixture tell the runbook already warns about; a real instance has
this morning's work beside last week's.

The adoptable sessions already back-date their mtimes; the chats do not.
Recorded as a known improvement, not a fix: no committed frame is wrong because
of it.

* chore(docs-media): make HOME overridable, and actually forward it

On Discover the home path IS the content: DiscoverView renders {candidate.path}
and {result.homeDir} verbatim into <code>. So cropping fails (the path is the
subject), masking fails (the leak-masker would blank the element being shot),
and a symlink fails (paddock canonicalises for display). A rig whose HOME sits
under a scratch directory cannot produce a publishable Discover frame at all —
the shot is lost at launch, not at capture.

The override was inert as first written: env -i wipes the environment before the
re-exec, and PADDOCK_RIG_USER_HOME was not in the forwarding list, so the second
pass always fell back to the default. Same for PADDOCK_RIG_FIXTURES, which the
README documented and the launcher could never receive. Both now forwarded, and
verified with a control: without the override HOME resolves to <rig>/home, with
it to the path given.

Also documents the no-recorded-cwd exclusion — a staged transcript without a cwd
is dropped silently, which is the most common way a Discover seed fails while
looking like it worked.

* fix(docs-media): make HOME overridable, or Discover can never be shot

serve.sh hard-coded HOME to a scratch directory. On the Discover screen the home
path IS the content: DiscoverView renders {candidate.path} and {result.homeDir}
verbatim in <code>, with no truncation. So that rig cannot produce a publishable
Discover frame, and every escape is blocked — cropping loses the subject, the
leak-masker would blank the subject, and a symlink is defeated by paddock
canonicalising for display. The reason is in the comment because the reason is
what stops someone simplifying it back.

Also implements PADDOCK_RIG_FIXTURES, which the README documented and serve.sh
never had — so the committed fixtures.json was a dead file and the authored
replies never reached the fake claude. Both new vars are threaded through the
env -i re-exec; without that the override is scrubbed and silently does nothing.

Documents the other Discover trap: a staged transcript with no cwd is excluded
as no-recorded-cwd, which fails while looking like it worked.

---------

Co-authored-by: HomeLab Agent <homelab-infra@valfenda.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant