Commit graph

7 commits

Author SHA1 Message Date
dan-claude-bot
d8f73f446d fix(labels): unknown check outcomes and mixed rounds must not read green
Round 2 of #128. Two blockers from the bot panel, both real holes in the
invariant this PR exists to establish.

The check-rollup classifier enumerated the outcomes that block and
defaulted everything else to SUCCESS, so ERROR, CANCELLED and STALE fell
through to green. Inverted to an allow-list of the outcomes that do NOT
block — SUCCESS, NEUTRAL, SKIPPED and the pending set — with everything
else blocking. The rollup mixes two closed enums (CheckRun.conclusion and
StatusContext.state) and the costs are asymmetric: a false failure parks
the PR on the agent, who looks; a false success invites a human to merge a
tree that will not merge. Superseded runs are dropped first, each context
collapsing to its newest entry keyed on workflow + job name, so a re-run
does not strand its own PR in needs-rebase. The classifier also moved out
of main() into checks_state(), which is why no fixture caught this — it
was inline in the fetch loop and could only ever be injected pre-decided.

decide_state() returned from inside the bot loop on the first MISSING, so
a STALE belonging to a later bot in BOTS was never read, and a round that
was both unfinished and staled came out needs-human over a head nobody had
reviewed — the original bug wearing a different hat. The whole round is
now collected before any precedence is applied, STALE checked before
MISSING. The MISSING-yields-to-an-explicit-human-request rule is untouched.

Fixtures 29 -> 44, pinning the check-outcome enum, the supersede rule at
both orderings, and the mixed round at both ends of BOTS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:06:41 +00:00
dan-claude-bot
b063e6bc42 fix(labels): state:needs-human means a human could merge it right now
Ported from heavy-duty/box#137 (heavy-duty/box#136) so the three repos'
reconcilers stay byte-identical. The state machine here was byte-identical to
box's before this change and remains so after -- only the scope:* taxonomy
differs, correctly.

decide_state() derived state from three inputs -- draft flag, requested
reviewers, submitted reviews -- and read NOTHING about mergeability or checks.
With the `if requested "$HUMAN"` short-circuit at the top of its precedence,
the label was sticky: once the maintainer was requested, a PR read
state:needs-human through conflicts, through red CI, through a force-push that
staled every approval.

In this repo the SECOND half is the live one: three PRs sit at
state:needs-human simultaneously with nothing saying which to merge first, and
they will conflict through CHANGELOG.md the moment one lands. The stickiness
has not bitten here yet only because nothing has conflicted -- the code carried
it identically, so the first merge would have reproduced box's situation.

The rule the label now keeps: state:needs-human means a human could merge this
RIGHT NOW, so anything making that false outranks the request that put it
there.

  CONFLICTING or failing checks -> state:needs-rebase (new; the agent's to fix)
  approvals staled by a push    -> state:addressing   (nobody reviewed this tree)

An UNFINISHED round still yields to an explicit human request -- MISSING
(nobody has reviewed yet) is a different fact from STALE (everyone reviewed
something else). UNKNOWN mergeability is NOT treated as unmergeable: GitHub
reports it for about a minute after every merge, and flapping every open PR
through needs-rebase on each merge would be worse than the bug. A failed read
degrades to the same "do not know" value.

Also adds merge-next -- the label this repo needs most today, since a correct
needs-human still does not say which of three ready PRs to merge first. Queue
order is intent, so the reconciler never sets it, only CLEARS it.

Fixtures 19 -> 29. DRY_RUN against this repo changes NOTHING, which is the
correct result: every open PR here is currently mergeable, so the new
precedence is a no-op on a healthy board and fires only when something is
actually wrong. npm test 623 passed.

Closes #127

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:28:50 +00:00
dan-claude-bot
ced5c497c0 feat: release flow — tagged releases with a prebuilt dist asset (#96)
The cast half of the flow designed in heavy-duty/box#83, aligned with
box#90 and rig#40, plus the piece unique to cast: a prebuilt release
asset, because cast is the one repo where the source tarball is not the
package.

- CHANGELOG.md (box's format) with this PR's entry under Unreleased;
  feature PRs land their entry as part of the PR.
- `cast --version` / `-V` answers with package.json's version, read
  relative to the compiled module so a source checkout and an installed
  prebuilt tree agree.
- release.yml, on EVERY tag push (no shape filter — a mismatched tag
  must fail the assert loudly, not be pattern-skipped): asserts tag ==
  package.json version FIRST, extracts that version's changelog section
  (.github/scripts/release-notes.sh, shared with the tests; missing or
  empty refuses), builds once (npm ci && npm run build && npm prune
  --omit=dev), stages bin/ dist/ node_modules/ package.json as
  cast-X.Y.Z/ and attaches cast-X.Y.Z.tgz to `gh release create
  --verify-tag`. No tests here — ci.yml gated the merge commit, and the
  suite needs age.
- install.sh grows the three channels: default = the latest release's
  asset (tag resolved off the releases/latest redirect Location — no
  API, no token; failure dies loudly naming CAST_REF=main, never a
  silent fallback), CAST_REF=<tag> = pinned (asset first, source
  fallback), CAST_REF=main = dev build-from-source. npm is required
  only on the source path, and a prebuilt tree is sanity-checked
  (dist/, node_modules/) before $DEST is replaced.
- test/release.test.ts drives it all offline: --version, the extraction
  against fixtures (0.7.0 never matches 0.7.0-rc1) and the real
  changelog, and REAL install.sh runs through all three channels with a
  stub curl and a poisoned npm — including the loud no-releases refusal
  with no $DEST side effects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:27:55 +00:00
dan-claude-bot
19e5401067 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:59 +00:00
dan-claude-bot
42d138edbf 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:55 +00:00
dan-claude-bot
00c748c879 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:04 +00:00
dan-claude-bot
884a95f5fa 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:54 +00:00