Commit graph

16 commits

Author SHA1 Message Date
claude-bot-andresmgsl
26129672ea fix: the panel roster is the CURRENT bench — kimi was dropped by porting a stale list
The conversion preserved cast's pre-ceremony CONTRIBUTING panel verbatim,
but that list predates kimi-bot joining the bench — and the omission then
self-licensed: the handoff read the roster this very PR had authored,
counted codex+grok as the full panel, and handed off one reviewer short.
A conversion PR must write the roster from the current family bench, not
inherit whatever the repo last knew.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:14:42 +00:00
claude-bot-andresmgsl
f223aa699e docs: CONTRIBUTING points at the mirror; docs swept for deleted-path pointers
CONTRIBUTING keeps only what is genuinely cast's — the panel roster, the
local checks, the changelog house style, the prebuilt-asset contract and
the promotion drill meaning; the review-round doctrine and label taxonomy
now live in .ceremony/. drills/README.md and labeler.yml repoint at the
pinned guard and the vendored LABELS.md (the sweep rule from ceremony#12,
learned in rig#112).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 13:59:24 +00:00
dan-claude-bot
e690c89fee refactor: one drill record per version, in drills/
Drill records move from sections inside drill/RUNS.md to one file per
version: drills/<version>.md. The old guard parsed headings — em-dash
field matching, an optional ' — DATE' tail, a whole-version comparison
so 0.2.0-rc1 could not satisfy 0.2.0, a '(NF == 5 || $6 == dash)' tail
constraint to match box, and a non-blank body rule.

All of that existed only because records shared one file, and this repo
shipped two defects out of the complexity in review: the sed '/./,$!d'
whitespace bypass, and heading-grammar drift from box's stricter form.
One file per version makes nearly all of it unrepresentable — 0.2.0.md
and 0.2.0-rc1.md are simply different files, so the whole-version rule
is the filesystem's rather than a comparison anyone can get wrong.

One rule survives: a file of only whitespace is not a record.

Plain drills/, not .drills/ — dot-directories are invisible to globs
without dotglob, the cause of #118/#121 here and box#116.

drill/RUNS.md is deleted. It was created in this same unmerged PR and
held only format documentation, no real records; the useful reasoning
moves to drills/README.md. (box keeps ITS drill/RUNS.md, a genuine
harness log with real run history.)

The docs also drop an over-constrained ordering claim: the three repos'
drills are INDEPENDENT, run in any order and any sitting. What makes
that safe is that each pins the same fixed set of candidate refs — and
that pinning, not sequencing, is what dissolves the box/rig recursion,
since refs are static identifiers that exist as soon as the release
branches do. Each repo also drills a different thing: box the isolation
contract, rig convergence, cast promotion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:36:13 +00:00
dan-claude-bot
2578a570ca feat: CI refuses a release PR with no drill record
CONTRIBUTING has always asked for the full real-hardware drill on a
release. Nothing asserted it, so it was performed exactly as often as a
reviewer remembered to ask — which is never, across every release in the
family, until a reviewer bot finally blocked on it. The gate moves out of
memory and into the tree.

drill/RUNS.md is cast's own run log, starting empty: no fabricated
history, and an honest note that cast has no drill harness script yet —
its legs are run by the documented procedure. The file is the record, not
the instrument.

.github/scripts/drill-recorded.sh reads package.json and asserts that a
bare version has a non-empty '## Release drill — X.Y.Z' section. A -dev
tree has no ship claim and passes trivially. The version is matched
WHOLE via awk field equality, release-notes.sh's fix for the same trap:
0.2.0 is not satisfied by 0.2.0-rc1, or the reverse.

It requires a RECORD, not a PASS. A maintainer waiver is legal and is
itself a section in drill/RUNS.md, so skipping the drill stays possible
and stays a deliberate, reviewable commit rather than an oversight.

The drill itself is ONE orchestrated run over the whole stack: rig
bootstraps a bare host and installs box, box new mints a seed, the seed
calls rig back to converge, and cast's legs run on the result. rig sits
below box and above it, so the repos are mutually recursive rather than
linearly ordered and their releases are not published in a fixed
sequence. The run pins candidate refs (RIG_REPO/RIG_REF at mint time),
so no repo must ship before another can be drilled, and drilling the
candidate is drilling the release — a release diff is the version file
and CHANGELOG.md, nothing executable.

Each repo records its own legs from that run, citing the shared run ID
and the other repos' SHAs. cast never reads box's or rig's drill log to
decide whether cast may ship: a cross-repo lookup degrades to "pass" the
moment it fails to resolve — the unreadable-rollup class.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:24:08 +00:00
dan-claude-bot
79f754549b docs(changelog): one line per entry, and a pass over the whole file 2026-07-21 14:27:34 +00:00
dan-claude-bot
d9bb634a5e 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
dan-claude-bot
ee723c1126 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
dan-claude-bot
22a4e26e02 fix: the arming guard names the section that ships, not the top one
Review round on #114. Both blockers were real and reproduced here.

The re-arm and the older extraction guard contradicted each other: that
guard asserted the TOP section extracts non-empty, and CONTRIBUTING now
mandates a deliberately EMPTY `## Unreleased` on top of the stamp. The
mandated ceremony tree was CI-red — #108's unshippability by another
route. Keying to the top section was only ever a stand-in for "what
release.yml publishes", so the assert now names that section: on a bare
version the `## X.Y.Z` being shipped, on a `-dev` tree the newest
stamped one. rig#67 retargeted the identical assert for the same reason.

And a bare version with no matching stamped section — bumped, never
stamped — passed every test and failed only after the merge, in
release.yml's notes step, past the ship decision. Red here instead.

Plus a double re-arm (two `## Unreleased` headings, the extracted
section silently the empty first one) is now red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:33:49 +00:00
dan-claude-bot
3e1b579e61 fix: the ceremony re-arms the changelog, and CI notices when it doesn't (#113)
Repair main's missing `## Unreleased`, re-arm in the CONTRIBUTING ceremony
step, and add a version-keyed guard in test/release.test.ts.

Cross-refs heavy-duty/rig#66 (origin, confirmed occurrence) and
heavy-duty/box#108 (box sibling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:39:45 +00:00
dan-claude-bot
b92919d4d0 fix: pull-requests scope for the door's two PR-API calls; docs catch up to the shipped shape
claude-bot's round-4 blockers: (1) a declared permissions: block zeroes
every unspecified scope, so the decide step's label read
(commits/<sha>/pulls) and the bump fallback's gh pr create could only
403 — every genuine ceremony would end red at the label check, the exact
failure shape this feature exists to kill, one layer down. labels.yml
already carries the precedent; pull-requests: write added with the two
consumers named. (2) CONTRIBUTING still prescribed the follow-up bump PR
the workflow now performs itself, and the changelog entry described the
old PR-base interlock — both now tell the shipped story (event.before
interlock, self-re-arm, manual-path bump stays the operator's). Nits
taken: fetch-depth 2 for the all-zeros fallback's first parent, re-runs
refuse-loudly wording, the bump-window arithmetic comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:31:40 +00:00
dan-claude-bot
775af63063 feat: merging a release-labeled PR is the release (#111)
box#95 taught the family that a forgotten manual tag is the worst
failure shape: silent, no red X, a release that simply doesn't happen.
The ship decision already lives in the ceremony PR — the one whose whole
diff is the version leaving -dev, carrying the reviews and the
maintainer's merge — so tagging after it is transcription, and
transcription belongs to the machine (box#96's design; this is cast's
twin).

release.yml now also triggers on pull_request closed against main,
gated on merged == true AND the hand-set release label. The merge path
asserts four facts in order, each fail-loud and creating nothing: the
merged package.json version is non--dev (read via node, never regex —
the pkg_version discipline); the version CHANGED in this PR (base vs
merge — the -dev interlock, so a mislabeled ordinary PR fails loudly);
the version's changelog section extracts non-empty via the existing
release-notes.sh; and no tag or release exists yet (idempotent re-runs,
and the loud answer to a manual-tag race). Then, in the same job, it
tags the merge commit via the API and publishes. Same-job is
load-bearing: a GITHUB_TOKEN-created tag triggers no workflows, so the
tag-push path cannot fire on it and double-publish.

Both trigger paths converge on literally the same steps — each entry
step exports RELEASE_VERSION, and the notes extraction, the exact
existing asset build (npm ci, npm run build, npm prune --omit=dev,
staged as cast-X.Y.Z/), and the gh release create read only that — so
the paths cannot drift and the installer keeps finding the one asset
name it knows, cast-X.Y.Z.tgz. The tag-push path survives as the
documented manual fallback and backfill, and it matters immediately:
0.1.0 never carried -dev (cast predates the ritual), so the interlock
correctly does not fire for #110's ceremony — that one ships by manual
tag, and the automation applies from 0.1.1 on.

test/release.test.ts pins the new wiring in the house grep style,
fail-closed: the merged+labeled gate, the four asserts strictly ordered
ahead of tag/build/publish, the single job, the anti-recursion comment,
and that no per-path asset name exists. CONTRIBUTING.md's Releasing now
says it plainly: merge is the ship decision; the tag is the fallback.

Fixes #111

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:21:07 +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
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
d3065cbc9a 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:53 +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