Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bcb1477
Merge branch 'fix/workflows-cluster' into feat/release-gate-scaffold-…
REPPL Sep 25, 2026
56b41d5
chore: capture two findings from the release-gate wiring lane
REPPL Sep 25, 2026
c04a12d
fix(release-gate): bind the derived content commit to the release it …
REPPL Sep 25, 2026
2f01968
chore: resolve iss-2609251755386183 — the receipt gate binds its release
REPPL Sep 25, 2026
273f8cb
feat(launch): run the release job's receipt gate locally and end the …
REPPL Sep 25, 2026
8b38458
chore: resolve iss-2609251751298408 — launch.md places the receipt ga…
REPPL Sep 25, 2026
1cb31e2
feat(launch): wire the scaffold to the repo's own CI checks and write…
REPPL Sep 25, 2026
09e0af6
test(launch): drive a scaffolded repo's first release through the mer…
REPPL Sep 25, 2026
62e76ca
chore: close spc-2609230613193436 and ship itd-93
REPPL Sep 25, 2026
27ce923
fix(launch): say "full sha" in the receipts protocol, and report no C…
REPPL Sep 25, 2026
89bd10f
Merge branch 'feat/launch-payload-diff' into feat/release-gate-scaffo…
REPPL Sep 25, 2026
ddb84cb
chore: capture six release-gate review findings
REPPL Sep 25, 2026
2ccf16b
fix(release-gate): bind receipts to a version the reader can read
REPPL Sep 25, 2026
4318ca4
chore: resolve iss-2609251939461459 — a released tree with no version…
REPPL Sep 25, 2026
7d09975
chore: resolve iss-2609251939468296 — an unreadable newest heading re…
REPPL Sep 25, 2026
5bd2600
chore: resolve iss-2609251939460232 — version filter precedes nearest
REPPL Sep 25, 2026
066e12b
chore: resolve iss-2609251939466588 — receipt dirs require a full sha
REPPL Sep 25, 2026
3ab36f8
fix(release): the receipts protocol says the release publishes from t…
REPPL Sep 25, 2026
bbce257
chore: resolve iss-2609251939476304 — the protocol names the tagged m…
REPPL Sep 25, 2026
477269a
docs(launch): say the scaffold's workflow audit is not a full zizmor run
REPPL Sep 25, 2026
4ff767e
chore: capture the release gate's missing tag-to-CHANGELOG binding
REPPL Sep 25, 2026
3f613d3
fix(release): bind the pushed tag to the released CHANGELOG version
REPPL Sep 25, 2026
857a2e8
docs(launch): the scaffolded verify derives from the receipts directory
REPPL Sep 25, 2026
7f5ded9
chore: resolve iss-2609251945586202 — the pushed tag binds to the rel…
REPPL Sep 25, 2026
47508ce
chore: capture the derivation's shadowing by a post-roll receipts dir
REPPL Sep 25, 2026
c4937b1
chore: capture the abcd runbook's glued deterministic-gates heading
REPPL Sep 25, 2026
a55a86d
fix(scaffold): the runbooks list the tag-binding gate, and abcd's hea…
REPPL Sep 25, 2026
b411faf
chore: resolve iss-2609252029191920 — the abcd runbook heading renders
REPPL Sep 25, 2026
5d55740
chore: defer the receipts-shadowing sharpening out loud pending a pro…
REPPL Sep 25, 2026
72bd363
Merge branch 'feat/launch-payload-diff' into feat/release-gate-scaffo…
REPPL Sep 25, 2026
d18b477
Merge branch 'main' into feat/release-gate-scaffold-wiring
REPPL Sep 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions .abcd/development/brief/04-surfaces/04-launch.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ workflow that tags it.
| Verb | Bucket | Status |
|---|---|---|
| `archive` | gate | shipped |
| `receipts` | gate | shipped |
| `scaffold` | — | shipped |
| `ship` | gate | shipped |

Expand Down Expand Up @@ -81,6 +82,26 @@ to the pin, the render leaves the dirty-tree gate to it — a payload file that
differs from the commit changes the digest and refuses; unbound, it runs the
gate, and an uncommitted change refuses the render.

