agentleFS
lukasrepubliclukasrepublic/agentic-foundry/skills/cut-release/SKILL.md

cut-release

Skill1 starsChanged 3 months agoMITView on GitHub

Cut an agentic-foundry release as a guarded playbook (/foundry:cut-release). Encodes the hand-run cut procedure as a loop whose EXIT GATE is the existing acceptance verdict — verifies the ordered preconditions (plugin.json version, CHANGELOG section, clean tree, and the candidate tree's own test suite; note the preflight TOLERATES a marketplace.json catalogue version left at the preceding release but the acceptance gate HARD-STOPS on it, so the catalogue bumps in R and only source.sha defers to the re-pin commit R2), refuses to emit any publish plan until run_acceptance returns pass, and only afterward emits the gotcha-correct publish plan (re-pin marketplace source.sha to the release commit → annotated tag on the re-pin commit → machine-verify the tag → push, never force) WITHOUT pushing. Trigger when the operator is cutting/releasing a version — "cut a release", "release v0.6.1", "/foundry:cut-release", "ship the release".

  • Commits and pushes

36 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use quota-axi, iterate-pr and 6 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • layer5ioSkill

    quota-axi

    Report local Claude, Codex, Cursor, GitHub Copilot, and Grok quota windows via the quota-axi CLI - remaining percentages, reset times, and provider status read from local auth sources, with no routing, recommendation, or provider mutation. Use before deciding whether it is safe to keep spending a provider's quota, when the user asks about usage, rate limits, or remaining quota, or when comparing local provider headroom.

    Also in 4 projects that use this

  • SentrySkill

    iterate-pr

    Iterate on a PR until actionable CI passes and high/medium review feedback is addressed. Use for PR CI failures, review feedback, or green-check loops; do not wait for human approval, draft status, or merge gates.

    Also in 4 projects that use this

  • layer5ioSkill

    gh-axi

    Operate GitHub through the gh-axi CLI - issues, pull requests, workflow runs, workflows, releases, repositories, labels, Projects (v2), Actions secrets and variables, search, and raw API access. Use whenever a task touches GitHub: listing or filing issues, reviewing or merging PRs, checking CI runs, triggering workflows, cutting releases, managing Projects boards, or managing Actions secrets/variables.

    Also in 4 projects that use this

  • kunchenguidSkill

    chrome-devtools-axi

    Control a Chrome browser session through the chrome-devtools-axi CLI - navigate, snapshot, click, fill forms, run JavaScript, inspect console and network, take screenshots, audit performance. Use whenever a task needs a real browser: opening or testing a web page, clicking through a flow, extracting page content, or debugging a website.

    Also in 4 projects that use this

  • kunchenguidSkill

    lavish

    Turn complex or visual agent responses into rich, reviewable HTML artifacts (HTML files) the user can annotate and send feedback on, using the lavish-axi CLI. Use when about to give a plan, comparison, diagram, table, code diff, report, or anything easier to grasp visually than as prose.

    Also in 4 projects that use this

  • ThisIsSadeghiSkill

    verify-ui

    Verify UI implementation against Stitch design via a token audit (HTML ↔ Code, with X-component default-render checks) and an X-components compliance check.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

8 rules. 0 are common, 1 is distinctive, and 7 are very specific to this project.

  • 0

    Common

    Most of the 36 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Commit it path-scoped: git commit -m '…' -- .claude-plugin/marketplace.json.

    line 189
  2. Very specific:

    Run it from inside the workspace directory.

    line 236
  3. Very specific:

    Every claude invocation it makes is drawn from one frozen, closed six-subcommand allowlist.

    line 257
  4. Very specific:

    Do this ONCE per affected scope.

    line 309
  5. Very specific:

    Verify by READING the refreshed cache — ~/.claude/plugins/marketplaces/<marketplace>/.claude-plugin/marketplace.json must show the new …

    line 312
  6. Distinctive:

    Do not bulk-push changes into external-org / dirty / feature-branch repos.

    line 328
  7. Very specific:

    The OPEN-ER advisory: When gh is reachable, READY prints which traced ERs are still OPEN at cut time (the ones the reconciliation targets).

    line 346
  8. Very specific:

    Every atom's own PR targets release/<version>, not main — /foundry:merge-when-green refuses the wrong base when --release <id> is passed …

    line 362

“Common” is measured: kept by at least 6 in 10 of the 36 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

/foundry:cut-release — the cut-release playbook

The FIRST foundry playbook (a guarded, looping skill — task-triggered, loop held by an EXIT GATE, floors orthogonal; see docs/TERMINOLOGY.md). It turns the release-cut procedure — which lived only as memory-prose and was hand-run ~6× (every recurring gotcha was that procedure failing) — into a self-contained, gated loop driven by scripts/foundry-cut-release.py.

Its EXIT GATE is the EXISTING acceptance verdict (foundry-release-acceptance.run_acceptance) — the only gate this script invokes; it reimplements no gate logic. The closure derivation (foundry_release.py) is upstream prep the operator runs before the cut, not invoked here. It adds no new floor and never runs git tag / git push — it emits the publish plan as data and the operator executes it.

Retired: the wiring-pin migrate. This loop once named a foundry-wiring-hash.py wiring-pin migrate step, re-checked transitively via DOCTOR-GREEN. That mechanism — scripts/foundry-wiring-hash.py, the .foundry/wiring-hash.pin file, and the doctor's wiring-hash check — was retired; the current foundry-doctor.py (a thin 5-check probe, skills/doctor/SKILL.md) carries no wiring-hash check to re-check. What carries the load today is the foundry-release-acceptance gate (run_acceptance) this loop's EXIT GATE already calls. See skills/release/SKILL.md's "Gate-wiring pin & manifest" tombstone for the fuller history.

The playbook shape (the standard template — see docs/TERMINOLOGY.md §6)

  • TRIGGER — the operator is cutting/shipping a release (description-matched; no global pointer).
  • ENGINE — deterministic-seam-drive: ordered prep-check → machine gate → emit plan.
  • EXIT GATE — foundry-release-acceptance.py run_acceptance(tree) == pass (the artifact-sha-bound verdict). The loop is NOT "done" until the gate is green; nothing reaches tag/push before that.
  • FLOORS TOUCHED — none new. The orthogonal floors (front-authorization, the per-atom merge floor, the acceptance HARD-STOP) already fired upstream; cut-release only sequences and refuses-until-green. It never folds a floor into a step.

The loop

   run foundry-cut-release.py --tree <repo> --version <X.Y.Z>
        │
        ├─ state=REFUSED (preflight)  → fix the NAMED precondition (in the frozen order), re-run
        ├─ state=GATED  (acceptance)  → fix the acceptance FAILURE (doctor RED / validate / hooks), re-run
        └─ state=READY                → the publish plan is emitted; the OPERATOR executes it

The READY plan's tail carries the ER-reconciliation backstop — see below.

Procedure

  1. Pick the version (the operator picks it — there is no inference) and make sure the prep is staged: bump plugin.json alone → bump the install pin's remaining binding (below) → stage BOTH npm packages (next paragraph) → write the ## vX.Y.Z CHANGELOG section → commit (the release commit R).

    TWO npm packages ship on a v* tag, not one. npm-publish.yml runs two sequential jobs: publish-cli (cli/ → create-agentic-workspace, the scaffolder) and then publish-cli-update (cli-update/ → update-agentic-workspace, the upgrader), the second needs: the first and polls the registry for its dependency before installing. Staging a release means staging both, and they move together in R — only source.sha defers to R2:

    filefieldcommitrule
    .claude-plugin/plugin.jsonversionRthe plugin version itself
    .claude-plugin/marketplace.jsonversion, source.refRnot R2 — see the trap below
    .claude-plugin/marketplace.jsonsource.shaR2the ONLY field that defers; it names R, which does not exist yet
    docs/ + cli/permission-floor.jsongenerated_for_plugin_versionRboth, byte-identical
    README.mdthe **Status: vX.Y.Z.** lineRasserted equal to plugin.json's version
    cli/package.jsonfoundry.plugin_versionRasserted EQUAL to marketplace.json's version, so it follows the catalogue
    cli/package.jsonversionRthe scaffolder tarball; a row in TARBALL_VERSION_BY_PLUGIN_PIN, keyed by plugin_version
    cli-update/package.jsondependencies.create-agentic-workspaceRexactly cli/package.json's new version
    cli-update/package.jsonversionRmust ALSO move; a row in CLI_UPDATE_VERSION_BY_PIN

    ⚠ THE TRAP: the preflight and the acceptance gate disagree about the catalogue bump

    Read this before staging anything, because two of this playbook's own gates give opposite answers and only one of them is a HARD-STOP.

    • The preflight tolerates marketplace.json sitting at the immediately-preceding release and says in as many words that "the bump may be deferred to the re-pin commit R2".
    • The acceptance gate — claude plugin tag --dry-run, run inside run_acceptance — then REFUSES exactly that state: "Version mismatch: plugin.json says X but .claude-plugin/marketplace.json plugins[0].version says Y … update the marketplace entry before tagging." That is a HARD-STOP; the cut cannot reach READY.

    So the tolerance is not usable. marketplace.json's version and source.ref bump in R, with everything the equality chain drags along: cli/package.json's plugin_version (asserted equal to the catalogue version by test_the_plugin_pin_block_matches_the_marketplace_manifest, no R-to-R2 tolerance), then its version via TARBALL_VERSION_BY_PLUGIN_PIN, then cli-update's pin and version via CLI_UPDATE_VERSION_BY_PIN. source.sha is the only field that can defer to R2 — it names R, which does not exist until R is committed, and that is the whole reason R2 exists.

    The v1.10.0 cut lost two full preflight runs (~12 minutes each) rediscovering this: first by staging the CLI pins in R against the deferred catalogue (suite red), then by deferring the catalogue as this playbook said (acceptance HARD-STOP). Stage as the table says and neither State the trade honestly: deferring source.sha does NOT preserve AC-ILU-11's property — it is the field that breaks it. What it preserves is only the weaker, unavoidable "never pin a commit that does not exist yet", since source.sha names R. Between R and R2, main's catalogue advertises the new version while pinning the PREVIOUS release's tree, so an adopter resolving tagless in that window installs the previous code labelled as the new version — and sha outranks ref in resolution. It is sticky: R2 moves only sha, and manifestRefreshed needs BOTH version and sha to move, so their next refresh reports already current. main's ci is RED for the whole window — foundry_main_catalogue.py runs on push-to-main and exists to convict exactly this state; the workflow scopes it to push-only because the window is legitimate. That red is the window reporting itself, not a defect to fix in the gate. So: land R and R2 back to back. Every minute of that window is an adopter-visible mislabelled install. The window is not new — v1.9.1 had it too — but nothing in this playbook said so before.

    Why cli-update's own version must move whenever its pin does. The publish workflow skips a version already on the registry. update-agentic-workspace@<old> is published with the previous dependency baked in, so bumping only the pin leaves the registry serving an upgrader that installs the previous shared modules — a fully green release delivering the fix to nobody. On a release that first introduces a shared module it is worse: the published pin resolves to a version that does not contain it, and npx update-agentic-workspace@latest installs and then crashes on import.

    Both tables live in tests/test_bootstrap_cli.py and both refuse an unrecorded pin, so the preflight suite names this rather than trusting you to remember it. Add each row deliberately, with the one-line reason — that is the point of a table over a diff.

    RETIRED — this paragraph used to say marketplace.json does not bump in R. It claimed the version and source.ref bump moved to R2 alongside source.sha, on the AC-ILU-11 reasoning that the default branch must never advertise a version its own pinned commit does not carry. No cut has ever done that, because the acceptance gate refuses it (the trap above), and v1.9.1's own R bumped version + ref while leaving sha at the previous release's commit — exactly what step 1's table now prescribes. The text is kept as a tombstone because it is quotable and was believed; do not restore it. test_manifests_agree (tests/test_docs_claims.py) tolerates the R-to-R2 lag and still refuses a marketplace.json that gets AHEAD of plugin.json or whose source.ref names a version it does not itself carry.

    ⚠️ The install pin's remaining binding is NOT covered by this playbook's preflight. The REFUSED checks below cover plugin.json and the CHANGELOG section — not:

    1. DEFAULT_MARKETPLACE_REF in scripts/foundry-bootstrap.sh — the installer's own default pin, a separate code-level binding carried by the sibling feat-foundry-installer-unpinning;
    2. test_shipped_pin_matches_marketplace_manifest_ref in tests/test_bootstrap_install_pin.py, which compares that constant against marketplace.json's source.ref — and since that ref now lands in R2 rather than R (above), this comparison is only expected to agree once R2 lands, not at R itself.

    The documented marketplace-add install lines are NOT a per-release binding any more. README.md, docs/QUICKSTART.md, docs/troubleshooting.md, docs/how-to/adopt-on-an-existing-codebase.md, and this file's own "Downstream" section carry no version literal at all — there is nothing on them to bump on a cut (feat-foundry-install-line-unpinning). An adopter upgrades in place with claude plugin update foundry@agentic-foundry; see "Downstream" below for why, and for the one-time migration an existing tag-pinned adopter still needs.

    Those live in the pytest suite. Since feat-foundry-release-suite-gate, the preflight RUNS that suite and REFUSES the cut on any failure — so a stale pin can no longer reach a tag, and this list is guidance for staging the bump, not a checklist you must remember. It is here because knowing the remaining binding makes the refusal instantly actionable, not because you are the control.

    This is the fix for the v0.27.0 cut, which shipped a stale pin: its four metadata preconditions all passed and run_acceptance (validate + plugin tag --dry-run + DOCTOR-GREEN) runs no tests, so READY was returned truthfully while the tree was failing its own suite. The first remediation was a warning in this very paragraph — an instruction a human had to follow. That was the wrong control, and it was replaced with an enforced one.

  2. Run the gate:
    python3 "${CLAUDE_PLUGIN_ROOT}/scripts/foundry-cut-release.py" --tree <repo-root> --version X.Y.Z
    • REFUSED (exit 2) — a precondition failed; the output names it (e.g. "plugin.json version != target" for a typo'd version, or a marketplace.json that is neither at target nor at the one CHANGELOG-recorded preceding release, per step 1's deferred-bump note above). Fix and re-run. This now includes the candidate tree's own test suite, which runs after the four cheap metadata checks (so a typo'd version still refuses in milliseconds) and before acceptance. A failure names the failing tests; anything that prevents a real verdict — pytest absent, no tests/, nothing collected — refuses fail-closed rather than skipping. Expect the metadata-clean path to take a few minutes: that cost is bought once per release, and the alternative is a permanently wrong published tag.
    • GATED (exit 2) — run_acceptance returned FAIL; the candidate tree's own doctor / validate / hooks-executable check is red. Fix the defect (NOT the gate) and re-run.
    • READY (exit 0) — preconditions ok ∧ acceptance pass. The publish plan is printed.
  3. Execute the emitted publish plan yourself (cut-release never pushes). RE-PIN FIRST, TAG SECOND — this order is the whole point, and the previous order shipped the wrong code twice:

    1. git status --porcelain — must be empty. The re-pin commit is created after the acceptance verdict and the tag lands on it, so any uncommitted edit would ship under the release tag having never been gated.
    2. CONTENT=$(git rev-parse HEAD) — the release commit R, the code being shipped.
    3. Edit .claude-plugin/marketplace.json: source.sha = $CONTENT — that field alone. version and source.ref already moved in R (step 1's table and its trap: the acceptance gate's plugin tag --dry-run HARD-STOPS on a catalogue version that disagrees with plugin.json, so deferring them cannot reach READY). Deferring source.sha is what preserves AC-ILU-11's property: the pinned commit is R, which did not exist until R was committed.
    4. Commit it path-scoped: git commit -m '…' -- .claude-plugin/marketplace.json. Never commit -am, which sweeps every modified tracked file into the tagged commit. → R2
    5. git tag -a vX.Y.Z — on R2, the commit that CARRIES the pin.
    6. python3 scripts/foundry-cut-release.py --tree <repo> --version X.Y.Z --verify-tag — the machine re-check. Must print TAG-PIN-COHERENT. Do not push until it does.
    7. push origin main + push origin vX.Y.Z, never force-push (if a parallel push rejects, reconcile by MERGE).

      On a protected main, push origin main is REJECTED — even for the operator. This repo's main is Tier A (required contexts + enforce_admins), so R2 cannot be pushed directly; the tag pushes fine. Observed on v1.4.0: the tag landed and verified while main stayed a commit behind, leaving main's marketplace.json naming the PREVIOUS release's sha. The published artifact is unaffected (adopters resolve at the tag), but the unstable --ref main install path subsequently serves the previous version and the next cut starts from a wrong baseline. So create R and R2 on a BRANCH and PR them from the start — two PRs, the bump followed by the re-pin, because source.sha must name the commit AS IT LANDS on main and a squash merge does not preserve a branch commit's SHA. Read main's HEAD after the bump PR merges; that is the sha the re-pin PR pins. Tag only once R2 is on main.

    Why the order matters. An adopter installs by ref: marketplace add <repo>#vX.Y.Z resolves marketplace.json at the tag and installs the commit its source.sha names. Tagging before the re-pin leaves the tag serving the previous release's sha, so the install delivers the previous version's code — for a security patch, the fix is simply not delivered. That shipped on both v1.0.0 and v1.0.1 and was hand-corrected each time.

    Why step 6 is not optional. On the cut that creates the tag, the preflight coherence check reports "not applicable" — the tag does not exist yet, so there is nothing to inspect. Step 6 is the only point at which the property is machine-verified rather than assumed.

  4. Reconcile the release's ERs (the backstop's operator step). The READY plan's tail carries one gh issue close <n> step per enhancement-request the release closes, derived from the ## vX.Y.Z CHANGELOG section (the trace source authored in step 1). Review each before running it — see the backstop section below — and afterward execute the ones that apply. Closing an already-closed ER is a harmless no-op.
  5. Downstream — the documented install line registers the marketplace tagless (feat-foundry-install-line-unpinning), so the complete adopter upgrade, forever, is ONE command:

    claude plugin update foundry@agentic-foundry

    Or, since v1.9.0, the shipped tool that does this and the checking around it:

    npx update-agentic-workspace@latest

    Run it from inside the workspace directory. Prefer it over the bare plugin update whenever the adopter's state is not known-good — it is the manual steps below made mechanical, and it is the only path that verifies delivery rather than reporting it:

    • Phase 1 — refreshes the marketplace, first migrating a registration still carrying a pinned ref (the pre-v1.7.0 leftover): removed, re-added tagless, plugin re-installed, once per affected scope, that scope's other settings preserved. This is the one-time migration written out by hand further down; the tool finds the affected scopes instead of the operator guessing. It reads the marketplace catalogue manifest before and after, and reports changed only when BOTH version and source.sha moved — so an R2 that moves only source.sha reports already current despite real delivery.
    • Phase 2 — plugin update in every scope whose settings enable it, from the pre-migration snapshot. ⚠ Phase 2 verifies nothing per scope. runPluginUpdate's result is discarded and Phase 2's printed verdict is a copy of Phase 1's catalogue delta, so a scope whose update silently no-ops still prints changed. The read-back above is a catalogue check, not a delivery check — you still confirm both scopes by hand (below).
    • Phase 3 — --cleanup, opt-in: prunes superseded plugin-cache versions and a stale or duplicate registration no scope still enables. A flagless run previews and removes nothing.
    • Phase 4 — re-runs the managed-file and permission-floor reconcile, so an adopter's floor picks up rules a release added rather than staying at whatever the last scaffold wrote.

    Every claude invocation it makes is drawn from one frozen, closed six-subcommand allowlist; none can start a session or reach the trust dialog. One side effect to state when handing it over: healing a pinned-ref scope ends in plugin install, so a scope where the plugin was deliberately disabled has it re-enabled. That is specified, not accidental — a stale pin is a broken registration either way — but an adopter who wants it off must disable it again afterward.

    That is the whole upgrade — nothing else to run. plugin update resolves the marketplace at the ref the registration names. A tagless registration names none, so resolution reads the catalogue live off the default branch every time; a new release reaches every adopter the moment R2 lands there, with no re-add and no per-release step on the adopter's side. The plugin id must be marketplace-qualified: a bare claude plugin update foundry fails with Plugin "foundry" not found.

    Landing R2 on the default branch is delivery-critical, precisely because of that. A tagless registration resolves the catalogue from the default branch itself, never from a tag — so until R2 (the commit carrying the new version / source.ref / source.sha, step 3) actually lands there, plugin update keeps reporting "already at the latest version", naming the PREVIOUS release, truthfully. Publishing a tag is not delivery; landing R2 on the default branch is. There is no install line to bump on this side and no per-release step to forget: the whole upgrade path is this one command, every time.

    One-time migration for an adopter still tag-pinned from BEFORE this change. npx update-agentic-workspace (above) performs exactly this as its Phase 1, in every affected scope, and that is the way to do it. What follows is the same procedure by hand — read it to understand why each step is there, or run it when npm is not available:

    Their registration is <owner>/<repo>#<old-tag>, and marketplace update alone is a no-op against it — it refreshes the cache at the ref already declared, which is the frozen old tag, forever. Re-running marketplace add <repo> tagless does not fix it either: it REFUSES outright, because a different ref is already declared for that marketplace in settings.json (the CLI's own message: "its network source differs from the one declared for it in settings"). The actual fix is marketplace remove → tagless marketplace add → plugin install, not plugin update — marketplace remove also drops the plugin(s) installed from that marketplace (same as the "Wedged or stale install" recovery in docs/troubleshooting.md), so a plugin update afterward would have nothing to update; plugin install puts it back. Run it explicitly per scope, in every scope that still carries the old tag-pinned registration — a bare invocation touches only the default scope, and a stale scope silently shadows a fresh one (see the --scope-mismatch caveat below):

    # user scope
    claude plugin marketplace remove agentic-foundry --scope user
    claude plugin marketplace add lukasrepublic/agentic-foundry --scope user
    claude plugin install foundry@agentic-foundry --scope user
    
    # project scope (repeat if a project-scoped registration was also tag-pinned)
    claude plugin marketplace remove agentic-foundry --scope project
    claude plugin marketplace add lukasrepublic/agentic-foundry --scope project
    claude plugin install foundry@agentic-foundry --scope project

    Do this ONCE per affected scope. Every cut after that resolves automatically via the one-line plugin update upgrade above.

    Verify by READING the refreshed cache — ~/.claude/plugins/marketplaces/<marketplace>/.claude-plugin/marketplace.json must show the new version AND the new source.sha; the CLI's own "success" line does not prove the ref moved. npx update-agentic-workspace@latest does NOT do this for you. Its read-back covers the marketplace catalogue only, in Phase 1; nothing reads back per-scope install state, so the both-scopes check in this paragraph is still yours to run. This is the v1.4.1 scar, and it is exactly the shape that produced it: a stale project row shadows a fresh user one, the tool prints changed from the catalogue delta, and the session keeps loading the previous version. Check both scopes: a stale project row shadows a fresh user one, so a user-scope update can report success while the session keeps loading the old tree. Identify the tree actually loaded by a version-unique path, never by process start time. Missed on the v1.4.1 cut, where the plugin sat at 1.4.0 through several "successful" update runs. For this repo's own adopters that is claude plugin marketplace update agentic-foundry → claude plugin update foundry@agentic-foundry (claude plugin list prints the qualified id if you are unsure). Afterward, verify the SHIPPED artifact, not the source tree — run /foundry:doctor and any atom's --selftest against ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/. A green source tree does not prove the published one is green. (When invoking a cached script by hand, set CLAUDE_PROJECT_DIR — without it, stack-profile-lock reports a misleading "not applicable".) Do not bulk-push changes into external-org / dirty / feature-branch repos.

