docs(runbook): corrections from the v0.69 media pass - #833
Merged
Conversation
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.
Deploying paddock with
|
| 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 |
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.1leak baseline was stale and did not add upIt claimed 38, attributed all of them to
website/src/content/docs/**, then added "plus 6 inREADME.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,
md5sumshowing 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.yamland every.chats/*.jsonlwhile 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.mjscarried 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.