**The emit step ends with the receipts protocol.** After the cut's report, the
render closes with a numbered checklist, composed in the core and carried in
the machine-readable report too: commit the roll, run each semantic gate the
release workflow requires against that commit, key every receipt to it, commit
the receipts on top so the branch is exactly two commits, then prove the gate
locally. The gate names come from the committed `release.yml`, the list the
release job enforces, so the checklist cannot ask for a gate the release does not
require. A workflow that arms no semantic gate gets a checklist that says no
receipt is required.

**The receipts check is the release job's receipt gate, run before the merge.**
It reads the required gates from the committed `release.yml`, derives the
content commit from the receipts directory the way the release job does, and
runs the release job's own check over it — one reader, which a test holds to the
release job's verdict and reasons on the same repository state by running the
workflow's step beside it. It names each missing or non-PROMOTE receipt and the
commit the receipt must name, and refuses on an uncommitted receipt change,
because the release job reads the committed tree. It exits 0 when the gate would
admit (or nothing is armed), 1 when it would refuse, and 2 on a structural fault.

`commands/launch.md` carries the emit, compose and ingest orchestration over the
`release-changelog-composer` agent, including the release page's retry loop. The
deterministic emit alone is `abcd changelog`, read-only and prose-free.
Expand All @@ -96,8 +117,17 @@ version flag at all: the version is derived, never authored
([adr-31](../../decisions/adrs/0031-derived-versioning-from-intents.md)).

**The scaffold writes the release machinery into a managed repo that lacks
it**: the two release workflows and the adr-37 release runbook, wired to the
repo's own default branch and Go version, token-scoped and injection-safe. The
it**: the two release workflows, the adr-37 release runbook and a reviews-charter
check, wired to the repo's own default branch and Go version and to the check
names its own pull-request CI reports, token-scoped and injection-safe. The check
names are read from the repo's pull-request and merge-queue workflows — a name
only a run knows (a matrix job, an expression-named job, a reusable-workflow
call) is omitted rather than guessed, and every name is held to an injection-safe
allowlist — and written into the runbook as the contexts to require on the default
branch and into the release workflow's verify header as its merge gate. The
reviews-charter check holds dated review directories to their shape and exempts
the sha-keyed receipt directories, and the scaffolded verify job runs it as a
deterministic gate, so a release's own receipts never fail the charter. The
workflows ship from a single embedded template that abcd's own release workflows
are regenerated from, proved byte-exact by a test, so a scaffolded repo and this
one cannot drift. The scaffolded workflow carries a **rehearsal** that arms the
Expand Down Expand Up @@ -697,7 +727,7 @@ _Generated from the command tree; a drift test fails `go test` when this appendi

### `abcd launch`

Sub-verbs: `abcd launch archive`, `abcd launch scaffold`, `abcd launch ship`, `abcd launch smoke-pages`.
Sub-verbs: `abcd launch archive`, `abcd launch receipts`, `abcd launch scaffold`, `abcd launch ship`, `abcd launch smoke-pages`.

| Flag | Type |
|---|---|
Expand All @@ -717,6 +747,12 @@ Sub-verbs: none.
| `--tag` | string |
| `--verify` | bool |

### `abcd launch receipts`

Sub-verbs: none.

Flags: none.

### `abcd launch scaffold`

Sub-verbs: none.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private-plan repos where no ruleset is possible. Deterministic expert work
that repeats per repo is precisely the facilitation abcd exists to absorb;
leaving it manual means most repos simply never get the bar.

The seam already exists. [itd-93](../planned/itd-93-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md)
The seam already exists. [itd-93](../shipped/itd-93-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md)
has abcd scaffolding the hardened release workflows into a managed repo,
parity-tested against the live workflows so the template cannot drift from
reality. This intent generalises that machinery to the standing CI gates and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ builds_on: []
related_adrs: [adr-37]
severity: minor
related_issues: [iss-327]
impact: additive
---

# abcd Scaffolds a Release Gate That Works on the First Try
Expand Down Expand Up @@ -219,4 +220,5 @@ queued in `../../plans/2026-07-24-next-run-queue.md` (Track 1)._

## Audit Notes

