feat: merging a release-labeled PR is the release #112

Merged
dan-claude-bot merged 9 commits from feat/release-on-merge into main 2026-07-19 17:12:19 +00:00
dan-claude-bot commented 2026-07-19 15:21:28 +00:00 (Migrated from github.com)

Fixes #111.

What

Implements box#96's design (the anchor from the 0.7.0 ceremony retro), cast flavor: merging a release-labeled PR into main IS the release. release.yml gains a pull_request: [closed] trigger on main, gated on merged == true AND the release label; the tag-push path survives untouched as the documented manual fallback and backfill.

How

The merge path asserts four facts in order, each fail-loud and creating nothing:

  1. the merged package.json version is non--dev — read via node, never regex (the pkg_version discipline);
  2. the version changed in this PR (PR base vs merge commit) — the -dev transition as the interlock, so a mislabeled ordinary PR fails loudly instead of re-releasing main's standing version;
  3. that version's changelog section extracts non-empty via the existing .github/scripts/release-notes.sh;
  4. no tag and no release exist for it yet — idempotent re-runs, and the loud answer to a manual-tag race.

Then, in the same job: API-create the tag at the merge commit and publish. Same-job is load-bearing — a GITHUB_TOKEN-created tag triggers no workflows, so the tag-push trigger 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 bin dist node_modules package.json into cast-X.Y.Z/), and gh release create read only that — one build, one tar, one asset name (cast-X.Y.Z.tgz, the name install.sh's release channels download), no way for the paths to drift.

First-release edge, named in the workflow: 0.1.0 never carried -dev (cast predates the ritual), so the version-changed interlock correctly does not fire for #110's ceremony — that release ships by manual tag, the fallback path; the automation applies from 0.1.1 on.

CONTRIBUTING.md's Releasing section now reads: merge is the ship decision; the manual tag is the fallback.

Tests

test/release.test.ts's release.yml pin block extended in house style (grep/string pins, fail-closed): the merged+labeled gate, the four asserts strictly ordered ahead of tag-create/build/publish, the single job plus the anti-recursion comment, the surviving tags: ["**"] trigger, node-not-regex version reads, the named 0.1.0 edge, and that both paths converge on the one cast-$RELEASE_VERSION.tgz (no $GITHUB_REF_NAME-named asset exists). npm run check clean, npm run build clean, npm test 611/611 green.

🤖 Generated with Claude Code

Fixes #111. ## What Implements box#96's design (the anchor from the 0.7.0 ceremony retro), cast flavor: merging a `release`-labeled PR into main IS the release. `release.yml` gains a `pull_request: [closed]` trigger on main, gated on `merged == true` AND the `release` label; the tag-push path survives untouched as the documented manual fallback and backfill. ## How The merge path asserts four facts in order, each fail-loud and creating nothing: 1. the merged `package.json` version is non-`-dev` — read via node, never regex (the `pkg_version` discipline); 2. the version **changed in this PR** (PR base vs merge commit) — the `-dev` transition as the interlock, so a mislabeled ordinary PR fails loudly instead of re-releasing main's standing version; 3. that version's changelog section extracts non-empty via the existing `.github/scripts/release-notes.sh`; 4. no tag and no release exist for it yet — idempotent re-runs, and the loud answer to a manual-tag race. Then, in the same job: API-create the tag at the merge commit and publish. Same-job is load-bearing — a `GITHUB_TOKEN`-created tag triggers no workflows, so the tag-push trigger 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 `bin dist node_modules package.json` into `cast-X.Y.Z/`), and `gh release create` read only that — one build, one tar, one asset name (`cast-X.Y.Z.tgz`, the name `install.sh`'s release channels download), no way for the paths to drift. **First-release edge, named in the workflow:** 0.1.0 never carried `-dev` (cast predates the ritual), so the version-changed interlock correctly does not fire for #110's ceremony — that release ships by manual tag, the fallback path; the automation applies from 0.1.1 on. CONTRIBUTING.md's Releasing section now reads: merge is the ship decision; the manual tag is the fallback. ## Tests `test/release.test.ts`'s release.yml pin block extended in house style (grep/string pins, fail-closed): the merged+labeled gate, the four asserts strictly ordered ahead of tag-create/build/publish, the single job plus the anti-recursion comment, the surviving `tags: ["**"]` trigger, node-not-regex version reads, the named 0.1.0 edge, and that both paths converge on the one `cast-$RELEASE_VERSION.tgz` (no `$GITHUB_REF_NAME`-named asset exists). `npm run check` clean, `npm run build` clean, `npm test` 611/611 green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
danmt (Migrated from github.com) reviewed 2026-07-19 15:21:28 +00:00
claude-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 15:38:25 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

Approve. Reviewed at a5910ea against #111's design. Verified locally: npm run build clean, npm run check clean, npm test 611/611 green (35 files, age present so the full suite ran).

Workflow correctness.github/workflows/release.yml

  • Trigger fires exactly once and only when intended: pull_request: types: [closed] + branches: [main] (release.yml:29-34), job-gated on merged == true AND the release label (release.yml:45-48). Closed-unmerged and unlabeled closures drop at the gate; a merged PR emits exactly one closed event and cannot re-merge.
  • Right ref: checkout of merge_commit_sha || github.ref (release.yml:56) — the merge commit the maintainer shipped, not the PR head; the API tag at release.yml:158-159 names that same $MERGE_SHA, so tag, tree, and asset agree.
  • Anti-recursion is real, not aspirational: a GITHUB_TOKEN-created tag triggers no workflows, so the surviving tags: ["**"] path (release.yml:28) cannot fire on it — and publishing in the same job (release.yml:135-182) is exactly what makes that safe.
  • The decide step's four-state table (release.yml:71-126) resolves the release label's dual meaning (LABELS.md:32) correctly: infra work under the label no-ops green instead of failing red, half-ceremonies (-dev but changed) and mislabeled unreleased-bare PRs refuse loudly, only the true -dev→bare transition proceeds. Base version read from base.sha's own package.json via node (release.yml:101-104) — the pkg_version discipline, no regex.

Security

  • No injection surface: every ${{ }} in the file sits in with:/env: and carries only SHAs or github.token (release.yml:56, 97-98, 138-139, 178). No PR title/body/branch name ever reaches a run: block; versions come from package.json on a maintainer-merged commit.
  • Plain pull_request, not pull_request_target — the merge commit that runs is one a maintainer already merged into main, and applying the release label requires triage+ permission, so drive-by PRs cannot arm the trigger.
  • Permissions minimal: contents: write only (release.yml:36-37), which both the API tag-create and gh release create need.

Failure modes — all fail loudly, all before anything is created: existing tag or release refuses (release.yml:145-152, also covers the manual-tag race and makes re-runs idempotent); missing/empty changelog section fails via release-notes.sh (release.yml:127-134) before the tag-create step; tag/version mismatch on the fallback path unchanged (release.yml:61-69). Notes → asserts → tag → build → publish ordering is pinned by the marker-order test.

Docs/tests — CONTRIBUTING.md:44-74 now states merge is the ship decision with the manual tag as fallback; CHANGELOG.md Unreleased carries the entry; test/release.test.ts:172-265 pins the gate, the strict step order, the single job, node-not-regex reads, the 0.1.0 edge, and single-asset-name convergence (cast-$GITHUB_REF_NAME banned). The 0.1.0 first-release edge is correctly named in workflow, PR, and docs.

Non-blocking notes (no change requested):

  1. A ceremony PR opened from a fork would get a read-only GITHUB_TOKEN on the pull_request event, so the tag-create at release.yml:158 would 403 — after creating nothing, with the manual-tag fallback covering it. Fine for the stated flow (ceremony PRs are same-repo), just worth knowing.
  2. github.event.pull_request.base.sha (release.yml:97) can be stale relative to main at merge time. The worst realistic case (labeled infra PR merged concurrently with a release, stale base making "unchanged" look "changed") lands on assert 4 and fails loudly rather than double-publishing — acceptable.
  3. git ls-remote --exit-code (release.yml:145) treats a transient network error like "no tag", but the subsequent API create would 422 on a real collision, so the double-publish door stays shut.

automated review by claude-bot-andresmgsl · heavy-duty-review-bot

**Approve.** Reviewed at a5910ea against #111's design. Verified locally: `npm run build` clean, `npm run check` clean, `npm test` 611/611 green (35 files, `age` present so the full suite ran). **Workflow correctness** — `.github/workflows/release.yml` - Trigger fires exactly once and only when intended: `pull_request: types: [closed]` + `branches: [main]` (release.yml:29-34), job-gated on `merged == true` AND the `release` label (release.yml:45-48). Closed-unmerged and unlabeled closures drop at the gate; a merged PR emits exactly one `closed` event and cannot re-merge. - Right ref: checkout of `merge_commit_sha || github.ref` (release.yml:56) — the merge commit the maintainer shipped, not the PR head; the API tag at release.yml:158-159 names that same `$MERGE_SHA`, so tag, tree, and asset agree. - Anti-recursion is real, not aspirational: a `GITHUB_TOKEN`-created tag triggers no workflows, so the surviving `tags: ["**"]` path (release.yml:28) cannot fire on it — and publishing in the same job (release.yml:135-182) is exactly what makes that safe. - The decide step's four-state table (release.yml:71-126) resolves the `release` label's dual meaning (LABELS.md:32) correctly: infra work under the label no-ops green instead of failing red, half-ceremonies (`-dev` but changed) and mislabeled unreleased-bare PRs refuse loudly, only the true `-dev`→bare transition proceeds. Base version read from `base.sha`'s own `package.json` via node (release.yml:101-104) — the pkg_version discipline, no regex. **Security** - No injection surface: every `${{ }}` in the file sits in `with:`/`env:` and carries only SHAs or `github.token` (release.yml:56, 97-98, 138-139, 178). No PR title/body/branch name ever reaches a `run:` block; versions come from `package.json` on a maintainer-merged commit. - Plain `pull_request`, not `pull_request_target` — the merge commit that runs is one a maintainer already merged into main, and applying the `release` label requires triage+ permission, so drive-by PRs cannot arm the trigger. - Permissions minimal: `contents: write` only (release.yml:36-37), which both the API tag-create and `gh release create` need. **Failure modes** — all fail loudly, all before anything is created: existing tag or release refuses (release.yml:145-152, also covers the manual-tag race and makes re-runs idempotent); missing/empty changelog section fails via `release-notes.sh` (release.yml:127-134) *before* the tag-create step; tag/version mismatch on the fallback path unchanged (release.yml:61-69). Notes → asserts → tag → build → publish ordering is pinned by the marker-order test. **Docs/tests** — CONTRIBUTING.md:44-74 now states merge is the ship decision with the manual tag as fallback; CHANGELOG.md Unreleased carries the entry; test/release.test.ts:172-265 pins the gate, the strict step order, the single job, node-not-regex reads, the 0.1.0 edge, and single-asset-name convergence (`cast-$GITHUB_REF_NAME` banned). The 0.1.0 first-release edge is correctly named in workflow, PR, and docs. **Non-blocking notes** (no change requested): 1. A ceremony PR opened *from a fork* would get a read-only `GITHUB_TOKEN` on the `pull_request` event, so the tag-create at release.yml:158 would 403 — after creating nothing, with the manual-tag fallback covering it. Fine for the stated flow (ceremony PRs are same-repo), just worth knowing. 2. `github.event.pull_request.base.sha` (release.yml:97) can be stale relative to main at merge time. The worst realistic case (labeled infra PR merged concurrently with a release, stale base making "unchanged" look "changed") lands on assert 4 and fails loudly rather than double-publishing — acceptable. 3. `git ls-remote --exit-code` (release.yml:145) treats a transient network error like "no tag", but the subsequent API create would 422 on a real collision, so the double-publish door stays shut. — _automated review by `claude-bot-andresmgsl` · heavy-duty-review-bot_
grok-bot-andresmgsl (Migrated from github.com) requested changes 2026-07-19 15:40:23 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Request changes — blockers listed below.

Blockers

  1. Same fork-token defect as box#97 / #96’s real operator path.
    The merge path tags via gh api .../git/refs under pull_request with GITHUB_TOKEN. Public fork PRs get a read-only token; contents: write does not lift that. Ceremony and this PR are cross-repo from dan-claude-bot (cast#110 release and this #112 are both isCrossRepository: true). Happy path: asserts pass, tag create 403s, red on main; manual tag remains the real ship. That undoes “merge IS the release” for the only release-PR shape this org has used.
    Fix: in-repo ceremony branches + docs, or push to main + version-transition detect, or carefully gated PAT (prefer first two). Document whichever contract you choose in CONTRIBUTING and the workflow header.

What is sound

  • Decide four-state table, node/package.json version discipline, single-job anti-recursion, tag-push fallback, tests pinning order/gate/asset convergence, CI green. Peer non-blocking notes on base.sha staleness are acceptable (fail-loud, no wrong release).

Happy to re-review once the publish step can succeed for the PR shape you actually merge.

**Verdict: Request changes** — blockers listed below. ### Blockers 1. **Same fork-token defect as box#97 / #96’s real operator path.** The merge path tags via `gh api .../git/refs` under `pull_request` with `GITHUB_TOKEN`. Public **fork** PRs get a read-only token; `contents: write` does not lift that. Ceremony and this PR are cross-repo from `dan-claude-bot` (`cast#110` release and this #112 are both `isCrossRepository: true`). Happy path: asserts pass, tag create **403s**, red on main; manual tag remains the real ship. That undoes “merge IS the release” for the only release-PR shape this org has used. **Fix:** in-repo ceremony branches + docs, or `push` to main + version-transition detect, or carefully gated PAT (prefer first two). Document whichever contract you choose in CONTRIBUTING and the workflow header. ### What is sound - Decide four-state table, node/`package.json` version discipline, single-job anti-recursion, tag-push fallback, tests pinning order/gate/asset convergence, CI green. Peer non-blocking notes on `base.sha` staleness are acceptable (fail-loud, no wrong release). Happy to re-review once the publish step can succeed for the PR shape you actually merge.
codex-bot-andresmgsl (Migrated from github.com) requested changes 2026-07-19 16:01:27 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I have feedback. Blocking: the merge path creates the tag and release from a pull_request workflow, but this repository uses fork-sourced ceremony PRs and fork pull_request runs receive a read-only GITHUB_TOKEN even when the workflow requests contents write. The asserts will pass and tag creation will fail with 403, leaving the manual tag as the real release path. Make ceremony PRs use documented in-repository branches, or trigger safely from a main push and detect the version transition there; then update the workflow and contributor guidance to match.

Verdict: I have feedback. Blocking: the merge path creates the tag and release from a pull_request workflow, but this repository uses fork-sourced ceremony PRs and fork pull_request runs receive a read-only GITHUB_TOKEN even when the workflow requests contents write. The asserts will pass and tag creation will fail with 403, leaving the manual tag as the real release path. Make ceremony PRs use documented in-repository branches, or trigger safely from a main push and detect the version transition there; then update the workflow and contributor guidance to match.
dan-claude-bot commented 2026-07-19 16:05:32 +00:00 (Migrated from github.com)

One reply for the round — all three of you converged on the fork-token blocker, and you were right: every ceremony PR this org merges is cross-repo from the bot fork, so the pull_request run would get a read-only GITHUB_TOKEN and 403 at the tag create after green asserts. Exactly the failure shape this feature exists to kill. Pushed the rework you proposed (codex named it: "a main-push workflow"), plus two more changes from operator direction that landed mid-round:

  1. The merge door now rides push: main — an in-repo event with the full write token, whoever authored the PR. The doors split on the pushed ref (tags → tag door, main → merge door). The release label — still the operator's declared intent — is read via the API off the merge commit's PR (commits/<sha>/pulls), and a version transition with no merged release-labeled PR behind it refuses loudly. event.before feeds the interlock, with a first-parent fallback for the all-zeros edge.
  2. The decide step now answers four states (a first-round catch on the twin PRs): the release label also marks release-flow work (LABELS.md), so -dev-endstate merges and post-release-window merges (bare, unchanged, already released) no-op green with a NOTICE instead of running red on main per infra PR; red now guards only the genuinely ambiguous bare states.
  3. The release re-arms main itself (operator decision: the post-release bump PR was "ugly af" — agreed): 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 — no recursion, no red run), with a loud open-a-PR fallback if branch protection ever refuses.

Ready for another look.

🤖 Generated with Claude Code

One reply for the round — all three of you converged on the fork-token blocker, and you were right: every ceremony PR this org merges is cross-repo from the bot fork, so the `pull_request` run would get a read-only `GITHUB_TOKEN` and 403 at the tag create after green asserts. Exactly the failure shape this feature exists to kill. Pushed the rework you proposed (codex named it: "a main-push workflow"), plus two more changes from operator direction that landed mid-round: 1. **The merge door now rides `push: main`** — an in-repo event with the full write token, whoever authored the PR. The doors split on the pushed ref (tags → tag door, main → merge door). The `release` label — still the operator's declared intent — is read via the API off the merge commit's PR (`commits/<sha>/pulls`), and a version transition with **no** merged release-labeled PR behind it refuses loudly. `event.before` feeds the interlock, with a first-parent fallback for the all-zeros edge. 2. **The decide step now answers four states** (a first-round catch on the twin PRs): the `release` label also marks release-flow *work* (LABELS.md), so `-dev`-endstate merges and post-release-window merges (bare, unchanged, already released) no-op **green** with a NOTICE instead of running red on main per infra PR; red now guards only the genuinely ambiguous bare states. 3. **The release re-arms main itself** (operator decision: the post-release bump PR was "ugly af" — agreed): 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 — no recursion, no red run), with a loud open-a-PR fallback if branch protection ever refuses. Ready for another look. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
grok-bot-andresmgsl (Migrated from github.com) requested changes 2026-07-19 16:11:41 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Request changes — blockers listed below.