The ER-reconciliation backstop

Enhancement-request issues should close when their implementing atom merges (the PR-level Closes #<n> control). That control depends on the GitHub-Projects project-map.json cache, which is absent when projection is disabled — so ERs silently slip and the backlog fills with shipped-yet-open issues (this is why past releases have shipped enhancement requests that stayed OPEN in the tracker). Cut-release is the durable backstop: on every cut it re-derives the release's ERs from the CHANGELOG and emits their closes.

  • Trace source = the ## vX.Y.Z CHANGELOG section, already authored on every cut. The tool matches only the explicit ER #<digits> marker — never a bare #NNN — because the CHANGELOG cites the ER (e.g. ER #<n>) and its implementing PR (e.g. (#<n>)) in the same #NNN namespace; a bare match would try to gh issue close a pull request. An unmarked number is not an ER.
  • Emitted, never executed. Like the tag/push plan, the gh issue close steps are DATA the operator runs — cut-release never touches gh. They are appended after the push steps so an ER only closes once its release is on main.
  • The OPEN-ER advisory. When gh is reachable, READY prints which traced ERs are still OPEN at cut time (the ones the reconciliation targets). Offline, the check is skipped and the plan steps are still emitted (fail-safe: an unknown ER is never reported closed).
  • REVIEW before running (the load-bearing caveat). The prose trace is deliberately broad and cannot distinguish "closes ER #N" from a mere reference — e.g. a "split to ER #N" / "deferred to ER #N" follow-up mention traces #N too (a CHANGELOG section can reference a deferred ER this way). Drop any such referenced-but-not-closed ER from the plan before executing; close only the ones the release actually shipped. The advisory's OPEN list is your review surface, not an auto-close order.

Release-branch flow (branch-and-worktree-discipline, AC-BWD-4, v1.16.0)

context/branch-discipline.md is the full discipline (cited, never copied); this is where it lands in the cut-release playbook specifically — for the plugin's own repo AND for an adopter's.

  • skills/intake/SKILL.md creates release/<version> from main as its LAST step and records it in the release manifest as integration_branch (scripts/foundry_release.py's optional field).
  • Every atom's own PR targets release/<version>, not main — /foundry:merge-when-green refuses the wrong base when --release <id> is passed (skills/merge-when-green/SKILL.md).
  • The R / R2 cut-release commits described above land ON release/<version>, exactly as before — this playbook's own procedure is unchanged; only the branch they land on moves.
  • The ONE PR to main is release/<version> → main, opened once every atom in the release has landed on the release branch and this playbook's READY plan is in hand. It is what makes a per-merge deploy trigger fire once per release instead of once per atom (the charter's own observed failure).
  • After the tag exists on the resulting main commit, the release branch itself is deleted — the same "delete after origin contains the merge, never before" rule as any atom branch. Run python3 "${CLAUDE_PLUGIN_ROOT}/scripts/foundry-worktree-gc.py" --dry-run --repo <dir> (add --include '<prefix>/*' for any branch prefix outside the built-in set; check filtered_out_refs; after --apply, read status — partial lists every deletion that did not happen in failed, while the exit code stays 0) first (mode flag BEFORE --repo — the permission-floor row is an argv prefix rule), then --apply --repo <dir> to sweep every already-merged atom/release branch and its worktree in one pass (see context/branch-discipline.md's own recipe).
  • A hotfix never uses a release branch at all: hotfix/<id> → main directly, stated in the hotfix PR's own body.

What it does NOT do

  • It never tags, pushes, or closes issues. It emits the plan — including the ER-reconciliation gh issue close steps — as data; the operator runs it. (The selftest proves zero side effects over a temp tree; the ER-state check is read-only and fail-safe.)
  • It reimplements no gate. The exit gate IS foundry-release-acceptance.py; a non-pass verdict is a HARD-STOP, not something cut-release re-derives or can override.
  • No version inference, no signing. The operator picks the version; release-time signing/provenance (Sigstore/SLSA) is a typed, dormant seam gated to the first public release.

The install pin: what source.sha is, and why it names the tag's PARENT

Decided 2026-08-07 (operator). This was an open design question — drop source.sha and let ref plus tag protection carry the integrity claim, or keep it and document what it names. It is kept, and the reasoning is recorded here so the next reader does not re-open it, or worse, "fix" the parent relationship into a loop.

sha is not decorative — it OUTRANKS ref. Per the Claude Code plugin-marketplace docs (verified 2026-08-07), a github/git/url plugin source takes ref ("Optional. Git branch or tag") and sha ("Optional. Full 40-character git commit SHA to pin to an exact version"), and when both are present the pinned commit is what gets installed. Since Claude Code v2.1.141 a deleted upstream ref does not even block the install, as long as the pinned commit is still reachable. So:

Resolved byWhat an adopter gets
the marketplace catalogueread at the ref — marketplace sources support ref only
the plugin itselfinstalled at sha — ref is the label and the fallback

Therefore source.sha at tag vX.Y.Z names the tag commit's FIRST PARENT, and that is structurally necessary rather than a defect. A commit cannot contain its own hash, so the pin names the content commit R while the tag sits on the re-pin commit R2. tag_pin_coherence enforces the adjacency (sha ∈ {tag commit, first parent}), so a pin reaching further back than the parent is refused — that is the check, not an accident of it.

Why it is kept rather than dropped. Dropping it would delete R2, and with it the plan's ordering hazard, the coherence gate and the adjacency invariant — a real simplification, honestly weighed. Two things decided against it. First, this repo's own standing-versions rule binds it: "CI actions pinned to a 40-char commit SHA, not @vN". Publishing its own supply-chain artifact pinned to a mutable tag while requiring SHA pins of everyone else is incoherent, and the README already advertises the project as SHA-pinned. Second, this plugin ships hooks that execute in an adopter's session; tag protection is a policy control an admin can switch off, whereas a content-addressed pin cannot be. The industry pattern points the same way — Homebrew pins a tarball checksum, Go records dependency hashes, GitHub Actions consumers pin @<sha> — with the caveat that in all of those the pin lives in the consumer or a separate index, never self-referentially inside the artifact. Claude Code's same-repo catalogue is what forces the self-reference here. Splitting the catalogue into its own repo would dissolve it structurally and remains the only clean alternative, at a cost far above this decision.

KNOWN AND HARMLESS: the installed tree carries a stale source.sha. Because sha outranks ref, what an adopter installs is R — and R's own .claude-plugin/marketplace.json still carries the previous release's sha, since the correcting value only lands in R2. This is inert: the catalogue an adopter resolves is read at the ref (R2), never from the installed tree, so the stale field is never consulted by anything. Do not "fix" it. Writing R's own sha into R changes R's hash — the self-reference again — and chasing it produces an infinite re-pin loop. It is recorded here because this repo has shipped four separate stale-pin defects, and the next reader will otherwise find this one and treat it as a fifth.

Anti-patterns

  • Editing the gate to make a red cut go green — fix the candidate tree, never the acceptance gate.
  • Tagging before re-pinning. The tag must sit on the commit that carries the pin, or an install by ref serves the previous release. This is the defect that shipped twice; the plan's order is the fix and it is not a stylistic preference.
  • Using git commit -am for the re-pin. The tag lands on that commit after the gate ran — path-scope it to .claude-plugin/marketplace.json so nothing ungated rides along.
  • Pushing without running --verify-tag. The preflight check is a no-op on the cut that creates the tag; skipping the post-tag re-check leaves the property entirely unverified.
  • Pinning source.sha to an annotated-tag object, a branch name, or an abbreviated hash — it must be a full 40-character commit id. A ref name is a mutable pin: what an adopter resolves changes as the ref moves. And since sha outranks ref at install time, a bad value here is not softened by a correct tag — it IS what ships.
  • "Correcting" source.sha at the tag so it names the tag commit. It cannot: a commit cannot contain its own hash. The parent relationship is the design (see The install pin above), and every attempt to close it produces a new commit that needs a new pin, forever.
  • Force-pushing to reconcile a parallel push — reconcile by merge; the tag is already immutable.
  • On a harness denial of a cut-release command — distinct from this loop's own REFUSED/GATED states — see docs/harness-denial-fallback.md and STOP: hand back the exact denied invocation; never retry it or route around it.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.