On a bare X.Y.Z tag push (the 0.6.0 tag set the no-'v' precedent),
release.yml asserts the tag names the tree's own VERSION — a mismatch
fails loudly and creates NOTHING — then publishes the GitHub release
with that version's CHANGELOG.md section as the body: the curated
prose, not the generated PR list. No assets, because for a pure-bash
tree GitHub's source tarball for the tag IS the package.
The extraction lives in .github/scripts/release-notes.sh, a file of
its own so test/release.sh drives the same code against fixtures and
the real changelog — it refuses a missing or empty section, so a tag
whose release ritual was skipped fails before anything is created.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-1 blockers, all three reviewers concurring:
- COMMENTED agreement now counts: agreement_signal recognizes the live bots'
durable markers (Verdict: Approve / I agree with everything / leading ✅) —
the gate to needs-human can actually close. Formal verdicts remain the
contract (CONTRIBUTING), this is the documented transitional workaround.
- Every counting verdict is bound to the head SHA; a stale approval parks the
PR in addressing (agent owes re-request) instead of promoting unreviewed
code. CHANGES_REQUESTED blocks at any head, per GitHub's own semantic.
- reconcile serializes under ONE job-level concurrency group; scope stays
per-PR. No more cron-vs-event race on the request-the-human-once guard.
- Sweep resilience: per-PR subshell (one failure logs and continues), label
edits warn instead of wedging; the self-heal claim now matches reality
(dispatch-only bootstrap).
- The state machine is extracted pure (globals in, state out) and sourceable:
test/labels-reconcile.sh proves 14 fixture transitions — comment-only
agreement, stale approval, comment-without-verdict, human precedence and
human-block — wired into CI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'box rm' deletes a box and every snapshot it has; 'box new --from' clones,
but the clone still lives on the same host. Nothing a box held could outlive
a teardown — which made #66's upgrade refusal honest but lossy. This adds
the way out and the way back:
- box export <box> [<file>] [--instance-only]: wraps 'incus export' into one
portable backup tarball (default <box>-<UTC stamp>.tar.gz), snapshots
included by default. Requires the box stopped (require_stopped grew an
honest reason parameter: export is down by OUR decision, not incus's).
Credentials are SHOUTED, not scrubbed — the artifact carries the box's
whole disk, and scrubbing a disk image is a promise tarball surgery
cannot keep.
- box import <file> [--name <box>]: reads the artifact's name from
backup/index.yaml up front, refuses any name an existing instance holds
(the resolve_box boundary from the other side), pre-flights the stack
(require_stack, factored out of cmd_new), imports, then re-stamps the
HOST's truth onto the artifact's: user.box=1 (legacy tag honored), the
box-net placement (profile assign, the migrate-host move), fresh volatile
MACs (imports restore volatile.* verbatim — a re-import beside its
sibling collided at start with 'MAC address already defined on another
NIC', measured live on Incus 6.0.4), and reset_identity, exactly like a
clone.
- restricted tier: box grant now converges restricted.backups allow —
export rides the backup API, which incus-user's restricted projects block
by default exactly like snapshots (incus 6.0 permissions.go,
AllowBackupCreation). Import is plain instance creation and needs no key.
- tests: driven usage errors + fail-closed grep/line-order guards for every
daemon-gated invariant; CI's rehearsal job now runs a live round-trip
(mint, write, snapshot, down, export, rm, import, assert the file, the
snapshot, the tag, the agent, and the collision refusal).
The whole flow was verified against a live Incus 6.0.4 daemon: running-box
refusal, export, overwrite guard, rm, import with and without --name,
re-home onto box-net, sibling re-import with distinct MACs and machine-ids,
pre-export file and snapshot present in both.
Closes#70
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The machinery LABELS.md promised. labels.yml runs the reconciler on a
15-minute cron plus PR events (pull_request_target — every PR here is from a
fork, where pull_request gets a read-only token; no PR code is ever checked
out). The script derives each open PR's state:* from GitHub's own facts and
converges labels statelessly; stale is judged from real activity (commits,
comments, reviews), never label churn, so the sweep cannot un-stale its own
mark. actions/labeler applies scope:* from changed paths. CONTRIBUTING.md is
the guideline: the PR loop, and who sets which labels. Rehearsed with
DRY_RUN=1 against the live repo; shellcheck-clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-1 convergence from all three reviewers, both findings real:
A version string used to be a path fragment: 'box uninstall
../../../.ssh' resolved below versions/ and rm -rf'd wherever it
landed, 'box use' could point current outside the root, and a hostile
flat-tree VERSION could steer the migration's mv the same way. One
strict validator now gates every caller — only [A-Za-z0-9._+-], no
leading '.' or '-' — byte-identical in install.sh and bin/box like
existing_boxes, diff-guarded in the tests, with traversal regressions
on use, uninstall and the migration (which now refuses BEFORE the tree
moves anywhere).
--force is uninstall's installer-family consent, and --purge-host now
forwards it: teardown-host.sh gets --yes under --force/BOX_YES, so the
combined non-interactive uninstall no longer dies at teardown's own
prompt. CI's drill now runs the combined verb with --force alone (no
BOX_YES, no TTY) — the exact invocation that used to abort.
Also grok's polish, taken: current flips by rename (ln to a side name,
mv -Tf over — no window with no current) in both install.sh and 'box
use'; BOX_REINSTALL swaps by two renames and deletes LAST; and the
single-version path refuses while current is dangling (readlink -f
resolves a missing last component, so the guard checks the DIRECTORY,
not just the string).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test/cli.sh drives REAL installer runs via BOX_INSTALL_SOURCE (temp
BOX_HOME/BOX_BIN, a fabricated second version for upgrades): fresh install
lands versioned, --version answers through the chain, side-by-side installs,
same-version no-op, BOX_REINSTALL, the 0.6.0 flat-tree migration, stale-
symlink healing, single-version and full uninstalls asserted to zero residue
(a planted survivor makes it scream INCOMPLETE), working-tree refusals, and
byte-identity of the existing_boxes copies. The rehearsal job now installs
via install.sh itself — CI proves the installer under review — and ends with
the uninstall drill: revoke --purge, teardown --yes, box uninstall, then
assert nothing is left (networks, profiles, nft, units, files, symlinks).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
drill/multiuser.sh: two throwaway users, real grants, real boxes, and the
contract measured from INSIDE them — #74's criteria (a)-(f) plus what the
findings added: the cross-user sibling drop, the closed private-bridge
escape hatches, incus-user re-sync survival, scoped revoke --purge. Its own
first runs are in RUNS.md (MU-1..3): both false FAILs were the instrument,
and both lessons (keep the mint's narration; probe a SIBLING's name, never
your own — /etc/hosts answers for you) are now comments in the script.
test/cli.sh: box_tier() driven against a shim id for all five cases
(including both-groups → admin: membership wins at the socket), the two
copies diffed byte-identical, the tier scripts' usage contracts driven for
real, and grep-guards on every load-bearing line a daemon-free run cannot
exercise — the expose guard's position, the boxnet-and-only-boxnet
narrowing, the snapshot allow, 'incus:snapshot restore'.
ci.yml: a second job stands up the real stack on the runner (setup-host,
doctor, then the rehearsal in container mode) — every PR now proves the
tier's semantics against a live daemon. The tier's mechanics are
instance-type-independent (the drop, the ACL, dns.mode and port_isolation
bind to boxnet, not the instance); the VM trust boundary itself stays a
real-hardware ritual, like the full drill.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
box had no CI and no unit tests — only the live-host drill. Mirror rig's CI:
one `check` job = globstar `shellcheck -x` over bin/* and **/*.sh, then
`bash test/cli.sh`. The suite is dependency-free and runs non-root with no
Incus: the full CLI contract; install.sh's DEST/BINDIR branch driven
functionally against a shim `id` (both tiers + the BOX_HOME/BOX_BIN overrides);
the root-only a+rX and #66's confirm/no-op flow grep-guarded; tmux asserted in
every template. Pre-existing repo shellcheck findings (bin/box SC2034/SC2015/
SC2020, and file-level SC2015 idioms in doctor.sh/wipe.sh/migrate-host.sh) were
resolved — real fixes where behaviour allows, reasoned disables otherwise — so
the new CI is green over the whole repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>