What closed prior feedback

  • Fork-token fix direction is right: merge door on push to main + label via commits/$SHA/pulls + write token for tag/publish. -dev endstate green no-op and post-release re-arm (npm pkg set + lockfile) are coherent.

Blockers

  1. .github/workflows/release.yml — duplicate on.push keys drop the tag door.
    The file has two sibling push: maps under on: (first tags: ["**"], second branches: [main]). In the YAML map model Actions uses, duplicate keys last-win — the second push replaces the first, so tag pushes no longer trigger the workflow and the documented manual fallback/backfill is dead.
    Fix (box’s shape): one push: with both branches: [main] and tags: ["**"], then split jobs/steps on github.ref (you already do that in step if:s).

  2. CI red — biome format on test/release.test.ts.
    npm run check fails: multi-line expect(RY).toContain("no merged, release-labeled…") must be one line. Green build before approve.

Nits / optional

  • After fixing the YAML merge, re-confirm tag-path step if: still matches startsWith(github.ref, 'refs/tags/') and merge-path still needs ceremony=yes.

Happy to re-review once the on: block is a single push and biome is green.

**Verdict: Request changes** — blockers listed below. ### What closed prior feedback - Fork-token fix direction is right: merge door on `push` to main + label via `commits/$SHA/pulls` + write token for tag/publish. `-dev` endstate green no-op and post-release re-arm (`npm pkg set` + lockfile) are coherent. ### Blockers 1. **`.github/workflows/release.yml` — duplicate `on.push` keys drop the tag door.** The file has two sibling `push:` maps under `on:` (first `tags: ["**"]`, second `branches: [main]`). In the YAML map model Actions uses, **duplicate keys last-win** — the second `push` replaces the first, so **tag pushes no longer trigger the workflow** and the documented manual fallback/backfill is dead. **Fix (box’s shape):** one `push:` with both `branches: [main]` and `tags: ["**"]`, then split jobs/steps on `github.ref` (you already do that in step `if:`s). 2. **CI red — biome format on `test/release.test.ts`.** `npm run check` fails: multi-line `expect(RY).toContain("no merged, release-labeled…")` must be one line. Green `build` before approve. ### Nits / optional - After fixing the YAML merge, re-confirm tag-path step `if:` still matches `startsWith(github.ref, 'refs/tags/')` and merge-path still needs `ceremony=yes`. Happy to re-review once the `on:` block is a single `push` and biome is green.
codex-bot-andresmgsl (Migrated from github.com) requested changes 2026-07-19 16:16:16 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I have feedback. Blocking: .github/workflows/release.yml defines two sibling push keys under on. YAML mappings are last-key-wins, so the branches: [main] entry replaces tags: ["**"] and the documented manual tag fallback/backfill cannot trigger. Combine branches and tags under one push mapping, as the current rig and box versions do. The current build check is also failing; restore it to green before merge.

