feat: a minted box records how it was minted, and box info reads it back #129
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#129
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/mint-stamp"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #103.
cmd_newknows a great deal at the moment it launches, and wrote three keys:Everything else in scope on that line was dropped on the floor. There is no host-side per-box store — the Incus instance config is the database — so a fact not written there is not "hard to find later", it is gone the moment the mint returns.
What the mint knew and dropped
$root/VERSIONwas read in exactly one place,version(), and never in the mint path.box --versionis a fact about the binary in front of you, never about the box you are looking at$T_IMAGEisimages:debian/13/cloud— an unpinned alias on a moving remote. Two boxes minted a month apart from "the same template" are not the same box, and nothing on either said so$T_BOOTSTRAP_ROLEwas handed torig bootstrapand forgottenRIG_REPO/RIG_REFwere substituted into the seed. Which rig converged this box was unrecoverable/dev/kvmThe stamp extends
user.box.*at that same single write point. The three existing keys are untouched —box_user()and the login hint read two of them, and the suite now pins that they still ride the same line.user.box.schema— what it promises, and what it does notAn integer naming the stamp's shape, not the box version. It moves when a key is removed or repurposed, never when one is added: a reader that meets a key it does not know simply does not print it, so adding fields is not a breaking change and does not need a bump.
The forward-compatibility contract runs in both directions, and the backward one is the load-bearing half:
user.claudeboxtag already says out loud atresolve_box. Reads tolerate absence by construction:incus config geton an unset key prints empty and exits 0 (drill audit item B4), so "no stamp" and "the daemon said no" arrive identically and both must render as a box with blanks. A legacy box underbox infoprintsMINTED (not recorded — this box predates the mint stamp), skips every line whose key it lacks, and exits 0. A pre-renameuser.claudeboxbox still reads as the claude template, the same mappingbox_user()makes. Three tests pin exactly this, including an absence assertion that no half-emptyIMAGE/ORIGIN/RIG/MODEline is printed for a key that does not exist.NOTEsaying there is more — refusing to describe a box a later release minted perfectly well is the wrong answer. A non-integer lands on that same side rather than failing arithmetic underset -e.The clone decision, argued
This was the sharpest question in the issue and I did not want it to default.
incus copypreserves everyuser.*key (audit item B2,drill/RUNS.md:26,drill/drill.sh:454-464) — which is precisely what makes a clone know its template and user for free, with no code. It is also exactly why the stamp cannot ride along untouched. An inherited stamp does not go stale, it goes false: the clone would carry a mint time it was not present for, a box version that never saw it, andorigin=mintfor a mint that happened to a different instance.So the clone re-stamps, and the split is deliberate. Updated after review (@claude-bot-andresmgsl): the split was originally two columns and one key fell between them — see Cleared below.
Re-stamped — the keys that describe this instance's coming into being:
schema,version,created,origin=clone,origin.from=<srcref>. The clone was made here, now, by this box.Left alone — the lineage keys:
template,user,image,image.fingerprint,role,rig.repo,rig.ref,mode. The clone's disk genuinely did come from that image, converged by that role, from that rig. Re-deriving them from the cloning process's own template lookup would be the actual lie — and would break the login hint, which readsuser.box.templateoff the instance precisely so a clone of a claude box still gets it. (incus copypreserves the instance type too, somodestays true without help.)Cleared —
mode.asked, the one key that belongs to neither column. It is a mint-event fact by this PR's own criterion (only the mint knew whether a container was asked for or fallen back into), but the asker was the source's operator — and a clone refuses--vm/--containeroutright, so nobody was asked anything about this instance. Inheriting it madebox infoon a clone printMODE vm (asked: auto), a demand never made of it, with nothing marking it ancestral. There is no true value to re-stamp it with, so the cloneincus config unsets it (cleared, not set-to-empty — an empty value is still a key), alongside the re-stamp and before the start. The read side needs no special case: theMODEline is already gated onasked, so absence renders as silence, andTYPEstill reports VM or CT off the preserved instance type.Two consequences I am stating rather than leaving implicit:
origin.fromnames the immediate source. A clone of a clone names its parent and forgets its grandparent. The alternative is an unbounded chain inside a config value, and the parent is the box an operator can actually go look at.incus config setruns on the copied instance beforeincus start, so a clone is never observable — not for a second, not by a concurrentbox info— wearing its source's provenance. Line-order-asserted, fail-closed.The issue also notes
limits.*carry over stale on a clone (--cpu/--memory/--diskare refused for clones). That is the same class of problem and I did not fix it here: it is a real resources question, not a provenance one, and folding it in would smuggle a behaviour change into a stamp PR.The image: alias and fingerprint
Recording the alias is what #103 asks for, and the alias alone is not a reproducible fact. Incus resolves it during the launch and records the result in
volatile.base_image, so box reads it back and pins it in a secondincus config set—IMAGE images:debian/13/cloud @ 8a2f1c9d4e5b…answers "was this box built on the image that broke?" where a template name cannot.The issue's open question was whether that second call is worth one more failure point in the mint path's hottest section. It is not one, by construction: the call runs only after a launch that already succeeded, and every failure in it is silent. A box that exists and boots must never be failed over a provenance field — with no fingerprint to be had, the mint still succeeds and the alias stands alone as the honest partial answer, which is exactly how
cmd_inforenders it. Both halves are tested: the pin lands, and an unknowable fingerprint neither fails the mint nor stamps an empty key.Mint time: a timestamp, said out loud
A timestamp in a convergent config file is churn — the same run writing a different byte every time. A mint is not convergent: it happens once, to one instance, and is never re-run against it.
createdis UTC ISO 8601 so it sorts as a string and means the same thing on every host that reads it back; the test pins the shape, so a local-time or epoch-seconds spelling fails here rather than on a host.Deliberately not stamped
limits.cpuandlimits.memoryalready hold them. A duplicate drifts the first time someone edits a limit by hand.--diskis explicitly dropped with a note — the container root rides the pool). A stamped container disk would be fiction, which is worse than a missing key.box_tier()derives admin/restricted/none from liveid -nG. It describes whoever is asking, not the box, and stamping it records who happened to mint and then rots.All four are absence assertions in the suite, so a well-meant later addition has to argue here first.
What I deliberately left alone
cmd_import— an imported box keeps the artifact's mint stamp verbatim.cmd_import's own comment already draws the line: whatincus importrestores is the artifact's truth, what box re-stamps is this host's. Mint facts are the originating host's truth and should survive the trip; anorigin=importoverlay would overwriteoriginand destroy whether the thing was a mint or a clone before it was exported. That is a real decision with a real trade-off, and I did not want to smuggle it into this PR. Filing it as follow-up if reviewers want the import event recorded — it wants its own key, not a repurposed one.host/migrate-host.sh— it backfillsuser.box.template/user.box.userfor pre-tag boxes and now backfills nothing new. Fabricating acreatedor aversionfor a box whose mint nobody witnessed is precisely the invented-fact failure this PR exists to avoid; leaving the keys absent is what makesbox infosay "not recorded", which is true.box listcolumns — the issue floats aTEMPLATE/AGEcolumn.listis width-constrained and the per-box answer isinfo's job.user.box.disk, per above.Surfacing it
A stamp nothing can read is not done.
cmd_infoprintedNAME / STATE / TYPE / IPV4, exposures and snapshots, and surfaced none of theuser.box.*keys — including the two that already existed. It now prints a provenance block in the existingprintf '%-11s%s\n'style, above the snapshots:box info --jsonneeded no code at all: it passesincus list --format jsonthrough, and the keys ride along inconfig.One refactor, and why it is not optional
RIG_REPO/RIG_REFdefaults now live inrig_repo()/rig_ref(), read by bothrender_userdata(which substitutes them into the seed) and the stamp (which records them). Two spellings of the same default would eventually disagree, and a stamp that disagrees with the seed it shipped alongside is worse than no stamp. The suite drives the override end to end —RIG_REF=probe-refmust appear in both the stamped key and the rendered seed on the same launch — and the extraction that drivesrender_userdatain isolation now fails loudly if the helpers move, rather than silently rendering an empty repo and passing against nothing.VERSIONlikewise has one reader,box_version(), pinned by a test:box --versionand the mint stamp must never disagree about what minted a box.Also: a template whose seed carries no
@RIG_REPO@token (blank) is stamped no rig pin and no role. Naming a rig that was never installed would be fiction.Tests
New section in
test/cli.sh, driven against a fake incus that logs the arguments box builds — the only proof available with no daemon, since the argument list is the write. Both halves: write (mint, clone) and read (info). Every key gets its own check, because a single grep for the whole block goes green on a partial stamp and "which fact was dropped" is the useful failure.Each new assertion was proven to bite by breaking the thing it pins and observing RED, then reverting:
--config user.box.schema=mint: stamps the schema--config user.box.mode.asked=mint: stamps the mode that was ASKEDuser.box.template=to the clone re-stampclone: does NOT re-stamp the template(the absence assertion)config unset user.box.mode.askedclone: clears the inherited mode.askedand the clear-precedes-start ordering checkconfig set …mode.asked=insteadmode.askedchecks, incl.clears it rather than setting it emptymode.askedin the clone's canned configinfo: a clone prints no MODE line(exit 0, wanted 1 — the exactMODE vm (asked: auto)bug)MINTEDlinedieinstead of degradeinfochecksRIGline's emptiness guardinfo: surfaces which rig converged itand the no-half-empty-lines absence checkChecks
bash test/cli.sh— 529 passed, 0 failed (was 493 before this PR's additions)bash test/labels-reconcile.sh— 19 passed, 0 failedbash test/release.sh— 90 passed, 0 failedshellcheck -xover CI's exact globstar list (bin/* **/*.sh, 15 files) — clean.github/scripts/changelog-armed.sh— passesCHANGELOG.mdwas edited by insertion only —git diff -- CHANGELOG.mdis58 insertions(+), 0 deletions, and zero lines beginning-##(checked explicitly, per #122)On real-Incus proof — what CI's rehearsal does and does not settle. I could not run a mint locally (the box this was written in has no daemon), so every assertion in
test/cli.shis against a fake incus and proves only the arguments box builds. CI'srehearsaljob, however, runsdrill/multiuser.shon a real Incus, and it is green on this head. That job really doesbox new --name mine --template blank,box new --name c1 --from mine/s1, andbox info mine— so the real daemon accepted every new--config user.box.*key on the launch line (schema,version,image,mode,mode.asked,created,origin), accepted the clone's multi-keyincus config set, andbox inforendered without erroring against live config.What that still does not prove, stated plainly:
volatile.base_imagewere empty at the moment box reads it, the mint succeeds silently and identically — that is the design, and it is also why a green rehearsal cannot distinguish the two.blankonly. The rehearsal mints the one template with no rig seed, so theroleandrig.repo/rig.refstamps — and the "no rig token, no rig pin" branch's counterpart — have never run against a real daemon.Those three want a release-drill run with a
claudemint and an eyeball onbox info.Merge-order note: #104 touches the adjacent region
heavy-duty/box#104 (a
pristinesnapshot at mint) is in flight onfeat/pristine-snapshotand lands incmd_newnear the rig bootstrap hook — the same function, a few lines from this PR's launch block and post-launch fingerprint pin. Both branch fromorigin/mainand neither knows about the other, so whichever merges second should expect a textual conflict incmd_new. No semantic interaction that I can see: this PR writes instance config, #104 takes a snapshot, and the two do not read each other's state.Verdict: Approve — I agree with this as-is.
Tip
1907987turns the mint's discarded facts into a durable stamp on the only store that exists (Incus instance config) and surfaces it inbox info.What holds
volatile.base_imageis. A box that boots must not die over provenance — silent partial stamp is the honest answer.origin.fromis one hop.box_version()/rig_repo/rig_refare single definitions shared with seed substitution.Nits / optional
box info --jsoncorrectly leans onincus list --format jsonconfig passthrough rather than a second serializer.Verdict: I agree with everything and have no additional feedback.
The current head records mint facts at their authoritative write point, re-stamps clone provenance without corrupting lineage, tolerates legacy and future schemas, and surfaces the data without duplicating the JSON path. Current checks are green.
🔧 Reviewed — I agree with most; feedback below.
The core decisions are right and argued honestly: the stamp rides the single write point, the clone re-stamps event facts and inherits lineage facts, the fingerprint pin-back is best-effort strictly after a successful launch, and
box_provenancetolerates absence, pre-rename boxes, and future schemas (I checked the non-integer arm:! [ "$schema" -eq "$schema" ]short-circuits before-gtcan error, so it lands on the NOTE side underset -e). One key falls between the argued columns:user.box.mode.askedrides throughincus copyuntouched, but by this PR's own criterion it is a mint-event fact, not a disk fact — "only the mint knew whether a container was asked for or fallen back into", and the asker was the source's operator, not the clone's (mode flags are refused for clones). Result:box infoon a clone printsMODE vm (asked: auto)(bin/box:1426) describing a demand nobody made of this instance, with nothing marking it ancestral. The PR body's re-stamp/leave-alone split listsmodebut never placesmode.askedin either column. Two clean resolutions: unsetuser.box.mode.askedin the clone re-stamp (TYPE and the inheritedmodestay true via the preserved instance type, and the MODE line simply doesn't print — the same absence-renders-as-silence rule the rest of the block uses), or state explicitly that it is inherited-on-purpose and why. I'd take the unset; either way the split should name it.— automated review by
claude-bot-andresmgsl· heavy-duty-review-bot🔧 Changes requested — I agree with most; feedback below.
Formalizing my earlier 🔧 review of this same revision as a request-changes verdict (the specific points are in that review, unchanged) — reviews from this bot now carry a formal verdict state instead of a comment.
— automated review by
claude-bot-andresmgsl· heavy-duty-review-botRound answered whole at
86f01b4. One change: the clone now clearsuser.box.mode.asked. Verdicts per reviewer below, then the stack note.@claude-bot-andresmgsl —
mode.askedon a cloneFixed. You were right, and I took the option you'd have taken (the unset).
The split had two columns and this key belongs to neither. By the PR's own criterion it is a mint-event fact — only the mint knew whether a container was asked for or fallen back into — but the asker was the source's operator, and a clone refuses
--vm/--containeroutright, so nobody was asked anything about this instance. There is no true value to re-stamp it with. The honest answer is absence, which is already how the whole block renders what it doesn't know.Three details worth stating, because each is a decision:
askedits operator never gave. It has its own fail-closed ordering check, mirroringrestamp_precedes_start.The read side needed no code. The
MODEline was already gated onasked, so absence renders as silence whileTYPEstill reports VM or CT off the instance typeincus copypreserves — exactly the "absence renders as silence" rule you pointed at. The PR body's split now has a third Cleared column naming this key and why, and the CHANGELOG's clone entry says the same.Also confirming your reading of the non-integer arm —
! [ "$schema" -eq "$schema" ]short-circuiting before-gtunderset -eis deliberate, and it's pinned by a test. Thanks for actually checking that one rather than assuming it.+7 checks (529 → 536), each proven to bite:
config unsetclone: clears the inherited mode.asked+ the clear-precedes-start ordering check (2 RED)config set …mode.asked=insteadclears it rather than setting it empty(4 RED)mode.askedin the clone's canned configinfo: a clone prints no MODE line— exit 0, wanted 1, which is literally theMODE vm (asked: auto)bug you describedThat last one is the one I'd point at: the RED is the reported defect, reproduced as a test.
@grok-bot-andresmgsl — approve
Nothing to answer, but confirming I read it rather than just banking the approval. Your
--jsonnote is right and is the reason there's no second serializer: the keys rideincus list --format jsonconfig passthrough, so the JSON path cannot drift from the human one — it isn't a parallel implementation, it's the same data with no renderer in front of it. That property survives this round unchanged; the clear removes a key rather than adding a rendering rule, so--jsonneeds no update.@codex-bot-andresmgsl — approve
Acknowledged, nothing outstanding. Flagging only that the head you approved has moved by one commit — the
mode.askedclear above. It touches the clone branch ofcmd_newand no other behavior.The looseness item I flagged for attention — nobody pushed on it
I asked in the body whether the stamp reader had the same tolerate-several-shapes looseness the vendored OpenAPI repo-list reader has. No reviewer raised it, so I'm recording my own read rather than letting it pass silently:
box_cfgcollapses every failure into empty — "no stamp", "no such key", and "the daemon said no" arrive identically. That is deliberate and I'm not changing it, but the reason it's safe here and not safe in general is narrow: every consumer of a stamp key treats empty as absence, and absence has exactly one rendering (the line does not print, orMINTED (not recorded …)). There is no arm where an empty value is coerced into a default that would then read as a fact. The looseness is bounded because the only thing it can produce is silence, and silence is never a claim. If a future key ever wants a default, that's the moment this stops being safe — worth remembering rather than rediscovering.⚠️ Stack note — #133 (
feat/import-provenance)#133 is based directly on this branch's old tip (
1907987), so it needs a rebase onto86f01b4— as it would for any new commit here. I have not touched that branch.Shared surfaces changed: none of the ones that would hurt. Specifically unchanged: key names,
user.box.schemasemantics,origin/origin.from, andcmd_info's provenance block (box_provenancehas zero diff this round). The only change is one added line incmd_new's--frombranch, ~300 lines above anything #133 touches.Verified rather than assumed:
No semantic dependency either — #133's
IMPORTEDline inserts betweenMINTEDandTEMPLATE, above theMODEline, and nothing in its 544 test lines referencesmodeor the clone's canned config. So the rebase should be mechanical.Checks
test/cli.sh536 passed, 0 failed (from 529) ·test/labels-reconcile.sh19/0 ·test/release.sh90/0 ·shellcheck -xclean over CI's exact globstar list (15 files) ·changelog-armed.shexit 0 (VERSION 0.8.1-devagrees withUnreleased). CHANGELOG amended by insertion only —git diff -- CHANGELOG.md | grep '^-'is empty.What the rehearsal still does not prove. Unchanged from the body and worth repeating, because this round's fix sits partly inside the gap: the rehearsal mints
blank, so the role and rig-pin stamps have still never run against a real daemon, and the fingerprint pin-back is indistinguishable there from a silent no-op. The new clear is better off — the rehearsal really doesbox new --from, and theblanksource really does carrymode.asked, so theincus config unsetwill execute against live Incus rather than a shim. But that is a claim about the next CI run, not evidence I hold yet; I'll say so plainly if it comes back red. Everything intest/cli.shproves only the arguments box builds.✅ Approved — I agree with all of this, no concerns.
My one point from
1907987is resolved at86f01b4, and with the option I'd have taken: the clone nowconfig unsetsuser.box.mode.asked— cleared rather than set-to-empty (an empty value is still a key), failure-tolerated for pre-stamp sources, and landing beforeincus startwith its own fail-closed ordering check mirroringrestamp_precedes_start. The read side needing no code is the design paying off: theMODEline was already gated onasked, so absence renders as silence whileTYPEstill reports off the preserved instance type. Theinfo: a clone prints no MODE linetest reproducing the exact reported symptom (exit 0, wanted 1 →MODE vm (asked: auto)) is the right RED, and the PR body's third Cleared column names the key and the reason, which was the other half of the ask.— automated review by
claude-bot-andresmgsl· heavy-duty-review-botRebased onto
main(239428b) and re-requesting the panel — the force-push staled the round you approved.Two things changed, and the second is the one worth your attention.
1.
CHANGELOG.md— positional conflict. Both sides wrote under## Unreleased.mainhas since grown### Changedand### Fixedthere, so this entry's### Addednow leads the section, matching the ordering every released heading in the file uses (Addedfirst, 0.5.0 through 0.8.0). No prose altered on either side;changelog-monotonic.shconfirms all 4 merge-base headings survive.2.
test/cli.sh— a semantic conflict git had no way to flag. #123 renamed the tenant templates (claude→claude-box, following heavy-duty/rig#76) while this branch was open. The mint drive here was written against the old name. The two changes never touch the same line, so the merge was textually clean and behaviourally broken:box new --template claudedies at the template lookup, and all 18 mint assertions failed on a shimmed mint that never reached a launch line. Fixed in its own commit (c3823e9) rather than folded into the rebase, so the adaptation is reviewable on its own.What I deliberately did not rename, both of which are load-bearing:
user.box.user=claude— #123 split these two namespaces on purpose: the template and the rig role areclaude-box, the user that role converges staysclaude. That assertion is what pins the split, so renaming it would have deleted the coverage and gone green.$STAMPED/$PRERENAMEinfo fixtures — they stand for boxes minted before the rename, which is the casecmd_newexplicitly promises to keep reading (it matches both spellings ofuser.box.templatefor the login hint). They are legacy fixtures on purpose.Local, mirroring CI:
shellcheck -xclean overbin/* **/*.sh,test/cli.sh546/546,test/labels-reconcile.sh72/72,test/release.sh134/134, changelog armed + monotonic green.Note for #133, which is stacked here: this rebase moved the base, and the mint drive it extends now mints
claude-box. I will rebase it once this lands.Verdict: I agree with everything and have no additional feedback.
The rebased head is acceptable and all current check runs pass. Non-blocking documentation follow-up: the new README provenance example still shows
TEMPLATE claude (user claude, role claude), but current mints now record template and role asclaude-box; please update that example when convenient so it matches the rename this head already adapted in the tests.✅ Approved — I agree with all of this, no concerns.
Re-approving at
c3823e9. Everything I approved at86f01b4— the stamp riding the single write point, the clone re-stamp, themode.askedclear with its fail-closed before-start ordering — came through the rebase unchanged, so I reviewed the delta as a delta:CHANGELOG.md:### Addedleading## Unreleasedmatches the ordering every released section uses; no prose changed.c3823e9is exactly the four lines the semantic conflict demanded — the two mint-drive invocations and the two assertions naming the renamed template/role — isolated in its own commit, which is how a textually-clean-but-behaviourally-broken merge should be fixed. The two deliberate non-renames are the load-bearing half and both are right:user.box.user=claude(test/cli.sh:1173) is the assertion that pins #123's template/user namespace split — renaming it would have deleted that coverage and gone green — and the$STAMPED/$PRERENAMEfixtures model pre-rename boxes, precisely the legacy casebox_provenancepromises to keep reading.I agree with codex that the README provenance example (
TEMPLATE claude (user claude, role claude)under abox 0.8.1mint) is now a pre-rename output shown as a current one — worth a follow-up, not a blocker.— automated review by
claude-bot-andresmgsl· heavy-duty-review-botVerdict: Approve — I agree with this as-is.
Tip
c3823e9(rebase onto main + mint-drive rename) re-forms cleanly from the new tip.What holds
user.box.*on the single launch line (schema/version/image/mode/mode.asked/created/origin + optional role/rig); fingerprint is best-effort after launch so a bootable box never fails over provenance.schema/version/created/origin=clone/origin.fromre-stamped before start; template/user/image/role/rig/mode left alone (disk truth).mode.askedcleared (neither column) sobox infodoes not print a demand never made of this instance.user.claudeboxstill maps to claude for the login hint.c3823e9). Mint drive correctly usesclaude-boxfor template/role;user.box.user=claudeand legacy$STAMPED/$PRERENAMEfixtures stay as the split and pre-rename contract. CHANGELOG### Addedleads Unreleased after the positional merge.CI green (check / reconcile / rehearsal / scope). No blockers.
Round summary — handing off to @danmt
All three bots approved on head
c3823e9, every verdict after the head commit:codex-bot-andresmgslclaude-bot-andresmgslgrok-bot-andresmgslNo changes requested, nothing left unaddressed. What changed since the round you last saw is the rebase, detailed above — and one part of it is worth your eye at merge time rather than mine:
The rebase was textually clean and behaviourally broken. #123 renamed the tenant templates (
claude→claude-box) while this branch was open. The rename and this PR's mint drive live in different files, so git had nothing to flag — butbox new --template claudenow dies at the template lookup, and all 18 mint assertions were failing on a shimmed mint that never reached a launch line. That is the whole reasonc3823e9exists as its own commit; it is an adaptation tomain, not part of the feature.Two things it deliberately leaves on the old spelling, both load-bearing and both easy to "tidy" wrongly later:
user.box.user=claude— #123 split the namespaces on purpose: template and rig role areclaude-box, the user that role converges staysclaude. That assertion is what pins the split; renaming it would delete the coverage and still go green.$STAMPED/$PRERENAMEinfo fixtures — they stand for boxes minted before the rename, whichcmd_newexplicitly promises to keep reading (it matches both spellings ofuser.box.templatefor the login hint).Green locally, mirroring CI:
shellcheck -xoverbin/* **/*.sh,test/cli.sh546/546,test/labels-reconcile.sh72/72,test/release.sh134/134, changelog armed + monotonic.Setting
merge-next: #133 is stacked here and unblocks the moment this lands. #127 is also ready for you and independent of this one — they touch different files and will only meet inCHANGELOG.md, so whichever you merge second takes a positional changelog rebase that I will handle.