_Empty. Populated by intent-fidelity-reviewer when intent moves to shipped/._
<!-- abcd-review: OWED receipt=rcp-1957b22ad5cc -->
Fidelity review OWED (receipt rcp-1957b22ad5cc).
2 changes: 1 addition & 1 deletion .abcd/development/plans/2026-07-18-next-drain-run-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ PR #99) — scaffold `release.yml`/`auto-release.yml` (armed against the reviewe
content commit), the runbook, and the sha-keyed-receipt/RD001 interop into a
managed repo, so its first public release cannot hit the self-reference abcd-cli
paid to discover. Backing intent:
[`../intents/planned/itd-93-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md`](../intents/planned/itd-93-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md).
[`../intents/planned/itd-93-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md`](../intents/shipped/itd-93-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md).

**NOT yet run-ready — readiness gates (a drain burst must check these first and
SKIP-with-reason if unmet, per the protocol's skip filter):**
Expand Down
30 changes: 27 additions & 3 deletions .abcd/development/release-gate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ this list is the human-readable mirror.
8. Reviews-charter discipline (RD001-RD003)
9. Smoke every command (self-discovering harness)
10. Plugin archive reproduces the committed pin (fail-closed)
11. The release tag names the released CHANGELOG version (fail-closed)

Gate 11 runs on a real release only too: it refuses unless the release tag is
`v` plus the version `record-lint --released-version` reads from the released
tree, the reader the semantic receipts are bound with. Gate 10 refuses such a
tag first on this repository's own release; gate 11 is the check a scaffolded
repository, which has no archive gate, relies on (iss-2609251945586202).

Gate 10 runs on a real release only (a rehearsal has no release tag to bind). It
re-renders the release's plugin archive from the tagged commit and refuses unless
Expand Down Expand Up @@ -100,8 +107,19 @@ name the commit they gate, so `record-lint --derive-content-sha` reads the
`.abcd/work/reviews/<sha>/` entry on the released lineage and returns that `<sha>`
— not the merge commit, and not `<merge>^2^` ancestry, which a batched
merge-queue push can point at an unrelated PR's commit (`github.sha` is the batch
tip, iss-355). `subject.digest.gitCommit` therefore still matches the armed
commit exactly and the gate stays strict. (Before this, the gate armed with the tagged merge commit, whose
tip, iss-355). The entry must also belong to THIS release: the commit it names
carries the released tree's own newest dated CHANGELOG version, or the derivation
fails closed — the nearest entry on a release that recorded no receipts of its
own is the previous release's, whose valid receipts would otherwise admit it
unreviewed (iss-2609251755386183). Entries carrying another version are passed
over before the nearest is taken, so a co-batched pull request's own sha-keyed
entry cannot tie with or shadow the roll's (iss-2609251939460232), and an entry
must be named by the full sha (iss-2609251939466588). The released tree's newest
release heading is read strictly: a pre-release or undated head, or a tree that
names no dated release, refuses, because there is no version to bind the receipts
to (iss-2609251939468296, iss-2609251939461459) — which is why the rehearsal
rolls a plain `## [0.0.0]` heading. `subject.digest.gitCommit` therefore still
matches the armed commit exactly and the gate stays strict. (Before this, the gate armed with the tagged merge commit, whose
tree can never hold a receipt naming itself — an unsatisfiable self-reference.
Dormant while the repo was private, it surfaced at the first public release and
fail-closed it, v0.3.0, iss-108.)
Expand Down Expand Up @@ -204,7 +222,13 @@ semantic refusal blocks the release without the version-consuming wedge of a gat
that sat in the publish path (iss-2608231226347380). `verify` supplies the
content commit and the required-gate list from the workflow (the trust root), not
the in-tree config: `record-lint --release-gate <sha> --require-gate <name>…`,
where `<sha>` is `record-lint --derive-content-sha`. The rule is skipped on the
where `<sha>` is `record-lint --derive-content-sha`. Before it, on a tag push,
`verify` requires the tag to be `v` plus the version `record-lint
--released-version` reads from the released tree — the strict reader the
receipts are bound with — so a hand-pushed tag naming another version cannot
publish under the receipts of the CHANGELOG's version (iss-2609251945586202).
On abcd's own profile `launch archive --tag` refuses such a tag earlier still.
The rule is skipped on the
rehearsal path (`workflow_dispatch`), where no real receipts exist. Once `verify`
has admitted the release, `release.yml`'s publish job signs the receipts with
`actions/attest` (predicate `.../semantic-release-gate/v1`) and verifies the
Expand Down
6 changes: 6 additions & 0 deletions .abcd/development/release/surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,12 @@
}
]
},
{
"path": "abcd launch receipts",
"hidden": false,
"sentence": "Run the release job's semantic-receipt gate locally, before the merge: Writes nothing; refuses with exit 1 when the release job would refuse the receipts.",
"flags": []
},
{
"path": "abcd launch scaffold",
"hidden": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ production_mode: hand-written

## Summary

The remainder of [itd-93](../../intents/planned/itd-93-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md)
that [spc-14](../closed/spc-14-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md) did not deliver. spc-14 closed on
The remainder of [itd-93](../../intents/shipped/itd-93-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md)
that [spc-14](spc-14-abcd-scaffolds-a-hardened-changelog-driven-release-gate-into.md) did not deliver. spc-14 closed on
2026-09-23 with acceptance criteria 3, 4 and 6 delivered and criteria 1 and 5 in part: `launch scaffold` writes `release.yml`, `auto-release.yml` and the runbook with a `GITHUB_TOKEN`-only gate, the bare render states that no semantic detector is configured, a re-run is an idempotent no-op that refuses a hand edit, and the rehearsal publishes nothing. This spec carries what did not ship.

The delivered part was already announced in the [0.4.1] changelog section,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
schema_version: 1
id: "iss-2609251939472371"
slug: "the-scaffolded-release-workflows-in-repo-audit-covers"
severity: "minor"
category: "future-work-seed"
source: "review-followup"
found_during: "autonomous run A resumed 2026-09-25"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/core/launch/scaffold/cichecks.go"
---

The scaffolded release workflows' in-repo audit covers injection and duplicate keys for every profile, but it is not a full zizmor stand-in for the bare profile: action pinning, permissions and credential classes are unchecked there, and only the abcd profile is zizmor-audited in CI. Follow-up: run zizmor over a rendered bare profile in CI.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
schema_version: 1
id: "iss-2609252024442310"
slug: "the-release-gate-s-content-commit-derivation-can-be-shadowed"
severity: "minor"
category: "bug"
source: "review-followup"
found_during: "autonomous run A resumed 2026-09-25"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/core/lint/releasegate_derive.go"
deferred_after: "v0.10.0"
deferral_reason: "needs a product-thinker ruling on the receipts protocol: must the reviewed content commit always be the CHANGELOG roll itself, or may receipts name a later revision the reviewers read (a roll, then a prose fix)? The derivation admits the second shape today and the docs do not forbid it; the sharpening that would stop a post-roll receipts directory shadowing the roll depends on the answer. Ruling S in autonomous run A's rulings-owed list, 2026-09-25"
---

The release gate's content-commit derivation can be shadowed by a pull request that lands between the roll merge and the tag. DeriveReleaseContentSha keeps the nearest receipts directory carrying the released version, so a PR branched after the roll merged (it carries the new version) with its own sha-keyed receipts directory, merged on top before the tag, wins: the gate then judges that commit's receipts, either a genuinely reviewed nearer commit or a fail-closed wedge that ignores the roll's real receipts (review2-gatewire probe S11). It is reachable only when a PR lands in that window (a failed auto-release re-run by hand) and admits nothing past the committed-receipts trust boundary. The reviewer's sharpening, admitting only the candidate whose first parent carries a DIFFERENT version (the roll itself), is not contained: it refuses a release branch whose receipts name a later CHANGELOG revision (roll, then a prose fix the reviewers read), whose first parent already carries the new version, which the derivation admits today and commands/launch.md does not forbid. It needs a ruling on whether the reviewed content commit must be the roll itself before the derivation narrows.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
schema_version: 1
id: "iss-2609251751298408"
slug: "commands-launch-md-describes-the-release-job-s-semantic"
severity: "minor"
category: "documentation"
source: "agent-finding"
found_during: "autonomous run A resumed 2026-09-25"
origin: researcher-authored
production_mode: hand-written
found_at: "commands/launch.md"
resolution: "commands/launch.md states where the receipt gate runs as release.yml runs it: in verify, before the tag on the auto-release path, with the content commit derived from the receipts directory."
impact: fix
resolved_by:
commit: "273f8cbc"
---

commands/launch.md describes the release job's semantic receipt gate at a position it no longer holds. Four places — the Ship intro ('the tag is already created by then'), the release-day failure list ('The tag exists by then'), Semantic receipts ('release.yml derives the content commit as <merge>^2^') and 'Prove the gate before you merge' ('receipt_gate runs inside the release job, which is after the tag is created') — predate adr-52 and iss-355: release.yml runs the gate in its verify job, which the tag job needs, so on the auto-release path a refusal leaves no tag and the version free, and the content commit is derived from the receipts directory of the released tree (record-lint --derive-content-sha), not from merge ancestry. Only a hand-pushed tag exists before the gate. An operator reading the page believes a refusal consumes the version and reaches for a tag deletion the machinery no longer needs.

## Grounds

- pursued: an operator reading the page now expects a refused gate to leave the version free on the auto-release path and to consume it only on a hand-pushed tag; it would be shown wrong if release.yml's receipt step moved out of the verify job the tag job needs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
schema_version: 1
id: "iss-2609251755386183"
slug: "the-release-job-s-semantic-receipt-gate-admits-a-release"
severity: "major"
category: "security"
source: "agent-finding"
found_during: "autonomous run A resumed 2026-09-25"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/core/lint/releasegate_derive.go"
resolution: "The derivation binds the derived content commit to the release: it must carry the released tree's newest dated CHANGELOG version, else it fails closed naming both."
impact: fix
resolved_by:
commit: "c04a12de"
---

The release job's semantic receipt gate admits a release that carries no receipts of its own, whenever an earlier release's receipts are in the tree. record-lint --derive-content-sha (lint.DeriveReleaseContentSha) arms the gate against the NEAREST commit on the released lineage that a .abcd/work/reviews/<sha>/ directory names, and never checks that the commit belongs to this release. A roll to a new version with no new receipts derives the previous release's content commit, whose PROMOTE receipts are valid, so receipt_gate passes and the release publishes unreviewed. Reproduced on a scratch clone of this repository at the v0.10.0 tip: a commit rolling CHANGELOG to 0.11.0 derived 64ea8f62 (the v0.10.0 cut) and the armed gate printed no finding. Every release after the first that ever recorded receipts is exposed; the itd-93 scaffolded gate and abcd launch receipts inherit it through the same reader. Fix direction: bind the derived commit to the release — its newest dated CHANGELOG version must equal the released tree's, else fail closed naming both.

## Grounds

- pursued: a roll with no receipts of its own now fails at --derive-content-sha instead of arming against the previous release's cut (TestDeriveReleaseContentSha_RefusesAnEarlierReleasesReceipts); it would be shown wrong by a real release whose roll commit legitimately carries a different newest dated version than its merged tree, which no release shape produces
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
schema_version: 1
id: "iss-2609251939460232"
slug: "the-release-gate-s-content-commit-derivation-applies-the"
severity: "minor"
category: "bug"
source: "review-followup"
found_during: "autonomous run A resumed 2026-09-25"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/core/lint/releasegate_derive.go"
resolution: "Candidates are filtered by version before the nearest is taken, so a co-batched pull request's receipts directory neither ties with nor shadows the roll's."
impact: fix
resolved_by:
commit: "2ccf16b73b3b870a493bf9a526dac30cbb330c9c"
---

The release gate's content-commit derivation applies the version binding to the NEAREST receipts directory only. A co-batched pull request that carries its own commit-keyed receipts directory either ties with the release roll's directory or sits nearer and carries the previous version; both refuse although the roll's correct receipts are on the lineage, and the cut cannot be rerun without a fresh reviewed commit. Candidates should be filtered by version first and the nearest taken from those.

## Grounds

- pursued: the roll derives when a batch-mate's receipts directory ties with or sits nearer than its own (TestDeriveReleaseContentSha_SkipsANearerReceiptsDirOfAnotherVersion, review probes S3 and S3b); a tie or version refusal there would show it wrong.
Loading
Loading