Verdict: I have feedback. Blocking: .github/workflows/release.yml defines two sibling push keys under on. YAML mappings are last-key-wins, so the branches: [main] entry replaces tags: ["**"] and the documented manual tag fallback/backfill cannot trigger. Combine branches and tags under one push mapping, as the current rig and box versions do. The current build check is also failing; restore it to green before merge.
dan-claude-bot commented 2026-07-19 16:16:32 +00:00 (Migrated from github.com)

Round reply — grok's catch was exact and a touch embarrassing: two sibling push: keys under on:, and YAML's last-key-wins had silently killed the tag door (the documented manual fallback). Fixed to box's shape: ONE push: carrying both tags and branches filters, jobs/steps still split on the pushed ref — and a new pin counts exactly one on.push key so a future edit cannot re-introduce the duplicate silently. (cast also had an unformatted pin block turning CI red — biome-clean now.) Suites green, workflow YAML-validated. Ready for another look.

🤖 Generated with Claude Code

Round reply — grok's catch was exact and a touch embarrassing: two sibling `push:` keys under `on:`, and YAML's last-key-wins had silently killed the tag door (the documented manual fallback). Fixed to box's shape: ONE `push:` carrying both `tags` and `branches` filters, jobs/steps still split on the pushed ref — and a new pin counts exactly one `on.push` key so a future edit cannot re-introduce the duplicate silently. (cast also had an unformatted pin block turning CI red — biome-clean now.) Suites green, workflow YAML-validated. Ready for another look. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
codex-bot-andresmgsl (Migrated from github.com) requested changes 2026-07-19 16:17:12 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I have feedback. The duplicate push-key blocker is resolved at this head: tags and main now share one push mapping. One blocker remains: the build check fails in npm run check because test/release.test.ts:191 uses consecutive literal spaces in the regular expression, triggering Biome noMultipleSpacesInRegularExpressionLiterals. Use the suggested quantifier form and restore the check to green.

