Commit graph

209 commits

Author SHA1 Message Date
Daniel Marin
df29d43a23
Merge pull request #121 from claude-bot-andresmgsl/build/120-kimi-panel
fix: kimi-bot-andresmgsl joins the review panel — the roster predated it joining the bench
2026-07-24 01:20:37 +01:00
claude-bot-andresmgsl
0b3c7cb055 fix: kimi-bot-andresmgsl joins the review panel — the roster predated it joining the bench
The ceremony conversion (#112) extracted the panel= line verbatim from the
pre-ceremony labels-reconcile.sh BOTS array, which predates kimi-bot joining
the family bench. A name missing from that line is a reviewer the machinery
never waits for: the handoff would report a full panel while one verdict
short — the exact defect cast@2612967 fixed after cast#143 shipped it.

labels.conf is the source of truth the reconciler reads; CONTRIBUTING
mirrors it for humans requesting reviewers by hand. Both move together.

Closes #120

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 23:53:30 +00:00
Daniel Marin
66d304f21f
Merge pull request #112 from claude-bot-andresmgsl/build/13-convert-rig
feat: convert rig to the shared ceremony — machinery by reference at 0.1.0
2026-07-23 12:58:33 +01:00
claude-bot-andresmgsl
3c72c1b8c3 docs: changelog — the conversion's Unreleased entries (ceremony#13)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 00:29:42 +00:00
claude-bot-andresmgsl
119fb68359 docs: CONTRIBUTING keeps only rig's specifics — the doctrine is the vendored .ceremony/ mirror (ceremony#13)
Root LABELS.md was rig's own copy of the family taxonomy doc; the
vendored .ceremony/LABELS.md is the driftless replacement, so the copy
goes. drills/README.md's gate pointer follows the guard upstream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 00:29:20 +00:00
claude-bot-andresmgsl
36da497352 docs: vendor the ceremony doctrine — .ceremony/ mirrors heavy-duty/ceremony@0.1.0, root AGENTS.md routes to it (ceremony#13, ceremony#19)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 00:27:43 +00:00
claude-bot-andresmgsl
be71e1c8b2 feat: the machinery hands over — guard scripts deleted, CI pins ceremony's actions at 0.1.0 (ceremony#13)
changelog-armed returns (rig#44's revert, now version-keyed upstream);
docs-sync guards the doctrine mirror the next commit vendors.
test/release.sh keeps rig's own surfaces — installer channels and
latest-tag resolution; the machinery halves and the workflow-shape pins
are tested in ceremony's own test/. test/labels-reconcile.sh goes whole:
it drove the deleted reconciler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 00:27:10 +00:00
claude-bot-andresmgsl
e6584ceb2b feat: labels.yml becomes the ceremony caller — panel and scope taxonomy extracted to labels.conf (ceremony#13)
The conf format takes no comment lines: every non-blank, non-panel line
must parse as name|color|description, so the file is data only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 00:26:11 +00:00
claude-bot-andresmgsl
d8a0e22894 feat: release.yml becomes the ceremony caller stub — both doors move upstream (ceremony#13)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 00:25:05 +00:00
Daniel Marin
595a152925
Merge pull request #109 from dan-claude-bot/feat/kimi-box-tenant
feat: kimi-box joins the box tenant roles — the Kimi CLI agent guest
2026-07-22 16:06:45 +01:00
dan-claude-bot
5bf0acd940 feat: kimi-box joins the box tenant roles — the Kimi CLI agent guest
The fourth agent tenant, through the one parameterized mechanism
(lib/tenant-config.sh + bootstrap-tenant.sh) — never a fifth
hand-maintained copy. The official installer (code.kimi.com/install.sh)
is uv-managed and lands kimi in ~/.local/bin, run as the tenant user
(grok's lesson: a root-owned install under a 0700 home is a CLI that
exists and cannot run); no node, uv brings its own Python. The context
file lands at ~/.kimi/AGENTS.md — the <dotdir>/AGENTS.md convention —
with an honest note that kimi documents only project-level AGENTS.md
today.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:03:25 +00:00
github-actions[bot]
7f8a0e0885 chore: bump main to 0.3.1-dev — a dev install must not impersonate 0.3.0 2026-07-21 18:07:01 +00:00
Daniel Marin
c6a3c23869
Merge pull request #101 from dan-claude-bot/release/0.3.0
release: 0.3.0
2026-07-21 19:06:51 +01:00
dan-claude-bot
9b7cb57005 docs: record the 0.3.0 drill as waived, with what that leaves unproven 2026-07-21 17:43:28 +00:00
dan-claude-bot
c77bc1a9bf release: 0.3.0 2026-07-21 17:43:28 +00:00
Daniel Marin
6b012ab548
Merge pull request #104 from dan-claude-bot/refactor/drills-per-version
refactor: one drill record per version, in drills/
2026-07-21 18:41:34 +01:00
dan-claude-bot
186d976686 fix: the guard's comment misstated set -e, and the form now matches its twins 2026-07-21 17:12:54 +00:00
dan-claude-bot
dbe04c7c36 test: the guard's verdict must match the tree, not always pass 2026-07-21 16:42:41 +00:00
dan-claude-bot
11dfeb2ecd refactor: one drill record per version, in drills/
Drill records move from sections inside drill/RUNS.md to one file per
version at drills/<version>.md.

The old guard had to parse headings: an em-dash prefix match, an optional
" — DATE" tail, a whole-version comparison so 0.3.0-rc1 could not satisfy
0.3.0, and a separate non-blank-body rule. All of it existed only because
records shared one file, and both sibling repos shipped a defect out of
that complexity in review — a `sed '/./,$!d'` extractor where `.` matches
a space (box#149, cast#138), and heading-grammar drift. One file per
version makes nearly all of it unrepresentable: 0.3.0.md and 0.3.0-rc1.md
are different files, and the filesystem does the whole-version comparison.

The awk drill_section() machinery is gone. What survives is the one rule
splitting the files does not make unrepresentable: a file of only
whitespace is not a record.

Plain drills/, NOT .drills/ — a dot-directory is invisible to globs
without dotglob, the cause of #70 here and box#116/box#118.

drill/RUNS.md is deleted; it was created in this same unmerged PR, held no
real records, and its useful reasoning moves to drills/README.md. (box
keeps ITS drill/RUNS.md — that one is a genuine harness log.)

Also corrects the ordering framing in CONTRIBUTING and the new README: the
three repos' drills are INDEPENDENT, run in any order. What makes that safe
is that each pins the same fixed set of candidate refs, so box and rig
measure the same pair — that, not sequencing, is what dissolves the mutual
recursion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:35:51 +00:00
Daniel Marin
750d93deda
Merge pull request #102 from dan-claude-bot/feat/drill-gate
feat: CI refuses a release PR with no drill record
2026-07-21 17:23:34 +01:00
dan-claude-bot
b234f48e68 test: pin that whitespace is not a drill record 2026-07-21 15:59:57 +00:00
dan-claude-bot
77a9a1ad76 feat: CI refuses a release PR with no drill record
CONTRIBUTING has always required a real-hardware drill on a release, and
nothing enforced it — so no release in this family has ever carried one.
Every other ceremony step is checked by a script; the one that costs an
afternoon was checked by a reviewer remembering. A reviewer bot finally
blocked on it.

- drill/RUNS.md: rig's own run log, starting EMPTY of records. rig has no
  drill harness of its own yet; the harness lives in box's drill/ and this
  file is the record, not the instrument.
- .github/scripts/drill-recorded.sh: a -dev tree asserts nothing; a bare
  VERSION requires a non-empty '## Release drill — X.Y.Z' section, version
  matched WHOLE so an -rc1 record is not evidence for the final.
- Per-repo on purpose. A cross-repo lookup into box fails on a token, a fork
  checkout or a network blip, and all of those degrade to 'pass' on precisely
  the tree that ships — the UNREADABLE-vs-NONE shape #90 fixed.
- It asks for a RECORD, not a RESULT, so a maintainer waiver stays possible
  but has to be written down under that version.
- Fixtures carry their own VERSION and RUNS.md (heavy-duty/box#146: fixtures
  reading the repo's real VERSION exercised only the -dev branch and went red
  first while cutting a release).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:24:42 +00:00
Daniel Marin
b8ce8e5e1c
Merge pull request #100 from dan-claude-bot/docs/changelog-one-line
docs(changelog): one line per entry, and a pass over the whole file
2026-07-21 15:44:03 +01:00
dan-claude-bot
d69ecd1104 docs(changelog): one line per entry, and a pass over the whole file 2026-07-21 13:56:39 +00:00
Daniel Marin
9c82d56230
Merge pull request #93 from claude-bot-andresmgsl/fix/sshd-privsep-verdict
fix: don't read a missing /run/sshd as a broken sshd config
2026-07-21 14:07:35 +01:00
Daniel Marin
9adf45a229
Merge pull request #99 from dan-claude-bot/fix/changelog-monotonic
fix: catch a deleted or duplicated release heading in CHANGELOG.md
2026-07-21 00:36:37 +01:00
dan-claude-bot
1db7779b03 test: terminate the ci.yml step block at the job boundary too
The monotonic step is the LAST step of its job, so stopping only at the next
`- name:` ran the block into the job below and swallowed that job level `if:`.
Unanchored `grep -q "if:"` then fired on it — the same bug the scoping was
meant to fix, moved from "any step in the file" to "this step plus the head of
the next job".

Terminates on a new step OR a new job now, and the key is anchored so an `if:`
inside a `run:` line is not mistaken for a step condition.

Found by claude-bot-andresmgsl on heavy-duty/box#144; this port carried the
identical awk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:56:57 +00:00
dan-claude-bot
c199e8fd04 fix(changelog-monotonic): report containment vacuous when the base IS HEAD
Dropping the pull_request gate made merge_base == HEAD a routine path rather
than a degradation, and the success line did not follow. On every push to main
the step printed "all N release heading(s) at the merge base are still present"
— a containment claim on the one event where deletion is undetectable, since
the comparison is the file against itself.

That is the dishonesty this PR fixed in the skip messages, surviving in the
success message. The line now has two forms: containment vacuous, naming
uniqueness as the half that ran, or the existing containment wording when a
real base exists. Both pinned, plus a negative that the two do not collapse.

The "an untouched branch passes" case turned out to be this exact shape — its
fixture never commits on 'work', so it was asserting containment on a
self-comparison. Its assertion moved to uniqueness's count, and a companion
case with a real base now carries the containment wording it used to claim.

Also scopes the ci.yml negative pin to the monotonic step's own block. As a
file-wide grep it forbade any FUTURE step from being pull_request-gated and
would have failed citing #98 when one legitimately was. A companion check
keeps the awk extractor from matching nothing and going tautological.

Ported from heavy-duty/box#144 (box@94d830c).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:49:02 +00:00
Daniel Marin
27a5fa282e
Merge pull request #97 from dan-claude-bot/fix/labels-sweep-on-labeled
fix(labels): sweep on `labeled` so the handoff is immediate
2026-07-20 21:32:44 +01:00
dan-claude-bot
7ffc30bacc fix(changelog-monotonic): check uniqueness before anything base-side (#98)
Uniqueness is a property of HEAD alone — no base ref, no merge base, no base
blob. It sat downstream of all three, so every degradation path returned
success on a tree carrying a duplicate.

The base-blob path was the worst: a branch that introduces CHANGELOG.md hit a
bare `exit 0` on a message that was true about deletion and silent about the
duplicate in front of it. STRICT could not reach it — STRICT guards the two
skip() calls, and that is not one of them.

That inverted the two halves. Deletion needs a diff to see; duplication is the
one changelog_section() actually mis-renders, stopping at the second heading
and truncating the release's real body. The half with the live extraction bug
behind it had the most ways to silently not run.

Moved, not rewritten. The skip messages now say containment skipped and that
uniqueness already passed. The CI step is no longer pull_request-only, with a
`github.ref_name` fallback because base_ref is empty on a push and a bare
`origin/` under STRICT would redden every push to main. The script is also now
100755, matching cast's copy of the same file.

Regression cases pin the ORDER, not the exit code: verified they go red
against the pre-fix script (7 failures) and green against the fixed one.

Same defect fixed upstream in heavy-duty/box#144 (heavy-duty/box#143), which
rig's copy of this script was ported from. Found by claude-bot-andresmgsl and
codex-bot-andresmgsl reviewing #99.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:22:43 +00:00
dan-claude-bot
0865ce6c02 fix(changelog): cite the sibling PRs, not the tracking issues
The "Landed in all three repos together" line pointed at the sibling
tracking issues rather than the sibling PRs. The entry already opens with
its own issue ref, so a reader following "landed together" was sent to more
issues and never reached the actual sibling changes.

All three PRs carried it identically because the three entries came from one
generator that took sibling references from its issue-number map, and
expanded them into the sentence without re-wrapping — which is also why the
line ran to 108 columns in a file that wraps at 83.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:07:58 +00:00
dan-claude-bot
b2e7febf08 fix: catch a deleted or duplicated release heading in CHANGELOG.md (#98)
The arming rule (#66) guards ONE heading — does the top section agree with
VERSION? — and is silent about the rest of the file. The failure that lives
there is an author adding an entry under `## Unreleased` who replaces the
shipped heading below it instead of inserting above it. git merges the
one-line edit cleanly, `changelog_armed()` stays green (correctly: the top
section is still right), and the shipped release loses its section entirely.
It surfaces a whole release later, when release.yml refuses to publish a
section `changelog_section()` can no longer find by heading.

"A heading disappeared" is a property of a DIFF, not of a tree, so this is
its own script rather than a clause in the arming check — which is also
driven from test/release.sh against constructed non-git VERSION + CHANGELOG
pairs that could not express it. The rule needs no tuning: release headings
are append-only, so SUPERSET is exact, and the ceremony's stamp passes by
construction because `Unreleased` fails the version shape.

Ported from heavy-duty/box#122, with box's second half intact: containment
cannot catch a DUPLICATED heading, since the duplicate is head-side surplus
and `comm -23` is blind to extras there — so uniqueness on HEAD is asserted
alongside it. rig's symptom differs from box's and the comments say so:
box's extractor re-arms on every `## ` line and ABSORBS what sits between
the copies, while rig's `changelog_section()` has `if (found) exit` and
TRUNCATES at the second copy, dropping the release's real body.

Wired on pull requests only (on a push to main the merge base is HEAD, so
the assert is vacuous), with CHANGELOG_MONOTONIC_STRICT=1 and fetch-depth: 0
so a checkout that cannot reach the base ref fails rather than skipping
quietly forever.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:00:48 +00:00
dan-claude-bot
23cca65855 fix(labels): sweep on labeled so the handoff is immediate
A review landing was never a trigger for the labels workflow, so the exact
moment `state:needs-human` became true — the third bot approving — fired
nothing, and the label waited on the `*/15` cron. That cron does not run at
its declared rate: measured across box, rig and cast over a two-hour window
on 2026-07-20, one scheduled run each against the eight `*/15` implies.

The obvious fix does not work. There is no `pull_request_review_target`, and
on fork PRs — all of them here — `pull_request_review` runs with a read-only
token and cannot label anything.

So the handoff wakes the sweep itself:

- `pull_request_target` also fires on `labeled`/`unlabeled`
- the author sets `state:needs-human` at handoff, as the third act after the
  round summary and the review request

The author's own label write fires the sweep that validates it — an
optimistic write, not a transfer of ownership. The reconciler confirms or
corrects it seconds later, and the cron falls back to a last resort. It
cannot loop: the reconciler writes with GITHUB_TOKEN, which does not create
workflow runs; agent writes use a PAT, which does.

`labels-reconcile.sh` is unchanged — it already recomputes every open PR
from scratch on every run, which is what makes the optimistic write safe.
The `scope` job is skipped on label events, where no path can have changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:44:07 +00:00
Daniel Marin
ac1bb3bf76
Merge pull request #94 from dan-claude-bot/docs/contributing-blocker-axis
docs(contributing): document the blocker axis and merge-next ownership
2026-07-20 20:27:32 +01:00
Daniel Marin
9ba16946fd
Merge pull request #91 from codex-bot-andresmgsl/fix/issue-89-readme-channel
docs: align README quick start with main
2026-07-20 19:41:12 +01:00
dan-claude-bot
8b2ddbdd2d docs(contributing): document the blocker axis and merge-next ownership
The who-sets-what table is the day-to-day answer to "can I move this by
hand", and it never mentioned blocker:* -- a whole machine-owned family
added when state:needs-rebase was retired. merge-next was missing too, and
that is the one label whose ownership actually needs saying, because it is
the only one in the machine's vocabulary the machine deliberately does not
set.

Step 6 also read as though requesting the maintainer is sufficient to flip
state:needs-human. It is not: needs-human requires zero blockers, so the
request does nothing on a conflicted or red PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 18:40:12 +00:00
Daniel Marin
88d83a1aba
Merge pull request #90 from dan-claude-bot/fix/labels-two-axis
refactor(labels): split PR labels into state and blocker axes
2026-07-20 19:30:07 +01:00
dan-claude-bot
5eddf2e2ed fix(labels): a missing state label skips the edit, not the whole PR
Round 2 review (claude-bot, codex-bot — both raised this, independently).

The `return` added in round 1 aborted all of reconcile_pr, not just the
label edit. Everything below it is independent of the state:* taxonomy:
`merge-next` clearing and the stale sweep both stopped running. So a
cold-start repo left `merge-next` claiming "merge this one next" on a PR
the board had moved to the agent — the original false-invitation bug,
reintroduced inside the very fix meant to survive a cold start.

It was also a regression against main, not just a missed improvement:
the old code failed the `gh issue edit`, logged, and fell through to both
blocks. Round 1 turned a per-edit failure into a per-PR abort.

Now a `skip_edit` flag skips only the edit and control reaches the rest.

Also from review: drop the dead `"$desired"` term from the filter loop
(it was appended and then unconditionally continued past), and turn
`[ -n "$missing" ] && log` into a proper `elif` rather than an
&&-as-statement under `set -e`.

Adds the first four fixtures that exercise reconcile_pr itself, stubbing
run/gh to probe a cold-start repo against a bootstrapped one. Everything
before this tested pure functions, which is exactly why a per-PR return
got through: nothing could see it.

Fixtures 68 -> 72.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 18:02:03 +00:00
claude-bot-andresmgsl
2dc47112f2 fix: don't read a missing /run/sshd as a broken sshd config
`sshd -t` folds two questions into one exit code — is the merged config
parseable, and is the privilege-separation directory there. Both call
sites ran it as `sshd -t 2>/dev/null` and read any non-zero exit as the
first question's answer, discarding the line that named the second.

Bootstrap aborted with "sshd rejects the merged config", a verdict sshd
never reached, and sent the operator to audit /etc/ssh files that were
never broken. /run is a tmpfs and /run/sshd is ssh.service's
RuntimeDirectory, so it is legitimately absent under socket activation
on a box whose SSH door is serving connections normally.

Classification is now a pure, sourceable sshd_privsep_gap: the status is
the verdict, the text only classifies a failure, so a passing sshd -t is
never diverted. sshd_config_ok repairs the gap with an idempotent
install -d and retests once. A genuine parse refusal still refuses and
the rollback is untouched.

Refusals now carry sshd's own stderr. users-close-root had the identical
three lines and now reaches the shared judgement through lib/sshd.sh
instead of keeping a second copy of it.

Fixes #92

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:58:10 +00:00
codex-bot-andresmgsl
491055c4cf docs: point stable installs at matching README 2026-07-20 17:50:18 +00:00
dan-claude-bot
b07e734fc0 fix(labels): survive a repo that lacks the new blocker labels
Round 1 review (claude-bot, codex-bot — both raised 1 and 2).

1. `gh issue edit --add-label` rejects the WHOLE call on one unknown
   label name, and the blocker:* labels are created only by the
   dispatch-only bootstrap. So the first sweep after this lands would
   have converged NOTHING on exactly the PRs this change exists to heal,
   surfacing only as a WARNING in a cron log. Batching state and blockers
   into one edit for anti-flicker is what widened that blast radius.
   Every label about to be ADDED is now filtered against the repo's real
   label set, read once per sweep; removals need no filter because they
   are built from has_label. An unreadable label set does not filter, so
   a failed read cannot silently strip the board.

2. blocker:unrequested fired only on MISSING, so a round whose approvals
   all staled behind a push — with nothing re-requested — carried no
   blocker at all, though the agent owes exactly the same ask. Now
   MISSING or STALE: both mean this head has no verdict from that
   reviewer.

3. LABELS.md: restore the substantive "Leaves when" text for
   state:addressing, and widen the blocker:unrequested row to name both
   shapes now that (2) changes what the label means.

Fixtures 66 -> 68.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:49:44 +00:00
dan-claude-bot
4f40cfabf2 fix(labels): distinguish an unreadable check rollup from no checks
When `gh pr view` failed, the fallback left the `statusCheckRollup` key
absent, and `(.statusCheckRollup // [])` collapsed that into the same
NONE as a PR with genuinely no checks. NONE blocks nothing, so an API
hiccup presented the PR as mergeable by a human — an unknown certified
as green, the shape #87 exists to stop, in the one place it never looked.

checks_state now returns UNREADABLE for an absent key versus NONE for a
present-but-empty array, and the sweep leaves an UNREADABLE PR alone
rather than recomputing on facts it did not read. Not a blocker on
purpose: blocking would flap the board on one bad call.

Fixtures 64 -> 66.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:38:32 +00:00
dan-claude-bot
4ea3579c2e docs(changelog): record that CI never ran the label fixtures
The CI wiring is independently valuable from the two-axis refactor and
was missing its own entry: rig's label state machine gates every PR and
its fixtures had never executed in CI, including for #88 which merged
today reporting 51 passing fixtures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:34:21 +00:00
codex-bot-andresmgsl
b88495105d test: tighten README channel contract 2026-07-20 17:32:43 +00:00
dan-claude-bot
3f6f05766f refactor(labels): split PR labels into state and blocker axes
Retires `state:needs-rebase` in favour of two independent axes: `state:*`
(whose ball, exactly one) and `blocker:*` (what is in the way, additive).
One rule joins them: `state:needs-human` requires zero blockers.

The single-label design projected independent facts — mergeability, check
status, review round — onto one totally-ordered value, so one always won
and the rest vanished. Every precedence bug this machine has had lived on
that ordering. Blockers are a set, so there is no precedence between them
to get wrong.

`state:bots-reviewing` tightens to mean strictly "a request is live"; a
ready PR nobody was asked to review is now `state:addressing` +
`blocker:unrequested`. The reconciler strips the retired
`state:needs-rebase` on sight via a RETIRED array.

Also wires test/labels-reconcile.sh into CI, where it had never run.

Fixtures 51 -> 64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:30:53 +00:00
codex-bot-andresmgsl
32e17246a2 docs: align README quick start with main 2026-07-20 17:30:11 +00:00
Daniel Marin
616ada76b9
Merge pull request #88 from dan-claude-bot/fix/labels-mergeability-aware
fix(labels): `state:needs-human` means a human could merge it right now
2026-07-20 18:02:57 +01:00
dan-claude-bot
108a944e1c chore(labels): converge reconciler and fixtures on the canonical text
box, rig and cast carry the same reconciler, and the three had drifted to
functionally identical but textually different versions of checks_state
after the same fix was reached independently in each repo. Comment prose
and one fixture helper name differed, so the files were no longer
byte-identical and the convergence check had nothing to compare.

Standardised on the cast wording for checks_state and on the canonical
test/labels-reconcile.sh, which carries the same 51 fixtures under
overlap_() where this repo had named the helper drained_(). No behaviour
change: the diff to the reconciler is comments only, and the suite is
unchanged at 51 passed, 0 failed.

Only this repo's own scope:* rows are preserved as local content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:44:19 +00:00
dan-claude-bot
57e37363f4 fix(labels): order check runs by one consistent quantity — when they began
The dating expression took the newest stamp each run carries. That reads
`completedAt` for a finished run and `startedAt` for a live one, so the
comparison comes down to "when this one ended" against "when that one
began" — which is not an ordering on runs at all.

A run cancelled by the concurrency group does not stop the instant its
replacement starts: the runner has to receive the signal and wind down. So
`predecessor.completedAt > successor.startedAt` is the ordinary case, not a
corner. On the box#137 tip that motivated the supersede rule the window was
13s wide — the superseding run started 15:19:38, the run it cancelled did
not finish until 15:19:51 — and for that whole window the dying predecessor
out-dated its own live replacement, so `last` discarded the replacement and
judged the corpse.

Both round-3 failure modes came back inside that window, narrowed rather
than closed: a draining CANCELLED predecessor reported FAILURE and sent the
agent to fix nothing, and a draining SUCCESS predecessor reported SUCCESS —
mergeable, all bots approve, state:needs-human — over a tree whose merge
button branch protection had already disabled. #136 again, one field over.

Dated by `first` of the preference-ordered stamps rather than `max` of
them: start time if the run recorded one, falling back only if it did not.
The sentinel filtering is unchanged, and finished runs still date by
completion when that is all they carry, so the supersede rule keeps the
case it exists for.

Found independently by claude-bot-andresmgsl and codex-bot-andresmgsl.
No existing fixture could express it — `run_()` carries no startedAt, so
every supersede fixture spaced the predecessor's completion safely before
the successor's start, the same blind spot as round 3 one field over. New
`drained_()` helper pins both directions; fixtures 48 -> 51 (with the
reverse-direction in-flight fixture ported from cast#128).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:32:53 +00:00
dan-claude-bot
800571fdfb fix(labels): date a check run by when it started, not by a zero completion
The supersede collapse added in the previous commit dated each run by
`.completedAt // .startedAt // .createdAt`. A run still in flight has no
completion, but `gh` does not omit the field: its Go struct marshals the
zero time as the string "0001-01-01T00:00:00Z", and jq's `//` falls
through null and false only. The sentinel was therefore taken as the sort
key, and it sorts before every real timestamp — so the LIVE re-run became
the oldest entry in its context, `last` discarded it, and the run it
superseded was judged instead.

That restored #136 through the fix for it: a green context with a
replacement mid-flight reported SUCCESS, so a PR read mergeable, green,
all bots approve — state:needs-human — while branch protection had the
merge button disabled. It also narrowed rather than removed the flap the
supersede rule exists to prevent: between "run A cancelled by the
concurrency group" and "run B finishes", the PR reported FAILURE and the
agent was sent to fix something that was not broken.

Runs are now dated by the newest timestamp they actually carry, with both
spellings of absent discarded (null, and the zero sentinel). Entries that
carry no usable timestamp sort LAST rather than first: something we cannot
date is most likely the thing just created, and treating it as newest
keeps an undateable in-flight run from being discarded in favour of a
stale success. Every ambiguity resolves toward "not settled".

Found independently by claude-bot-andresmgsl and codex-bot-andresmgsl.
The fixtures could not have caught it: `run_()` always emits a real
completedAt, so every supersede fixture was a race between two finished
runs, and the bug lived in the one shape the helper could not express.
New `inflight_()` helper covers it; fixtures 44 -> 48.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:18:50 +00:00