Commit graph

166 commits

Author SHA1 Message Date
dan-claude-bot
0ff520c850 fix: dropping the box role revokes through box, not behind its back
`users apply` converged group `incus` with a bare `gpasswd -d`, the same
move it makes for `rig-admin` and `rig`. Those two are rig's. `incus` is
box's, and `box revoke` does strictly more with it: it says out loud that
supplementary groups are read AT LOGIN, so a session the dropped operator
already holds keeps the Incus socket until that session dies, and it hands
over `loginctl terminate-user <user>` as the remedy.

rig logged "removed <user> from incus" and moved on. An operator who
dropped someone from the users file and watched apply succeed believed the
VM access was gone — and was wrong for as long as that user held a session.

Both removal paths — the per-user convergence loop and the dropped-user
sweep — now route the incus group through one `drop_incus` helper that
calls `box revoke`, keeping a single owner for the group. Never `--purge`:
that deletes the user's boxes, images and project, and destroying someone's
running machines is not a convergence step; it stays an explicit admin act.

The exit code is not trusted (the #12 lesson bootstrap already applies to
box's installer): a revoke that returns 0 with the membership still
standing has not closed the socket, so the effective state is checked and
rig falls back to removing the group itself — as it also does on a host
where box is not installed. Every fallback path carries the session warning
in rig's own voice, because the silence was the bug. The absent-group case
needs no new guard: `id -nG` cannot report a group that does not exist, so
the existing `in_group` test at both call sites is already false on a
host=no box or one where `box setup-host` never ran.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:04:22 +00:00
Daniel Marin
67386b4303
Merge pull request #60 from dan-claude-bot/fix/host-no-bare-socket
fix(users): the host= marker gates the box role, not the incus group
2026-07-19 20:02:12 +01:00
github-actions[bot]
5a30c72eae chore: bump main to 0.1.1-dev — a dev install must not impersonate 0.1.0 2026-07-19 18:03:46 +00:00
Daniel Marin
6ff90f04ea
Merge pull request #46 from dan-claude-bot/release/0.1.0
release: 0.1.0
2026-07-19 19:03:35 +01:00
dan-claude-bot
49471bde4f docs: README's users-apply section matches the new gate (#60 review)
#58 inverted the section's central claim — the host= trait now decides in
both directions and the incus group never overrides it — but README still
read "when the `incus` group is absent, the `host=` trait decides". That
qualifier is precisely the bypass the change removes, so the operator
reference asserted the bug as the contract.

It also omitted the behavior an operator most needs to know before running
apply on a repurposed box: on a host=no box carrying a stray incus group,
apply warns about the marker/reality mismatch and STRIPS box-role users out
of the group. Discovering that from a diff of your own fleet is the wrong
way to learn it.

Rewritten so the trait gates the role, the group only distinguishes
ready-vs-die once the trait already said yes, and the mismatch names both
its hazard and `rig bootstrap --host yes` as the repair. Pinned in both
directions — current sentences present, superseded one absent — following
the same grep-the-prose-stays-honest discipline the file already uses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:50:24 +00:00
dan-claude-bot
7614c1c171 docs: the 0.1.0 notes carry #39/#41 — the getent-$HOME installer fix shipped without its entry
Both reviewers caught it: #41 merged after the changelog convention
landed (#40) and skipped its entry; the release PR is the last gate
before the section becomes the permanent release body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:32:56 +00:00
dan-claude-bot
a702f1f8be release: 0.1.0
rig's first release: VERSION leaves 0.1.0-dev and the Unreleased section
is stamped with today's date. The surface was drilled twice on 2026-07-19
(offline suites, tenant guests on real incus via box, db round-trip, a
real GitHub runner lifecycle, coolify install) and the ceremony itself
rehearsed end to end on a scratch fork — including the guard that made
this very PR possible (#44).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:32:56 +00:00
Claude
b8e8e79b87 fix(users): the host= marker gates the box role, not the incus group
users apply consulted the host= trait only when group incus was ABSENT,
so a host=no or marker-less box that nonetheless carried the group handed
box-role users a bare `usermod -aG incus` — the socket with no tier, which
incus-user answers by lazily building an unhardened project under whoever
opens it.

The marker now decides in both directions through one pure gate,
assert_marker_hosts_vms, so the verdict is identical whether or not the
group exists. The marker wins over the machine deliberately — it is the
box's declared identity and every other host= decision already treats it
as authoritative — but not silently: when the group exists and the trait
disagrees, the skip names the contradiction and rig bootstrap as the fix.

Closes #58

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:29:20 +00:00
Daniel Marin
5902f61da4
Merge pull request #48 from dan-claude-bot/feat/release-on-merge
feat: merging a release-labeled PR is the release
2026-07-19 18:12:09 +01:00
dan-claude-bot
e533e86322 fix: issues: write — the fallback PR's --label rides the issues API (labels.yml precedent)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:35:50 +00:00
dan-claude-bot
0b6318fe24 chore: the permissions comment must not pre-match the ordering pin's grep
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:34:56 +00:00
dan-claude-bot
700e2185eb fix: pull-requests scope for the door's two PR-API calls; docs catch up
The cast-twin blocker (claude-bot): a declared permissions: block zeroes
unspecified scopes, so the label read and the bump fallback's gh pr
create could only 403 — every genuine ceremony would end red at the
label check. pull-requests: write added, consumers named. CONTRIBUTING
step 3 and the changelog entry now tell the shipped story: push-to-main
door, event.before interlock, self-re-arm, manual-path bump stays the
operator's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:34:29 +00:00
dan-claude-bot
0dbcad2e3a test: pin exactly one on.push key
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:14:42 +00:00
dan-claude-bot
4b5d62e405 fix: one on.push key — YAML last-key-wins had dropped the tag door
grok's round-2 catch: two sibling push: maps under on: leave only the
second (branches: [main]) alive — the tag-push fallback stopped
triggering entirely. Both filters now live under one push key, the jobs
still split on the pushed ref, and a pin counts exactly one on.push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:14:13 +00:00
dan-claude-bot
053e12d51e feat: the release re-arms main — the -dev bump folds into the release act
Operator decision: the post-release bump PR is ceremony debris — a
derivable one-liner with no judgment for a review to add. After tag +
publish, the same job computes X.Y.(Z+1)-dev and pushes it to main
directly (a GITHUB_TOKEN push fires no workflows, so no recursion and no
red run); if branch protection refuses, the step opens the bump PR
itself, loudly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:02:53 +00:00
dan-claude-bot
44717863a1 fix: the merge door rides pushes to main — fork PR tokens are read-only
Round-1 blocker (grok, claude on box#97's twin): a pull_request run from
a public fork gets a read-only GITHUB_TOKEN — permissions: cannot raise
it — and every ceremony PR this org merges is cross-repo from the bot
fork, so the tag create would 403 after green asserts, red on main per
release. The door now triggers on push to main (in-repo event, full
token): the decide step reads the version transition from event.before
(first-parent fallback for the all-zeros edge), and the release label —
still the operator's declared intent — is read via the API off the merge
commit's PR. A transition with no labeled PR behind it refuses. The two
doors now split on the pushed ref: tags to the tag door, main to this one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:44:52 +00:00
dan-claude-bot
c0cd4a1b15 fix: a -dev endstate is always work — the post-release bump must not run red
The four-state table called '-dev but changed' half a ceremony and
refused — but that state IS the mandatory post-release bump PR
(bare -> X.Y.(Z+1)-dev after every release), a red run on main once per
release, forever. A tree that ends -dev is by definition not a release:
every such merge is work, green NOTICE no-op. Red now guards only bare
endstates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:42:27 +00:00
dan-claude-bot
d08ec8c6f6 fix: the release label's two meanings part ways in a decide step
LABELS.md gives 'release' to release-flow WORK as well as to the ceremony
PR — including every PR that improves this very workflow. The old assert
pair turned each of those merges into a red run on main. The fused decide
step reads the version against the PR base and answers all four states:
-dev unchanged = work, green NOTICE no-op; bare unchanged but already
released = work in the post-release window, same no-op; -dev-but-changed
and bare-unchanged-never-released = half-ceremonies, refused loudly;
bare-and-changed = the ceremony. Later steps gate on its output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:23:56 +00:00
dan-claude-bot
4ce1ab50aa feat: merging a release-labeled PR is the release (#47)
The rig twin of heavy-duty/box#96, from the release-ceremony retro: the
tag was a separate, manual, silent-when-forgotten step, and a forgotten
tag produces no red X — the worst failure shape. The ship decision
already lives in the release PR; merging it is "ship". After that,
tagging is transcription, and transcription belongs to machines.

release.yml now also fires on pull_request closed into main, gated on
merged AND the `release` label. The job asserts in order, each fail-loud
and creating nothing: VERSION at the merge commit is non--dev; VERSION
changed in THIS PR (base vs merge — the interlock that fails a
mislabeled ordinary PR); the changelog section for that version extracts
non-empty via the existing changelog_section from release-lib.sh; and no
tag or release exists yet. Then, in the same job, it API-creates the tag
at the merge commit and publishes the release with the extracted notes.
Same-job is load-bearing: a GITHUB_TOKEN-created tag does not fire the
tag-push trigger, so the publish must live next to the tag and the
fallback job cannot double-publish; the nothing-exists assert covers a
manual race. The tag-push path survives verbatim as the documented
manual fallback and backfill, and CONTRIBUTING's Releasing section now
reads merge-is-ship with the manual tag as fallback.

test/release.sh pins the merge path in the house grep-pin style: the
merged+labeled gate, the four asserts, the same-job tag+publish (awk
from release-on-merge: to EOF), the asserts-precede-the-tag ordering,
and the surviving tag-push trigger.

Fixes #47

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:19:55 +00:00
Daniel Marin
219b0fbd72
Merge pull request #45 from dan-claude-bot/fix/release-suite-stamped-tree
fix: the release suite accepts the ceremony's own tree
2026-07-19 15:22:15 +01:00
dan-claude-bot
589854ae0b fix: the release suite accepts the ceremony's own tree (#44)
test/release.sh demanded a literal '## Unreleased' heading extracting
non-empty with '#32' in it — all false by construction on the very tree
the release PR produces, so the first real 'release: 0.1.0' PR turned CI
red and the ceremony blocked itself. Both fork rehearsals missed it: they
tag a branch, which runs release.yml and never ci.yml. The guard now
asserts its actual purpose — the TOP section, whatever its name, extracts
non-empty via the exact function release.yml runs — and passes on both
legitimate tree states (verified on main's shape and on a stamped copy).

Fixes #44

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:48:41 +00:00
Daniel Marin
525e24acd6
Merge pull request #43 from dan-claude-bot/fix/silent-prompts
fix: headless credential prompts refuse loudly, naming their variable
2026-07-19 14:00:48 +01:00
dan-claude-bot
e72663ef62 fix: headless credential prompts refuse loudly, naming their variable (#42)
A bare 'read -rsp' with no tty exits non-zero and set -e ends the script
with no output at all: the release drill watched 'rig runner remove' exit 1
in complete silence, and a guest bootstrap stop mid-log the same way. Every
prompt now checks for a tty first and dies naming the variable that
unblocks an unattended run; every read is || die-guarded so EOF at a real
prompt also gets a last word. The no-bare-read test swept up runner
repoint's two prompts, which the issue had not counted.

Fixes #42

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:15:08 +00:00
Daniel Marin
fb0c3067e3
Merge pull request #41 from dan-claude-bot/fix/home-unset
install: derive $HOME from getent when the environment has none (#39)
2026-07-19 00:53:21 +01:00
dan-claude-bot
229fcd2bf0 install: derive $HOME from getent when the environment has none (#39)
cloud-init's runcmd runs the installer with no $HOME, and under set -u
the first expansion died with an unbound-variable stack instead of an
install — found live by box#88's seed, which pins HOME=/root as its own
scar. Derive the home from getent for the effective user (root included)
before any path comes from $HOME; when getent has no answer either,
refuse by name instead of a bash stack. Driven with a shim getent: the
derived-home install lands, and the no-answer refusal is pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:36:05 +00:00
Daniel Marin
a3c69292a1
Merge pull request #40 from dan-claude-bot/feat/release-flow
feat: release flow — CHANGELOG, release.yml, and a tag-resolving installer (#32)
2026-07-18 23:17:12 +01:00
dan-claude-bot
834f0504d6 feat(install): default to the latest release — three channels, one installer (#32)
RIG_REF unset now means the latest RELEASE: the tag is resolved by
following the releases/latest redirect and reading the Location header
(no API, no token), and the download is archive/refs/tags/<tag>.tar.gz.
An explicit RIG_REF tries refs/tags first — a pin must outrank a branch
that shares its name — and falls back to refs/heads, which keeps
RIG_REF=main the dev channel. "Re-run any time to upgrade" now means
upgrade to the latest release.

Transitional, and loud about it: rig has no release until 0.1.0 is cut,
so the default channel dies naming RIG_REF=main as the way to install
today — never a silent fallback to main, because "I installed the latest
release" must not quietly mean "I installed whatever main was that
second". test/release.sh drives all three channels through the real
install.sh with a stubbed curl (no network in CI), and the resolve/
candidate functions are extracted and exercised the valid_version way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:57:13 +00:00
dan-claude-bot
b6205d81c5 feat(release): CHANGELOG.md and release.yml — a tag becomes the release it claims (#32)
The PR-then-tag half of box#83's flow, rig's side. CHANGELOG.md starts
with an Unreleased section (feature PRs land their entry as part of the
PR — box's convention, now written into CONTRIBUTING alongside the
release ritual). On a tag push, release.yml asserts the bare tag equals
the tree's own VERSION — a mismatch fails loudly and creates nothing —
then creates the GitHub release with that version's changelog section as
the body, extracted by changelog_section in .github/scripts/release-lib.sh:
one function, sourced by the workflow and driven by test/release.sh
against fixtures and the shipped CHANGELOG.md itself. No assets — for a
pure-bash tree, the tag's source tarball IS the package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:57:13 +00:00
Daniel Marin
e6b630b9f2
Merge pull request #37 from dan-claude-bot/feat/bootstrap-roles
feat(bootstrap): box tenant roles — claude, codex, grok, staging (#31)
2026-07-18 21:32:13 +01:00
Daniel Marin
7e57c10f01
Merge pull request #38 from dan-claude-bot/fix/human-handoff-guard
fix: base the human auto-request on this handoff, not review history
2026-07-18 21:31:51 +01:00
dan-claude-bot
1cef6ed751 review r1: staging tolerates only the workload guest; dockerd must answer; one CLI capture
- The staging marker tolerance now says what the docs meant: class=server
  with host=no only. A non-server machine (class=human via custom) refuses
  with its own message instead of dying later inside harden_sshd with
  server-specific advice. Fixture pins the refusal.
- The docker converge asserts the DAEMON answers (docker info, bounded
  30s settle), not just the client binary — grep-pinned.
- The agent-CLI version check is one capture serving assert and log;
  emptiness is the failure signal (head exits 0, a pipeline status can't be).
- Harness gains the codex login-flow context grep alongside claude/grok.

Verified: test/cli.sh 244/0, shellcheck -x clean, live container e2e
(staging round 1 + convergence round 2, dockerd answering).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:18:15 +00:00
Daniel Marin
ad33ffe844
Merge pull request #36 from dan-claude-bot/feat/versioned-installs
feat(install): versioned installs and a real uninstall — box#79's layout, ported (#35)
2026-07-18 21:18:04 +01:00
dan-claude-bot
ec7096c08e fix: base the human auto-request on THIS handoff, not review history
codex's late #85/#98 round-3 finding, valid post-merge: the needs-human
auto-request fired only when the human had NEVER reviewed, so any earlier
human comment or stale approval left a fully-approved PR labeled
needs-human with nobody actually requested — a wedged handoff.
human_request_needed() now asks whether a fresh head-current human review
is missing (live request or head-current approval → nothing to ask;
anything else → request). Five new fixtures cover the wedge, the stale
approval, the satisfied handoff, and request suppression (19 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:16:57 +00:00
Daniel Marin
0ead75428b
Merge pull request #34 from dan-claude-bot/feat/label-automation
feat: label automation — state reconciler, path-scoped labeler, and CONTRIBUTING
2026-07-18 21:02:56 +01:00
dan-claude-bot
03e5deca56 refactor: the author escalates — drop the agreement-body heuristic
Maintainer direction: body-parsing agreement was a guess, and the machine
must not guess. COMMENTED is now unconditionally a non-verdict; the judgment
that a comment-only reviewer's round passed belongs to the PR AUTHOR, who
escalates by requesting the human's review — an explicit request is a fact,
and it is the machine's top-precedence input. Auto-request survives only for
the no-judgment case: three formal head-current approvals. CONTRIBUTING and
LABELS.md state the handoff; fixtures updated (14 transitions, including
author-escalation and the three-formal-approvals path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:51:53 +00:00
dan-claude-bot
e40e223ecb docs: README — the box tenants section, and where the staging preset went
The layering story in the README's own voice: thin seeds on the box side,
tenant roles here; the per-tenant table; the creds-free vs operator-run
split; the box#80 guard living once in the renderer; the tenant marker and
its guards; and the RIG_REPO/RIG_REF pin-point note — the seed's rig install
tracks main unpinned, documented with the same honesty as the box unpinned
note (the inverted edge, rig#32 is the release flow that gives it something
to pin to). The machine-role section loses the staging row and gains the
"where the preset went" note: the VM-host shape is dev --class server or
custom with all three traits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:49:27 +00:00
dan-claude-bot
739db6e633 feat(bootstrap): box tenant roles — claude, codex, grok, staging (#31)
box templates collapse to thin, creds-free seeds (box#81); everything a
tenant machine BECOMES moves here, as convergent, re-runnable roles with
effective-state asserts. One mechanism (bootstrap-tenant.sh) parameterized
per tenant through a pure lib (tenant-config.sh) — never four copies —
dispatched from bootstrap.sh so 'rig bootstrap <role>' stays the single
entrypoint.

The agent tenants land the toolbelt (git, gh, tmux, …), docker, the agent's
CLI on the SYSTEM path (box exec shells read no rc files, #15), and the
agent-context file — rendered from ONE shared template that carries the
box#80 guard note once: never run box setup-host or the drill inside a box;
the box you are in is not a host you own. staging lands box#69's server
posture — docker + sshd hardening — through lib/sshd.sh, extracted verbatim
from bootstrap.sh so both families converge ONE drop-in with one converger;
its tailnet workload join stays operator-run, exactly the creds split #69
designed. Everything is asserted on effective state: the CLI must ANSWER as
the tenant user (the grok template's linked-but-cannot-run scar), docker
must answer, sshd -T must resolve.

'staging' therefore moves from the VM-host preset to the tenant role — the
thing box#81's seed will auto-run. The host shape lost nothing: it is
'dev --class server' (or custom with all three traits), the catch-all
effective-tag refusal still owns its tag policy, and a pre-#31 staging host
re-running its old command gets a loud refusal naming the new spelling —
tenants refuse host=yes boxes, agents refuse any machine-role box, staging
tolerates the workload-joined guest and leaves its marker alone.

Harness: the arg/refusal surface, the marker guards off fixture markers,
the pure parameter table, the rendered context file (guard included, all
three agents), creds-free-by-absence greps (no tailscale, no prompt), the
CLI-verified-not-trusted pin, marker-after-converge ordering, and the
re-pointed sshd-lib pins. 241 passed, 0 failed; shellcheck -x clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:49:20 +00:00
dan-claude-bot
f1ada543dc feat(install): versioned installs and a real uninstall — box#79's layout, ported
install.sh now lands every version at <root>/versions/<v> (each tree
carrying its own VERSION + INSTALLED_FROM), tracks the default through an
atomically-flipped 'current' symlink, and converges instead of clobbering:
a same-version re-run is a no-op that says so, RIG_REINSTALL=1 replaces
that version's tree by two renames (delete last), and a new version
installs side by side. A pre-versioning flat tree is migrated in place —
two renames, preserved bit for bit, VERSION-less trees as 0.0.0-unknown.

bin/rig grows the table verbs: 'rig versions' (current + running marked),
'rig use <v>' (atomic flip, asserted effective through the PATH chain),
'rig uninstall [<v>|--all]' — which ENDS with an absence assert: every
removed path re-checked, survivors exit 1 as 'uninstall INCOMPLETE' by
name. One strict valid_version gate guards every place a version string
becomes a path (byte-identical copies in bin/rig and install.sh, diffed by
the suite so they cannot drift). Plus the VERSION file and 'rig --version'
(rig#32's first item, folded in minimally — rig main had neither).

The flip gate is rig's own shape, deliberately: box refuses flips under
existing boxes; rig's stake is the converged host, so a flip (upgrade,
'rig use', full uninstall) on a host where /etc/rig/role exists WARNS and
proceeds — no user state to strand, and upgrading a bootstrapped host is
the normal case.

The suite drives REAL installer runs (RIG_INSTALL_SOURCE against throwaway
RIG_HOME/RIG_BIN roots): fresh install, converge, reinstall, side-by-side
upgrade, use/rollback, both migrations, hostile flat VERSION, wedged-
symlink healing, the marker warn gate (RIG_ROLE_MARKER fixtures), both
uninstalls and the INCOMPLETE scream — driven, not grepped.

Closes #35.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:33:17 +00:00
dan-claude-bot
27fb934780 fix: verdict contract, head-bound approvals, serialized reconcile — and a testable state machine
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>
2026-07-18 19:31:02 +00:00
dan-claude-bot
52dfc5c5c1 docs: reviews end in a verdict — approve or request changes, never a bare comment
Maintainer rule: a comment-only review is a non-verdict the state machine
(and the board) cannot read. Verdict carries blockingness only; nits ride an
approval, blockers — including verdict-gating questions — are request-changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:50:51 +00:00
dan-claude-bot
7109bb39d8 feat: label automation — the state reconciler, path-scoped labeler, and CONTRIBUTING
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>
2026-07-18 18:31:53 +00:00
Daniel Marin
7646346311
Merge pull request #33 from dan-claude-bot/docs/label-taxonomy
docs: LABELS.md — the label taxonomy (states, stale/blocked, scopes)
2026-07-18 19:20:06 +01:00
dan-claude-bot
ca37eb8450 docs: make the bootstrap trim idempotent — swallow delete of an already-gone label
All three bot reviewers flagged it: gh label delete is not an upsert, so a
second run of the safe-to-run block errored on the trim lines. Loop + '|| true'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:04:51 +00:00
Daniel Marin
539dee064a
Merge pull request #30 from dan-claude-bot/feat/close-root-reachability
users: finish #17 — close-root proves the door (sudo -n, per-user sshd -T), @root key seeding, runner row owned
2026-07-18 18:39:35 +01:00
Daniel Marin
0e2d66f456
Merge pull request #29 from dan-claude-bot/feat/class-model-remnants
feat: the #12/#25 remnants — README class table + box rename, unpinned-install note, box effective check, coolify marker warnings
2026-07-18 18:36:32 +01:00
dan-claude-bot
e07a8ab567 docs: LABELS.md — the label taxonomy (states, stale/blocked, scopes)
Documents the shared heavy-duty label design at the root: state labels
that answer 'who is the ball with' across the bot-review loop, sweep-managed
stale + blocked, rig's scope set, and the bootstrap block. State labels are
automation-owned; the reconciler workflow is a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:34:49 +00:00
dan-claude-bot
a32d2b04cb fix(close-root): the gate judges AllowGroups/DenyGroups too — same door, other hinge
Round-2 convergence (codex + claude-bot): sshd enforces the group
directives against the candidate's ACTUAL membership, and the gate read
only the *Users pair — an admin outside 'AllowGroups sudo' still
reached ADMIN_OK=1, and root closed on a false proof. The gate now
resolves id -Gn and judges both group directives with the *Users
discipline: DenyGroups flags on a held-group literal or ANY
pattern/host-qualified token; AllowGroups, when set, passes only on a
literal token naming a held group (a pattern that would admit proves
nothing — over-refusing stays the safe error). id failing yields no
groups, which makes a set AllowGroups flag: fail closed there too.

Both requested regressions ride the sourced lib (unmet AllowGroups,
DenyGroups naming a held group) plus the pattern/pass cases, and grep
guards pin the shipped gate to the verdicts and to real membership.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:17:39 +00:00
dan-claude-bot
af327cb660 docs(bootstrap): finish the sentence the doctor-gate edit truncated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:16:29 +00:00
dan-claude-bot
a5b48d5b2c fix(close-root): DenyUsers judged fail-closed — patterns and USER@HOST flag
All three reviewers, same substance, and they were right that it was
lockout-adjacent: the literal grep passed a candidate whom a DenyUsers
PATTERN really denies ('DenyUsers dan*' vs admin 'dan'), and the door
closed on a false proof. The judgment now lives in the lib as a pure
deny_verdict: a literal hit flags, and so does ANY pattern or
host-qualified token — a token the check cannot prove irrelevant counts
as a hit, never as a pass. The asymmetry with AllowUsers is now the
same direction on both sides: every error closes toward repair, never
toward a welded-shut door.

Also (claude-bot): the -C probe resolves Match blocks against a
synthetic addr=127.0.0.1, so Match Address is out of the local proof's
scope — named in --help, the README, and the gate's comment, so the
separate-session advisory reads as load-bearing, not ceremony.

Regressions ride the sourced lib: wildcard (the review's dan* case),
'?', USER@HOST, literal hit, irrelevant-literals pass, plus a grep
guard that the shipped gate consults deny_verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:02:48 +00:00
dan-claude-bot
cee7d4575e fix(bootstrap): gate the host-set-up claim on 'box doctor', not on PATH
All three reviewers, same substance: 'command -v box' proves the CLI
landed, not that setup-host took effect — and the success line claimed
both. The claim is now split to match its proofs: PATH proves the
install; 'box doctor' (box's own effective-state verdict — the daemon
stays box's domain) gates "host set up". A failed doctor WARNS with the
verdict verb and the manual path, and claims nothing it cannot prove.

Also: the coolify marker guard matched 'role=control-plane ' by its
trailing space, coupling it to the marker's field formatting — a bare
'role=control-plane' line now reads the same (claude-bot's nit), with a
fixture proving it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:59:43 +00:00