Operator documentation: a reviewer run from a fresh checkout¶
| Date | 18 October 2026 |
| Work item | Operator documentation and PoC package |
| Pages run | Operator guide, walkthrough (S0-S5), recovery (interrupted operations and restoring) |
| Decision enabled | Whether the operator pages can be followed as written on the disposable fixture, and which steps had to change first. |
1. Setup¶
A fresh clone of ginsys/bronzeward at c4beda4 (main after the walkthrough PR), outside every
worktree, with no local state carried over. The recovery page was run from its pull request's
head (cd8226d), before that pull request merged.
Pinned by the checkout: Go 1.27.1 (mise.toml); Talos v1.13.6, Kubernetes 1.36.2, PostgreSQL 17
(alpine digest), OpenBao 2.6.1 (fixtures/versions.env). Host: Docker Engine 26.1.5, Linux amd64;
mise 2026.5.18 with paranoid false and no trusted-path settings for the clone's directory.
Every secret is the fixture's synthetic material; the one rotated automation token was written to a
mode-600 file and never printed.
The reviewer is an agent session that wrote neither page. It ran each code block in order with the
pages' own helpers (mint, call, etag). Where a section starts from a state the page does not
create (an operation cut off mid-flight), the run produced it with the fixture's interruption seam
and link cuts, the mechanics fixtures/scenarios/s6 uses (the scenario itself was not run, so its
own prerequisites do not apply). Observation and generation choices followed the pages' prose rules
over the full listings.
The interrupted states were produced on the fixture the walkthrough left, with A paused throughout.
interrupt <step> <kill|stall> writes <step> <kill|stall> to instance C's control file
fixtures/.state/server/c/interrupt by rename, and interrupt - removes it. Each release changed
the label of the s3-handover fragment, was published, and was planned for the worker and approved.
| Case | Sequence | State reached |
|---|---|---|
| Cancel | inject linksplit B worker; inject kill B; start C; release; interrupt dispatch-committed kill; plan (transport 120 s) and approve; wait until C exits; interrupt -; inject netsplit worker; inject start B; inject netjoin worker |
unresolved with no attempt; B cannot read the worker until inject linkjoin B worker after the resolution |
| Send | inject kill B; inject start C; release; interrupt dispatch-attempted stall; plan (transport 30 s) and approve; wait for one attempt; inject netsplit worker; interrupt -; wait for unresolved |
one attempt with no response |
| Snapshot T | in the send case's state: inject db-snapshot rv30-t, then inject bao-snapshot rv30-t (12:06:22 UTC); then inject start B; inject netjoin worker |
restored in recovery step 1 after the send case completed |
2. Results¶
| Page, step | Outcome |
|---|---|
| README Build | Failed first: mise run build refuses an untrusted mise.toml in a fresh checkout. Passed after mise trust (D1). |
| README Configure | The copy is mode 600 as documented, but untracked and not ignored (D2). |
| README flags | Every command and flag the page names exists. |
| Start the fixture | up, bw migrate, inject start A/B/C, seed, /livez 204: as documented. |
| Walkthrough S0-S4 | Recorded, imported, adopted, published, planned, approved and applied as documented; S4's timeline showed the commitment, the attempt, the response, the classification and the applied change. S2's review block did not show the renderer the prose names (D3). |
| Walkthrough S5 | Drift opened on an out-of-band patch; adoption and the revert each ended with openDrift null. |
| Recovery: interrupted operations | Read, take over, account and cancel worked once the generation and the observations were read across every page (R-D1, R-D2). |
| Recovery: restoring, steps 1-6 | All six steps as documented: 409 recovery-mode-active before the entry; the entry named the restored operation; the restored automation token was refused until rotated; scope accounting completed the restored operation; marks ready, releases released, exit returned both scopes to normal. |
Recovery details (12:00-12:14 UTC):
- Cancel case (C killed after its commitment, B cut from the worker):
attemptswasnull(R-D3); a 9-character key got400naming the key rule;{"generation": 1}got409, since a start-time takeover had moved the generation to 2 (R-D2); the resolution tocancelledgot201. - Send case (C stalled after its attempt, the worker cut off): the current generation (2) took
it over (
201, generation 3). Accounting with the first read after the transport deadline and a recovery read 30 s later got201, and the operation completed. - Restore to a snapshot holding that operation unresolved: stop all, restore PostgreSQL and
OpenBao, recovery start, entry (
201, one operation), seed token401, rotate (200), normal restart, account both machines (201: the workerunresolved, then completed; the control planeblocked), mark, release, exit (201). The worker ended with Desired equal to Applied and no open drift; the rotated token still worked after the exit.
3. Divergences and fixes¶
| ID | Where | Observed | Fix |
|---|---|---|---|
| D1 | README Build | mise ERROR Config files ... are not trusted, rc 1, from mise 2026.5.18 on this host; other mise versions or trust settings may not refuse |
mise trust line in the build block (this change) |
| D2 | README Configure | bronzeward.yaml, holding the DSN password, shows as untracked; up counts it as a changed path |
/bronzeward.yaml in .gitignore (this change) |
| D3 | Walkthrough S2 review | jq .sources omits the renderer the prose names |
jq '{sources, renderer}' (this change) |
| R-D1 | Recovery, timeline and observation reads | The timeline is oldest first. The worker's held 605 entries; page 1 (limit=500) ended at revision 500 with next set and held none of the interrupted operation's entries. Observations are listed by id, not by time (internal/api/observations.go), so the first qualifying read is found only over every page, by basis |
Follow next with &cursor=; sort observations by basis (fixed in the recovery pull request) |
| R-D2 | Recovery, take over | The literal generation 1 is stale after a start-time takeover | Read the generation from the last commitment or ownership-transition entry (fixed there) |
| R-D3 | Recovery, read | attempts is null before the first attempt |
Stated (fixed there) |
The walkthrough's S5 note, which said the step was unverified until a reviewer ran it, now records this run.
4. Limitations¶
- The recovery page ran at its pull request's head before the R-D1-R-D3 fixes; the fixed text was not run again. The fixes change how values are read, not the acts.
- The observation list stayed on one page (272 entries) in this run, so paging was observed only
for the timeline. The run's picks scanned that whole page by the prose rules; the accounting
calls they fed returned
201. - Not run: the limits page, the troubleshooting page's commands, and a credential-store restore.
- One fixture, one host, one run: no timing claim beyond what each step returned.
- Three of the reviewer's own wait loops timed out on a quoting error in its driver; the reads
after each confirmed the documented state (S4's timeline; S5's release, source and
openDrift).