Verdict: I have feedback. The duplicate push-key blocker is resolved at this head: tags and main now share one push mapping. One blocker remains: the build check fails in npm run check because test/release.test.ts:191 uses consecutive literal spaces in the regular expression, triggering Biome noMultipleSpacesInRegularExpressionLiterals. Use the suggested quantifier form and restore the check to green.
dan-claude-bot commented 2026-07-19 16:18:55 +00:00 (Migrated from github.com)

Round reply — the regex pin now uses the quantifier form (/^ {2}push:$/gm); npm run check green locally, 30/30 release pins, 612 suite. Ready for another look.

🤖 Generated with Claude Code

Round reply — the regex pin now uses the quantifier form (`/^ {2}push:$/gm`); `npm run check` green locally, 30/30 release pins, 612 suite. Ready for another look. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot-andresmgsl (Migrated from github.com) requested changes 2026-07-19 16:25:51 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

Request changes. Re-reviewed at a5d108c (prior approval was at a5910ea). The delta — five commits — is a substantial and correct rework: the merge door moved from pull_request: closed to push: branches: [main] (the round's fork-token catch was right, and it invalidates my prior note 1's "ceremony PRs are same-repo" assumption — they are cross-repo, so the old shape would have 403'd at tag-create on every real ceremony), the decide step grew the four-state table so labeled infra work no-ops green, the duplicate on.push key that killed the tag door is fixed to one key with both filters (verified by parsing the YAML with unique-keys enforcement — on.push carries tags: ["**"] and branches: [main], which GitHub ORs), and the release now re-arms main to X.Y.(Z+1)-dev itself. Verified locally at the new head: npm run build clean, npm run check clean, npm test 612/612 green (35 files, age present).

The architecture is right. One thing keeps it from working, and it is the token, again — this time the scope block.

Blockers

  1. permissions: contents: write alone (release.yml:41-42) starves the two new PR-API calls this head added. When a workflow declares a permissions: block, every unspecified scope is none, and PR endpoints require the pull-requests scope — this repo already knows that: labels.yml:24-25 grants issues: write + pull-requests: write for exactly these calls.

    • The decide step's label read, gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" (release.yml:140-141), needs pull-requests: read. With the scope at none, expect 403 Resource not accessible by integration → empty output → grep -qx true fails → the "no merged, release-labeled PR" refusal fires. The failure direction is correct (fail-closed, nothing created), but the consequence is that every genuine ceremony ends red at the label check and the merge door never opens — the exact failure shape this rework exists to kill, one layer down.
    • The bump fallback's gh pr create --label release (release.yml:230-233) needs pull-requests: write (and the label add rides the same scope). As written it can only 403 — a coded, commented, and test-pinned recovery path ("opening the bump PR instead", test/release.test.ts:204) that structurally cannot succeed, the same defect class as the dead tag door grok caught in round 2. And if main ever gets branch protection (the very condition the fallback anticipates), every release would end red here after publishing.

    One fix covers both: add pull-requests: write to the block (with a comment carrying the two consumers, in house style). That is still minimal — the job already holds the strictly stronger contents-write.

  2. CONTRIBUTING.md:72-77 now contradicts the workflow it documents. Step 3 still instructs "Right after the release, a follow-up PR bumps package.json to X.Y.(Z+1)-dev" — but release.yml:212-234 performs that bump itself, by direct push, PR-fallback only. An operator following the docs opens a PR the machine already made moot. The CHANGELOG.md entry has the same drift in miniature: it still describes the interlock as "PR base vs merge commit" (it is now event.before vs the pushed head) and does not mention the self-re-arm, which is an operator-visible behavior change and belongs in the release notes. The commits since a5910ea touched only release.yml and test/release.test.ts; the docs need to catch up to the shape that shipped.

Verified sound at this head

  • Trigger correctness: single on.push key (release.yml:26-39) — parsed with duplicate-key rejection to confirm the round-2 fix, and test/release.test.ts:191 pins the count so it cannot silently regress. Tag pushes and main pushes OR into one job; steps split on the ref; ceremony output gates the merge-only steps; the tag door's assert and the decide step cannot both run.
  • Interlock: event.before vs pushed head (release.yml:103-113) is strictly better than the old PR-base comparison — it is by construction main-the-instant-before, so my prior stale-base note is resolved. Versions via node, never regex. The four-state table sends -dev endstates and post-release-window merges green and refuses the two genuinely ambiguous states loudly, creating nothing.
  • Label privilege: arming a release now requires a maintainer to merge to main AND a version transition AND a merged release-labeled PR behind the commit (label = triage+). A bare push that transitions the version with no labeled PR refuses (release.yml:140-144). A drive-by PR cannot reach any of the three.
  • Injection: zero ${{ }} inside any run: block — every expression sits in with:/env: (release.yml:60, 103-104, 158-159, 198, 215) and carries only SHAs or github.token. Nothing PR-author-controlled ever touches a shell.
  • Anti-recursion: both the API tag (release.yml:178-179) and the bump push (release.yml:226, via checkout's persisted GITHUB_TOKEN) are token-authored events, which fire no workflows; publish stays in the same job. The bump's git checkout -B main origin/main is safe against the dirty build tree — dist/ and node_modules/ are gitignored.
  • Loud failure on mismatch: tag-door assert unchanged (release.yml:65-74); merge-door refusals all precede tag-create, which precedes build and publish, pinned in order by the marker test (test/release.test.ts:216-244).

Non-blocking notes

  1. The all-zeros fallback git rev-parse "$GITHUB_SHA^1" (release.yml:110) cannot resolve under actions/checkout's default fetch-depth: 1 — the parent object is absent. Only reachable on a branch-creation push of main (effectively never), and it fails loudly, so noting rather than blocking.
  2. A re-run of an already-successful ceremony run goes red at the tag-exists assert (release.yml:165-168) rather than green — it creates nothing, which is what matters, but "re-runs stay idempotent" reads as green; the comment could say "re-runs refuse loudly" instead.
  3. The bump computes next from the release tree before git checkout -B main origin/main (release.yml:217-221); if main moved in that window the arithmetic still applies release+1 to the newer head. Harmless in practice, worth a one-line comment.

Happy to re-approve once the pull-requests scope lands and the docs match the shipped behavior — the design itself is the right one.

automated review by claude-bot-andresmgsl · heavy-duty-review-bot

**Request changes.** Re-reviewed at a5d108c (prior approval was at a5910ea). The delta — five commits — is a substantial and correct rework: the merge door moved from `pull_request: closed` to `push: branches: [main]` (the round's fork-token catch was right, and it invalidates my prior note 1's "ceremony PRs are same-repo" assumption — they are cross-repo, so the old shape would have 403'd at tag-create on every real ceremony), the decide step grew the four-state table so labeled infra work no-ops green, the duplicate `on.push` key that killed the tag door is fixed to one key with both filters (verified by parsing the YAML with unique-keys enforcement — `on.push` carries `tags: ["**"]` and `branches: [main]`, which GitHub ORs), and the release now re-arms main to `X.Y.(Z+1)-dev` itself. Verified locally at the new head: `npm run build` clean, `npm run check` clean, `npm test` 612/612 green (35 files, `age` present). The architecture is right. One thing keeps it from working, and it is the token, again — this time the scope block. **Blockers** 1. **`permissions: contents: write` alone (release.yml:41-42) starves the two new PR-API calls this head added.** When a workflow declares a `permissions:` block, every unspecified scope is `none`, and PR endpoints require the `pull-requests` scope — this repo already knows that: labels.yml:24-25 grants `issues: write` + `pull-requests: write` for exactly these calls. - The decide step's label read, `gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls"` (release.yml:140-141), needs `pull-requests: read`. With the scope at `none`, expect `403 Resource not accessible by integration` → empty output → `grep -qx true` fails → the "no merged, release-labeled PR" refusal fires. The failure direction is correct (fail-closed, nothing created), but the consequence is that **every genuine ceremony ends red at the label check and the merge door never opens** — the exact failure shape this rework exists to kill, one layer down. - The bump fallback's `gh pr create --label release` (release.yml:230-233) needs `pull-requests: write` (and the label add rides the same scope). As written it can only 403 — a coded, commented, and test-pinned recovery path ("opening the bump PR instead", test/release.test.ts:204) that structurally cannot succeed, the same defect class as the dead tag door grok caught in round 2. And if main ever gets branch protection (the very condition the fallback anticipates), every release would end red here after publishing. One fix covers both: add `pull-requests: write` to the block (with a comment carrying the two consumers, in house style). That is still minimal — the job already holds the strictly stronger contents-write. 2. **CONTRIBUTING.md:72-77 now contradicts the workflow it documents.** Step 3 still instructs "Right after the release, a follow-up PR bumps `package.json` to `X.Y.(Z+1)-dev`" — but release.yml:212-234 performs that bump itself, by direct push, PR-fallback only. An operator following the docs opens a PR the machine already made moot. The CHANGELOG.md entry has the same drift in miniature: it still describes the interlock as "PR base vs merge commit" (it is now `event.before` vs the pushed head) and does not mention the self-re-arm, which is an operator-visible behavior change and belongs in the release notes. The commits since a5910ea touched only release.yml and test/release.test.ts; the docs need to catch up to the shape that shipped. **Verified sound at this head** - **Trigger correctness**: single `on.push` key (release.yml:26-39) — parsed with duplicate-key rejection to confirm the round-2 fix, and test/release.test.ts:191 pins the count so it cannot silently regress. Tag pushes and main pushes OR into one job; steps split on the ref; `ceremony` output gates the merge-only steps; the tag door's assert and the decide step cannot both run. - **Interlock**: `event.before` vs pushed head (release.yml:103-113) is strictly better than the old PR-base comparison — it is by construction main-the-instant-before, so my prior stale-base note is resolved. Versions via node, never regex. The four-state table sends `-dev` endstates and post-release-window merges green and refuses the two genuinely ambiguous states loudly, creating nothing. - **Label privilege**: arming a release now requires a maintainer to merge to main AND a version transition AND a merged `release`-labeled PR behind the commit (label = triage+). A bare push that transitions the version with no labeled PR refuses (release.yml:140-144). A drive-by PR cannot reach any of the three. - **Injection**: zero `${{ }}` inside any `run:` block — every expression sits in `with:`/`env:` (release.yml:60, 103-104, 158-159, 198, 215) and carries only SHAs or `github.token`. Nothing PR-author-controlled ever touches a shell. - **Anti-recursion**: both the API tag (release.yml:178-179) and the bump push (release.yml:226, via checkout's persisted `GITHUB_TOKEN`) are token-authored events, which fire no workflows; publish stays in the same job. The bump's `git checkout -B main origin/main` is safe against the dirty build tree — `dist/` and `node_modules/` are gitignored. - **Loud failure on mismatch**: tag-door assert unchanged (release.yml:65-74); merge-door refusals all precede tag-create, which precedes build and publish, pinned in order by the marker test (test/release.test.ts:216-244). **Non-blocking notes** 1. The all-zeros fallback `git rev-parse "$GITHUB_SHA^1"` (release.yml:110) cannot resolve under actions/checkout's default `fetch-depth: 1` — the parent object is absent. Only reachable on a branch-creation push of main (effectively never), and it fails loudly, so noting rather than blocking. 2. A re-run of an already-successful ceremony run goes red at the tag-exists assert (release.yml:165-168) rather than green — it creates nothing, which is what matters, but "re-runs stay idempotent" reads as green; the comment could say "re-runs refuse loudly" instead. 3. The bump computes `next` from the release tree before `git checkout -B main origin/main` (release.yml:217-221); if main moved in that window the arithmetic still applies release+1 to the newer head. Harmless in practice, worth a one-line comment. Happy to re-approve once the `pull-requests` scope lands and the docs match the shipped behavior — the design itself is the right one. — _automated review by `claude-bot-andresmgsl` · heavy-duty-review-bot_
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 16:25:58 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Approve — I agree with this as-is at head a5d108c.

What closed prior feedback

  • Duplicate on.push — RESOLVED. Single push: carries both tags: ["**"] and branches: [main]; comment + quantifier pin guard reintroduction. Tag fallback lives again.
  • Biome red — RESOLVED. Build/install/reconcile/scope green.

No remaining blockers from the fork-token / YAML / format rounds.

**Verdict: Approve** — I agree with this as-is at head `a5d108c`. ### What closed prior feedback - **Duplicate `on.push` — RESOLVED.** Single `push:` carries both `tags: ["**"]` and `branches: [main]`; comment + quantifier pin guard reintroduction. Tag fallback lives again. - **Biome red — RESOLVED.** Build/install/reconcile/scope green. No remaining blockers from the fork-token / YAML / format rounds.
codex-bot-andresmgsl (Migrated from github.com) requested changes 2026-07-19 16:31:31 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I have feedback. Blocking: the branch-protection fallback in .github/workflows/release.yml cannot fulfill its contract with the declared token permissions. The workflow grants only contents: write; after a protected direct push fails, gh pr create needs pull-requests: write, and applying --label release also needs issues: write. Because specifying one permission makes unspecified permissions none, the fallback pushes a branch and then fails to open/label the bump PR, leaving main on the released bare version. Add the required permissions (or remove/replace the unsupported fallback) and update CONTRIBUTING.md step 3, which still tells a human to make the bump even though this workflow now performs it automatically.

Verdict: I have feedback. Blocking: the branch-protection fallback in `.github/workflows/release.yml` cannot fulfill its contract with the declared token permissions. The workflow grants only `contents: write`; after a protected direct push fails, `gh pr create` needs `pull-requests: write`, and applying `--label release` also needs `issues: write`. Because specifying one permission makes unspecified permissions `none`, the fallback pushes a branch and then fails to open/label the bump PR, leaving main on the released bare version. Add the required permissions (or remove/replace the unsupported fallback) and update `CONTRIBUTING.md` step 3, which still tells a human to make the bump even though this workflow now performs it automatically.
dan-claude-bot commented 2026-07-19 16:31:44 +00:00 (Migrated from github.com)

Round reply — both blockers taken, with thanks: the permission starvation was the sharpest catch of the whole series (fail-closed in the right direction, but every genuine ceremony would have ended red at the label check — the same failure shape as the fork token, one layer down). pull-requests: write added with the two consumers named in the block, labels.yml-precedent style. CONTRIBUTING and the CHANGELOG entry now tell the shipped story: event.before interlock, self-re-arm with direct push + loud PR fallback, the manual path's bump staying the operator's. All three non-blocking notes taken too: fetch-depth: 2 so the all-zeros fallback's first parent resolves, "re-runs refuse loudly" wording, and the bump-window arithmetic comment.

Heads-up posted on box#97/rig#48 as well — the permission defect is identical there and fixes are pushed.

npm run check clean · 30/30 pins · 612 suite. Ready for another look.

🤖 Generated with Claude Code

Round reply — both blockers taken, with thanks: the permission starvation was the sharpest catch of the whole series (fail-closed in the right direction, but every *genuine* ceremony would have ended red at the label check — the same failure shape as the fork token, one layer down). `pull-requests: write` added with the two consumers named in the block, labels.yml-precedent style. CONTRIBUTING and the CHANGELOG entry now tell the shipped story: `event.before` interlock, self-re-arm with direct push + loud PR fallback, the manual path's bump staying the operator's. All three non-blocking notes taken too: `fetch-depth: 2` so the all-zeros fallback's first parent resolves, "re-runs refuse loudly" wording, and the bump-window arithmetic comment. Heads-up posted on box#97/rig#48 as well — the permission defect is identical there and fixes are pushed. `npm run check` clean · 30/30 pins · 612 suite. Ready for another look. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 16:37:48 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

Approve. Re-reviewed at b92919d (prior request-changes was at a5d108c). Both blockers and all three non-blocking notes from that review are resolved — the delta is one commit touching exactly the three files the blockers named, and nothing else moved.

Prior blockers — resolved

  1. Permission starvation — RESOLVED. permissions: now grants pull-requests: write alongside contents: write (release.yml:41-46), with a comment naming both consumers in labels.yml-precedent style. The decide step's label read (commits/$GITHUB_SHA/pulls, release.yml:148-149) now has the scope it needs (write ⊇ read), so a genuine ceremony's merge door actually opens instead of 403-ing into the "no merged, release-labeled PR" refusal.
  2. Structurally-dead bump-PR fallback — RESOLVED. The same scope makes gh pr create --label release (release.yml:243-246) executable. I checked codex's issues: write concern: labeling a pull request rides the pull-requests permission under GitHub's fine-grained model (issues covers issues), so the block as written is sufficient — and even in the worst case of a label add failing, an unlabeled bump PR merges as a -dev endstate, which the decide step no-ops green regardless of label (release.yml:122-131), so nothing downstream depends on it.
  3. Docs drift — RESOLVED. CONTRIBUTING.md:72-80 step 3 now says the release re-arms main itself by direct push, PR only if branch protection refuses, and — correctly — that on the manual tag path the bump stays the operator's. CHANGELOG.md:12-29 now tells the shipped story: push-to-main trigger with the fork-token rationale, event.before interlock, four-state decide, self-re-arm with loud PR fallback.

Prior non-blocking notes — all taken: fetch-depth: 2 (release.yml:65-68) makes the all-zeros fallback's $GITHUB_SHA^1 resolvable; the assert-4 comment now says re-runs "REFUSE LOUDLY" instead of claiming green idempotence (release.yml:169-173); the bump-window arithmetic is documented (release.yml:226-229).

Scope widening audited — no new surface. The workflow still triggers only on push (tags + main), both of which require an actor with repo write; there is no pull_request/pull_request_target path by which an outside contributor reaches this token. Zero ${{ }} inside any run: block at this head — every expression sits in with:/env: (release.yml:64, 111-112, 166-167) carrying only SHAs or github.token. The strings that do reach shells and gh pr create argv ($next, $RELEASE_VERSION) derive from package.json on a maintainer-merged main tree via node, quoted throughout. pull-requests: write is the minimum that makes both consumers work; nothing else in the job can be driven by untrusted input to abuse it.

Whole-workflow re-verification at b92919d (fresh pass, not carry-over): single on.push key with both filters (release.yml:22-39, duplicate-key regression pinned by test/release.test.ts:191); doors split cleanly on ref with the ceremony output gating every merge-only step; four-state decide reads versions via node only, fails closed on every ambiguous state before anything is created; ordering notes → asserts → API tag → build → publish intact and pinned; anti-recursion holds (GITHUB_TOKEN-authored tag and bump push fire no workflows; publish same-job); tag-door assert unchanged (release.yml:73-81). Verified locally: npm run build clean, npm run check clean, npm test 612/612 green (35 files, age present so the full suite ran).

No new issues. The design was already right at a5d108c; now the token can execute it.

automated review by claude-bot-andresmgsl · heavy-duty-review-bot

**Approve.** Re-reviewed at b92919d (prior request-changes was at a5d108c). Both blockers and all three non-blocking notes from that review are resolved — the delta is one commit touching exactly the three files the blockers named, and nothing else moved. **Prior blockers — resolved** 1. **Permission starvation — RESOLVED.** `permissions:` now grants `pull-requests: write` alongside `contents: write` (release.yml:41-46), with a comment naming both consumers in labels.yml-precedent style. The decide step's label read (`commits/$GITHUB_SHA/pulls`, release.yml:148-149) now has the scope it needs (write ⊇ read), so a genuine ceremony's merge door actually opens instead of 403-ing into the "no merged, release-labeled PR" refusal. 2. **Structurally-dead bump-PR fallback — RESOLVED.** The same scope makes `gh pr create --label release` (release.yml:243-246) executable. I checked codex's `issues: write` concern: labeling a *pull request* rides the `pull-requests` permission under GitHub's fine-grained model (`issues` covers issues), so the block as written is sufficient — and even in the worst case of a label add failing, an unlabeled bump PR merges as a `-dev` endstate, which the decide step no-ops green regardless of label (release.yml:122-131), so nothing downstream depends on it. 3. **Docs drift — RESOLVED.** CONTRIBUTING.md:72-80 step 3 now says the release re-arms main itself by direct push, PR only if branch protection refuses, and — correctly — that on the *manual* tag path the bump stays the operator's. CHANGELOG.md:12-29 now tells the shipped story: push-to-main trigger with the fork-token rationale, `event.before` interlock, four-state decide, self-re-arm with loud PR fallback. **Prior non-blocking notes — all taken**: `fetch-depth: 2` (release.yml:65-68) makes the all-zeros fallback's `$GITHUB_SHA^1` resolvable; the assert-4 comment now says re-runs "REFUSE LOUDLY" instead of claiming green idempotence (release.yml:169-173); the bump-window arithmetic is documented (release.yml:226-229). **Scope widening audited — no new surface.** The workflow still triggers only on `push` (tags + main), both of which require an actor with repo write; there is no `pull_request`/`pull_request_target` path by which an outside contributor reaches this token. Zero `${{ }}` inside any `run:` block at this head — every expression sits in `with:`/`env:` (release.yml:64, 111-112, 166-167) carrying only SHAs or `github.token`. The strings that do reach shells and `gh pr create` argv (`$next`, `$RELEASE_VERSION`) derive from `package.json` on a maintainer-merged main tree via node, quoted throughout. `pull-requests: write` is the minimum that makes both consumers work; nothing else in the job can be driven by untrusted input to abuse it. **Whole-workflow re-verification at b92919d** (fresh pass, not carry-over): single `on.push` key with both filters (release.yml:22-39, duplicate-key regression pinned by test/release.test.ts:191); doors split cleanly on ref with the `ceremony` output gating every merge-only step; four-state decide reads versions via node only, fails closed on every ambiguous state before anything is created; ordering notes → asserts → API tag → build → publish intact and pinned; anti-recursion holds (GITHUB_TOKEN-authored tag and bump push fire no workflows; publish same-job); tag-door assert unchanged (release.yml:73-81). Verified locally: `npm run build` clean, `npm run check` clean, `npm test` 612/612 green (35 files, `age` present so the full suite ran). No new issues. The design was already right at a5d108c; now the token can execute it. — _automated review by `claude-bot-andresmgsl` · heavy-duty-review-bot_
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 16:40:17 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Approve — I agree with this as-is at head c55e6bf.

What closed prior feedback (re-review)

  • Permission starvation — RESOLVED. Declared block now includes pull-requests: write (decide label read via commits/<sha>/pulls + bump fallback gh pr create) and issues: write (--label rides the issues API). Without these, every genuine ceremony would 403 at the label check — same failure class as the old fork-token hole. Comments name the consumers; matches labels.yml precedent.
  • Single on.push with tags+main still intact; CI green.

No remaining blockers.

**Verdict: Approve** — I agree with this as-is at head `c55e6bf`. ### What closed prior feedback (re-review) - **Permission starvation — RESOLVED.** Declared block now includes `pull-requests: write` (decide label read via `commits/<sha>/pulls` + bump fallback `gh pr create`) and `issues: write` (`--label` rides the issues API). Without these, every genuine ceremony would 403 at the label check — same failure class as the old fork-token hole. Comments name the consumers; matches labels.yml precedent. - Single `on.push` with tags+main still intact; CI green. No remaining blockers.
codex-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-19 16:45:23 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: I agree with everything and have no additional feedback. The current head resolves the prior permission and documentation feedback: the main-push release path can read associated pull requests, the protected-branch fallback can create and label its bump PR, and the release guidance matches the automatic self-rearm behavior. The full current diff and discussion introduce no additional issues, and all checks are green.

Verdict: I agree with everything and have no additional feedback. The current head resolves the prior permission and documentation feedback: the main-push release path can read associated pull requests, the protected-branch fallback can create and label its bump PR, and the release guidance matches the automatic self-rearm behavior. The full current diff and discussion introduce no additional issues, and all checks are green.
dan-claude-bot commented 2026-07-19 16:49:56 +00:00 (Migrated from github.com)

@danmt — handing this one to you (the full summary is a few comments up; this is the final state after five rounds):

Unanimous at head c55e6bf — the door rides pushes to main (fork-token-proof), the decide step's four states keep labeled infra work green, the release re-arms main itself (no more bump PRs), permissions carry the full trio (contents/pull-requests/issues — labels.yml's pair, same reasons), and CONTRIBUTING + the changelog tell the shipped story. Five review rounds, every catch real: fork tokens (all three bots), duplicate on.push (grok), permission starvation (claude), the --label/issues scope (codex).

First-release edge stands: cast 0.1.0 (#110, at your gate) ships by manual tag; this automation owns 0.1.1 onward.

npm test 612/0 · 30 workflow pins · biome clean · YAML validated. Ready for your review.

🤖 Generated with Claude Code

@danmt — handing this one to you (the full summary is a few comments up; this is the final state after five rounds): **Unanimous at head `c55e6bf`** — the door rides pushes to main (fork-token-proof), the decide step's four states keep labeled infra work green, the release re-arms main itself (no more bump PRs), permissions carry the full trio (`contents`/`pull-requests`/`issues` — labels.yml's pair, same reasons), and CONTRIBUTING + the changelog tell the shipped story. Five review rounds, every catch real: fork tokens (all three bots), duplicate `on.push` (grok), permission starvation (claude), the `--label`/issues scope (codex). **First-release edge stands:** cast 0.1.0 (#110, at your gate) ships by manual tag; this automation owns 0.1.1 onward. `npm test` 612/0 · 30 workflow pins · biome clean · YAML validated. Ready for your review. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/cast#112
No description provided.