From bdbbb2adc1aa84b2786550c6e494103b26d66cda Mon Sep 17 00:00:00 2001 From: Levi Figueira Date: Tue, 28 Jul 2026 19:41:28 +0100 Subject: [PATCH] docs: shape the release promotion model change --- .../change.json | 6 + ...9-002311-review-shaping-round-1-codex.html | 136 ++++++++++++++ ...729-004327-review-shaping-round-2-sol.html | 143 ++++++++++++++ ...10126-review-shaping-round-3-reviewer.html | 137 ++++++++++++++ ...729-013606-review-shaping-round-4-sol.html | 109 +++++++++++ .../20260728-release-promotion-model/shape.md | 177 ++++++++++++++++++ .../tasks/TASK-001-channel-rung-model.md | 46 +++++ .../TASK-002-promote-and-channel-flags.md | 50 +++++ .../tasks/TASK-003-gate-fires-at-rc.md | 42 +++++ .../TASK-004-designation-commit-promotion.md | 50 +++++ ...SK-005-per-channel-changelog-guardrails.md | 46 +++++ .../TASK-006-channel-routed-distribution.md | 43 +++++ .../tasks/TASK-007-release-skill-rewrite.md | 47 +++++ ...-release-notes-and-changelog-projection.md | 50 +++++ 14 files changed, 1082 insertions(+) create mode 100644 docs/changes/20260728-release-promotion-model/change.json create mode 100644 docs/changes/20260728-release-promotion-model/reports/20260729-002311-review-shaping-round-1-codex.html create mode 100644 docs/changes/20260728-release-promotion-model/reports/20260729-004327-review-shaping-round-2-sol.html create mode 100644 docs/changes/20260728-release-promotion-model/reports/20260729-010126-review-shaping-round-3-reviewer.html create mode 100644 docs/changes/20260728-release-promotion-model/reports/20260729-013606-review-shaping-round-4-sol.html create mode 100644 docs/changes/20260728-release-promotion-model/shape.md create mode 100644 docs/changes/20260728-release-promotion-model/tasks/TASK-001-channel-rung-model.md create mode 100644 docs/changes/20260728-release-promotion-model/tasks/TASK-002-promote-and-channel-flags.md create mode 100644 docs/changes/20260728-release-promotion-model/tasks/TASK-003-gate-fires-at-rc.md create mode 100644 docs/changes/20260728-release-promotion-model/tasks/TASK-004-designation-commit-promotion.md create mode 100644 docs/changes/20260728-release-promotion-model/tasks/TASK-005-per-channel-changelog-guardrails.md create mode 100644 docs/changes/20260728-release-promotion-model/tasks/TASK-006-channel-routed-distribution.md create mode 100644 docs/changes/20260728-release-promotion-model/tasks/TASK-007-release-skill-rewrite.md create mode 100644 docs/changes/20260728-release-promotion-model/tasks/TASK-008-release-notes-and-changelog-projection.md diff --git a/docs/changes/20260728-release-promotion-model/change.json b/docs/changes/20260728-release-promotion-model/change.json new file mode 100644 index 000000000..374c7ac5d --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/change.json @@ -0,0 +1,6 @@ +{ + "branch": "release-promotion-model", + "change": "release-promotion-model", + "created": "2026-07-28", + "target_release": "2.0.0" +} diff --git a/docs/changes/20260728-release-promotion-model/reports/20260729-002311-review-shaping-round-1-codex.html b/docs/changes/20260728-release-promotion-model/reports/20260729-002311-review-shaping-round-1-codex.html new file mode 100644 index 000000000..555f8d8af --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/reports/20260729-002311-review-shaping-round-1-codex.html @@ -0,0 +1,136 @@ + + +Review — shaping round 1 (Codex) + + +
+
release-promotion-model + receipt-tree-binding · reviews · shaping round 1 · Codex
+

Shaping Review — Round 1 (Codex)

+

reviewed 2026-07-28/29 · over shape commits 513be735 (release-promotion-model, working tree) and 349bbd3d (receipt-tree-binding, via git show) · verdict REQUEST-CHANGES · 2 blockers + 5 major + 1 minor · all eight confirmed · dispositions applied same round, amended in place (shaping commits stay local)

+ +
+

Provenance correction — 2026-07-29

+

This round was originally attributed to Codex GPT-5.4. The Codex runtime's job registry later proved the dispatch never executed there: the original forward died to machine sleep two minutes in with no output, and the retry's report was produced by the rescue wrapper agent itself — a Claude-native review, not a Codex one — in silent violation of its thin-forwarder contract. The findings stand: every one was independently verified against source before boarding, and round 3 attested the fixes. Attribution corrected; the filename keeps its stamped identity. Discovery record: journal discover(review) 2026-07-29.

+
+ +
+

Eight findings, eight confirmed, zero refuted — and the two blockers are the same wound. A design decision made in conversation after the shape was committed (changeset-pattern release notes, buffer retirement) never re-entered the document, and the write path it governs (writeReleaseChangelog) belonged to no task. The round's fix gives both a single owner: Decision 3 rewritten around release-notes.md, and a new TASK-008 owning note collection, the rung-aware write path, buffer retirement, and the workflow-pre-pr re-point. Codex also independently cleared the risks flagged in the brief: no package cycles (single package cli), no test-name collisions, the rc-cohort core-literal mapping is sound, ~15 spot-checked line citations accurate, and validate-push's regex is already channel-agnostic.

+
+ +

Findings

+ +
+
R1-1blocker"rc cuts touch no changelog" had no implementing task — the write path was never made channel-awareconfirmedfixed → TASK-008
+

Evidence

runReleaseApply calls writeReleaseChangelog unconditionally (release_dry_run.go:481), preview at :288-294; TASK-002 owned flags only, TASK-004 the stable path only, TASK-005 post-merge verification only — no task owned the writer, so an rc cut would still splice a section, contradicting the shape's own Problem #3.

Disposition

TASK-008 created owning the rung-aware write path (rc writes nothing, alpha/beta aggregate notes); Per-channel guardrails subsection now states the writer is TASK-008's and the guardrails verify what it produces.

+
+ +
+
R1-2blockerThe logged release-notes/buffer-retirement decision was silently dropped from the shapeconfirmedfixed → Decision 3 + TASK-008
+

Evidence

Journal decision(shape) 2026-07-28 19:21 settled per-change release-notes.md, buffer retirement, and the pre-PR hook re-point; the coherence amendment (a8dd5ad9→513be735) carried only the council edits; grep for release-notes.md across the tree returned zero; runNativeWorkflowPrePR (check.go:834) still blocks on an empty [Unreleased] untouched by either shape.

Disposition

Folded in wholesale: Decision 3 rewritten around the changeset pattern, Scope gains the release-notes bullet, Observable Workflow shows the fragment, Rabbit Holes and TASK-004's seed material re-seed from notes (shape-lines demoted to warned fallback), TASK-007 teaches the authoring discipline, TASK-008 owns mechanics + hook re-point, V6 added (TestReleaseNotesProjection).

+
+ +
+
R1-3majorTASK-004 violated its own scope boundary on CI-green-at-HEADconfirmedfixed → shared-helper ownership
+

Evidence

TASK-004's Out assigned non-promotion guardrails to TASK-005 while its own Steps implement the Decision 11 check for "rc cut and promotion"; TASK-005 never mentioned it — implementer-in-isolation risk in both directions.

Disposition

TASK-004's Out now names the helper explicitly: one shared check owned here covering both rc cut and promotion; TASK-005's Context pointers state it consumes and never reimplements.

+
+ +
+
R1-4major"--post-merge exclusivity unchanged" would silently accept --promote/--channelconfirmedfixed → explicit list addition
+

Evidence

The incompatibility list (release_dry_run.go:163-200) checks fields that exist today; taken literally, "unchanged" leaves --post-merge --promote release accepted while the same sentence says post-merge takes no version input.

Disposition

Shape and TASK-002 now require adding both flags to the list, with a rejection test.

+
+ +
+
R1-5majorTASK-002 backtick-quoted a nonexistent flag value: `--bump core`confirmedfixed → literal vocabulary
+

Evidence

No core in releaseValidBumps (release_dry_run.go:87-93); TASK-001 used "core bump" as category language, TASK-002 promoted it to CLI syntax.

Disposition

Now reads --bump major|minor|patch --channel alpha, matching the Observable Workflow.

+
+ +
+
R1-6majorGate-at-rc pulls the sweep carrier's whole lift before rc.1 — undocumentedconfirmedfixed → documented in Sequencing
+

Evidence

loaf change list --target 2.0.0 shows spec-conversion-and-guidance-sweep captured-only (brief + change.json); under the new trigger set its convert-first block fires at 2.0.0-rc.1 instead of stable, and neither shape said so.

Disposition

Sequencing now names the consequence and frames it as the discipline working as designed: an rc claims stable intent, alphas/betas flow regardless, the rc block is never a regression.

+
+ +
+
R1-7majorThe cross-change dependency (boundary constant) has no structural encodingconfirmedaccepted — by design
+

Evidence

Task relations are change-local by the work model's own rule; promotion's TASK-004 cannot declare blocked-by on receipt-tree-binding/TASK-001, so derived executability cannot see the gap.

Disposition

Accepted: cross-change relations are deliberately forbidden — cohort membership and git order carry sequencing. The prose carrier is now explicit: promotion's Sequencing states TASK-004 does not start until receipt-tree-binding lands on main.

+
+ +
+
R1-8minorCouncil board filename uses kind `council`, outside the closed report registryconfirmedaccepted — routed to Intent
+

Evidence

The registry (change_report.go:15-21) is approval/review/visual/audit/note; loaf change report new would refuse council; the board was hand-authored under the owner's new convention and no hook re-validates existing filenames.

Disposition

Accepted for now: the convention is owner-directed and newer than the registry. Adding council to the closed registry (and letting CLI stamping own the shell) is already scoped in INTENT-20260728-council-reports-become-visual-html-boards-in-the-change-folder-s-reports-directory.

+
+ +

Cleared by the round (checked, not defects)

+
+
    +
  • No package cycles for the exported boundary constant — every file involved is package cli; the "import" is an intra-package reference.
  • +
  • No test-name collisions: none of the declared V-entry prefixes exist in internal/cli today.
  • +
  • The rc-cohort mapping (strip prerelease, then existing byte-equality) composes with change_release_gate.go:50-57, not against it.
  • +
  • ~15 spot-checked file:line citations across both shapes accurate, including the three-way help drift (cli_reference.go:90 still says "during a lineage freeze").
  • +
  • validate-push's release-commit regex already matches prerelease and rc subjects — no hook change needed there.
  • +
+
+ +
Authored snapshot — shaping round over both change contracts pre-implementation (Claude-native per the provenance correction above); every finding independently re-verified before boarding; fixes amended into the local shaping commits per convention. Finding IDs: release-promotion-model/20260729-002311/R1-n.
+
diff --git a/docs/changes/20260728-release-promotion-model/reports/20260729-004327-review-shaping-round-2-sol.html b/docs/changes/20260728-release-promotion-model/reports/20260729-004327-review-shaping-round-2-sol.html new file mode 100644 index 000000000..8ea228090 --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/reports/20260729-004327-review-shaping-round-2-sol.html @@ -0,0 +1,143 @@ + + +Review — shaping round 2 (GPT 5.6 Sol) + + +
+
release-promotion-model · reviews · shaping round 2 · GPT 5.6 Sol
+

Shaping Review — Round 2 (GPT 5.6 Sol, xhigh)

+

reviewed 2026-07-29 · over the round-1 remediation delta 513be735 → 09d36213 plus cross-links to receipt-tree-binding 349bbd3d · verdict REQUEST-CHANGES · 2 blockers + 2 major + 1 minor (+1 observation) · all confirmed · dispositions applied same round, amended in place

+ +
+

Provenance correction — 2026-07-29

+

This round was originally attributed to GPT 5.6 Sol at xhigh effort (the owner's selection). The Codex runtime's job registry later proved no dispatch from this round ever executed there — and the round-4 attempt established that gpt-5.6-codex-sol is rejected outright under this machine's ChatGPT-account auth, so a Sol run could never have happened. The report was produced by the rescue wrapper agent itself — a Claude-native review, not a Sol one — in silent violation of its thin-forwarder contract. The findings stand: all were independently verified at boarding, and round 3 attested every disposition LANDED-EXACT. Attribution corrected; the filename keeps its stamped identity. Discovery record: journal discover(review) 2026-07-29.

+
+ +
+

The round earned its keep: it caught the fold-in contradicting itself inside the very file it fixed. Round 1's blockers are functionally addressed (both attested), but the release-notes fold-in left the residue class this round was hunting: an undeclared function-level dependency (TASK-004 calls TASK-008's helper with no blocked-by and inverted Sequencing prose) and three surviving buffer-reset obligations — one of them a literal V-contract test requirement — contradicting the very retirement Decision 3 declares. The escape-surface gap (F3) was upgraded from "tag it open" to decided: the no-impact declaration lives inside release-notes.md itself, one auditable carrier, never a change.json field or PR-body text.

+
+ +

Findings

+ +
+
R2-1blockerTASK-004 calls TASK-008's collection helper with no declared dependency; Sequencing built them in the wrong orderconfirmedfixed
+

Evidence

TASK-004's Objective: seed material "per TASK-008's collection"; TASK-008's Out: "this task provides the collection helper it calls" — yet TASK-004 declared blocked-by: [TASK-001] only, and Sequencing ordered promotion mechanics before the notes projection. TASK-007 shows the convention applied correctly for the analogous case.

Disposition

TASK-008 added to TASK-004's blocked-by; Sequencing rewritten: gate placement and the notes projection first, promotion mechanics follows the projection, guardrails verify what the projection produces.

+
+ +
+
R2-2blockerThree surviving buffer-reset obligations contradict the retirement — including a V-contract test requirementconfirmedfixed
+

Evidence

Decision 9 ("buffer reset"), V4 ("unreleased-buffer reset" — an encoded TestReleaseDesignation obligation), and TASK-004's Steps ("[Unreleased] reset to stub") all survived the fold-in that removed the same clause from TASK-004's Objective three lines above — inconsistency inside the targeted file itself.

Disposition

All three struck. The retirement is now asserted nowhere-contradicted: Decision 3 and TASK-008 own it, no validator or step demands a buffer that no longer exists.

+
+ +
+
R2-3majorThe no-user-facing-impact escape had no mechanical carrier and wasn't tagged openconfirmeddecided → note-file carrier
+

Evidence

Three spots asserted the escape as settled with no surface defined (not a change.json field, not a note marker, not hook-readable PR text); the existing isWorkflowReleaseEscape is a different, git-derived mechanism; per-PR touched-folder detection was never specified.

Disposition

Decided rather than deferred: the carrier is release-notes.md itself — a no-impact change still writes the note containing the explicit declaration; one surface, auditable in the folder, skipped by aggregation. Touched-folder detection reuses the isReleaseOnlyPR diff pattern, now named in TASK-008's contract; TASK-007 teaches the declaration form and is barred from inventing an alternate carrier. Logged as a shape decision.

+
+ +
+
R2-4minorRound-1 board's "folded in wholesale" overclaims — R2-1/R2-2 are that fold-in's direct falloutconfirmedrecorded here
+

Disposition

The R1 board is an authored snapshot and stays unedited per convention; this board is the correction of record: R1-2's remediation was partial at the time that board was written — the decision content landed, the DAG and residual references did not. Read the two boards together.

+
+ +
+
R2-5observationThe 2.0.0 rollup will be largely fallback-sourced — the proving release exercises the new mechanism leastconfirmedacknowledged in H4
+

Disposition

H4 now states it: the first rollup leans on the shape.md-lines fallback because the note convention is newer than most of its cohort — so H4 judges the fallback path exactly where it will dominate. Nothing breaks; the framing is now honest.

+
+ +

Remediation attestation (round 1 → round 2)

+
+ + + + + + + + + + +
R1 findingAttestationNote
R1-1 write path unownedconfirmed-fixedTASK-008 owns it cleanly
R1-2 release-notes decision droppedpartial → now closedcontent landed; R2-1/R2-2 fallout fixed this round
R1-3 CI-green ownershipconfirmed-fixedstated consistently in both task files
R1-4 post-merge exclusivityconfirmed-fixedrejection test declared
R1-5 phantom `--bump core`confirmed-fixedno survivors; category language legitimate
R1-6 sweep carrier before rc.1confirmed-fixedindependently verified against gate block ordering
R1-7 cross-change dependency prose-onlyaccepted stands
R1-8 council report kindaccepted standsrouted to Intent
+
+ +

Cleared by the round

+
+
    +
  • Sweep-carrier paragraph verified against gate code: legacy/captured members hit convert-first before receipt checks, exactly as Sequencing states.
  • +
  • Guardrail-6 extraction is content-source-agnostic — no collision with the buffer retirement; the curated-content-wins precedence is explicitly owned and retired by TASK-008.
  • +
  • rc GitHub-notes fallback is TASK-005's alone — no overlap with TASK-008's write-path claim.
  • +
  • All remediation-introduced line citations byte-accurate (one uncited extra call site at release_dry_run.go:432, covered by TASK-008 claiming the function by name).
  • +
  • V6/V7 renumbering consistent; no test-name collisions; the receipt-tree-binding boundary cross-link unchanged and verbatim-consistent; release-notes.md correctly outside the evidence-boundary exclusion set (a note edit should expire a stale receipt).
  • +
+
+ +
Authored snapshot — round over the round-1 remediation (Claude-native per the provenance correction above); every finding independently re-verified before boarding; fixes amended into the local shaping commit per convention. Finding IDs: release-promotion-model/20260729-004327/R2-n.
+
diff --git a/docs/changes/20260728-release-promotion-model/reports/20260729-010126-review-shaping-round-3-reviewer.html b/docs/changes/20260728-release-promotion-model/reports/20260729-010126-review-shaping-round-3-reviewer.html new file mode 100644 index 000000000..2c2a40d90 --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/reports/20260729-010126-review-shaping-round-3-reviewer.html @@ -0,0 +1,137 @@ + + +Review — shaping round 3 (loaf:reviewer) + + +
+
release-promotion-model · reviews · shaping round 3 · loaf:reviewer
+ + +

Shaping Review — Round 3 (loaf:reviewer, approval-gate re-read)

+

reviewed 2026-07-29 · loaf:reviewer profile, read-only, session model · targeted re-read of the R2 delta 09d36213 → 93ca01cb + fresh-eyes structural sweep + tree-binding cross-links · verdict REQUEST-CHANGES · 2 blockers + 3 major + 5 minor · all confirmed · all four R2 dispositions attested LANDED-EXACT · dispositions applied same round, amended in place

+ +
+

The R2 fixes held; the round's finds were elsewhere. B1 is the R2-1 defect class recurring in a file R2 never re-walked — TASK-005 consuming two helpers whose tasks it didn't declare — and B2 is a three-round survivor visible only by counting: "the cycle's prerelease sections" was undefined against a CHANGELOG holding 64 2.0.0-* sections across three label families (dev.*, pre.*, alpha.*), with no prior stable to anchor "the cycle." Both closed with frontmatter and one defining sentence; the majors were ownership lines, not redesign. The reviewer also ran loaf change check itself (isolated DB): executable, zero findings.

+
+ +

Findings

+ +
+
R3-1blockerTASK-005 consumes TASK-004's and TASK-008's helpers with no declared dependency (R2-1 class, one file over)confirmedfixed
+

Disposition

TASK-005 now blocked-by: [TASK-002, TASK-004, TASK-008]; TASK-007 gains TASK-008 (it teaches a TASK-008 mechanic). Derived executability now matches the Sequencing prose everywhere.

+
+ +
+
R3-2blocker"The cycle's prerelease sections" undefined — 64 live 2.0.0-* sections in three label families, two contradictory readings in one documentconfirmeddecided → collapse set defined
+

Disposition

Contract sentence added: the collapse set is sections whose label is a recognized rung (alpha/beta/rc) AND whose core equals the candidate — 2.0.0's promotion collapses 2.0.0-alpha.* (+ future beta/rc) and leaves the legacy dev.*/pre.* history untouched, matching the Observable Workflow's narrower phrasing. TASK-004's tests now name the defined set; V4 references it.

+
+ +
+
R3-3majorThe buffer's physical removal was unowned; insertReleaseChangelog silently bottom-appends without its anchor; two scaffolds still emit the bufferconfirmedfixed → TASK-008 owns all of it
+

Disposition

TASK-008's delivering commit now owns: live CHANGELOG removal (heading, stub, preamble), rewritten anchor semantics (sections land above the previous top release; missing anchor is the new normal, the silent bottom-append at release_dry_run.go:1136-1138 dies), and both scaffolds (createReleaseChangelog, the loaf init template at init.go:274-283 — no new project born with a retired concept). V6 gains placement assertions.

+
+ +
+
R3-4majorDecision 11's CI-green block had no declared test; the rc-cut warning was verified by a post-merge-named suiteconfirmedfixed
+

Disposition

V4 and TASK-004's test list gain the CI-green-at-HEAD refusal fixture; V5's description broadened to cover the rc-cut feat warning explicitly (suite name unchanged — it is TASK-005's prefix, not a scope claim).

+
+ +
+
R3-5majorThe no-impact declaration's machine-readable token was undefined — and assigned to the last-sequenced taskconfirmeddecided → exact token
+

Disposition

Defined in TASK-008 (the parser's owner), taught by TASK-007: a note whose entire content is the single line No user-facing impact. Decision 3 carries the form; the skill teaches, never redefines.

+
+ +
+
R3-6minorFive minors: blocks-reciprocity ×3, vacuous cross-task check, formula-script overclaim, resolveReleaseSnapshot wayfinding, exclusion-set/allowlist conflationconfirmedall fixed
+

Disposition

Reciprocal blocks: added (TASK-002→008, TASK-004→005/007, TASK-008→004/005/007); TASK-008's vacuous "still green" check replaced with an honest after-both-land statement plus placement assertions; the CI-and-tap contract corrected (--formula already exists and is required — the class name at :27 is the only real work); TASK-002 now names change_release_gate.go:199 and declares the symbol-level share with TASK-003; the gate-placement sentence and TASK-004's import step now name releaseMetadataAllowlist specifically, never the full digest-exclusion set.

+
+ +

R2 disposition attestation (by this round)

+
+ + + + + + +
R2 findingAttestation
R2-1 TASK-004→TASK-008 dependency + Sequencing orderLANDED-EXACT (edge derived, DAG acyclic)
R2-2 three buffer-reset strikesLANDED-EXACT (all 14 remaining buffer mentions intentional-descriptive)
R2-3 escape carrierLANDED-EXACT on the carrier; token gap carried as R3-5, now closed
R2-5 H4 fallback acknowledgmentLANDED-EXACT
+
+ +

Cleared by the round

+
+
    +
  • loaf change check run by the reviewer against an isolated DB: exit 0, layout new, state executable, findings/warnings/gaps all empty.
  • +
  • Tree-binding cross-links verbatim-consistent (Decision 2 boundary language, the strictly-smaller divergence rule, RTB's Out ceding CI-green/rc-gate/designation to this change).
  • +
  • Every load-bearing citation spot-checked byte-accurate, including isReleaseOnlyPR at check.go:1236 with the diff at :1249, the prerelease bypass, the post-merge keying, both guardrails, the create-fallback's missing --prerelease, and the release skill's zero mentions of --post-merge/target_release/receipts.
  • +
  • The 2.0.0 cohort confirmed at three members with the sweep carrier the only captured-only one.
  • +
+
+ +
Authored snapshot — loaf:reviewer (read-only, session model) approval-gate re-read per the round-7 precedent; all ten findings dispositioned and amended into the local shaping commit same round. Finding IDs: release-promotion-model/20260729-010126/R3-n.
+
diff --git a/docs/changes/20260728-release-promotion-model/reports/20260729-013606-review-shaping-round-4-sol.html b/docs/changes/20260728-release-promotion-model/reports/20260729-013606-review-shaping-round-4-sol.html new file mode 100644 index 000000000..e2cdcd2b2 --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/reports/20260729-013606-review-shaping-round-4-sol.html @@ -0,0 +1,109 @@ + + +Review — shaping round 4 (gpt-5.6-sol) + + +
+
release-promotion-model · reviews · shaping round 4 · gpt-5.6-sol
+

Shaping Review — Round 4 (gpt-5.6-sol, xhigh — attestation pass)

+

reviewed 2026-07-29 · gpt-5.6-sol at xhigh effort, fresh thread · the sequence's first registry-verified Codex run — job task-ms5cd6jw-hloj79, Codex session 019fab41-6bd6-7690-bc54-e6642b932754, 6-minute pass · attesting the R3 disposition delta 93ca01cb → f33db023 · verdict REQUEST-CHANGES · 0 blockers + 0 major + 1 minor · fixed same round, amended in place

+ +
+

Convergence. Nine of ten R3 dispositions attested LANDED-EXACT with byte-accurate citations (the all-eight task graph checked reciprocal and acyclic; the collapse-set, token, buffer-removal, and CI-green fixes verified in all their stated locations; every code citation from the R3 remediation re-verified against source, including the silent bottom-append at release_dry_run.go:1136 and the init scaffold at init.go:274). The fresh-eyes sweep found zero new findings, the tree-binding cross-links remain semantically aligned (X = {receipts, reports} ∪ releaseMetadataAllowlist), and the reviewer's own loaf change check run exited 0, executable. The single surviving item was naming precision, fixed below.

+
+ +

The one finding

+ +
+
R4-1minorR3's allowlist-naming disposition DRIFTED: TASK-004 got the literal symbol, three shape.md spots did notconfirmedfixed
+

Evidence

TASK-004 landed "importing receipt-tree-binding's releaseMetadataAllowlist specifically (the allowlist alone, never the full digest-exclusion set…)", but shape.md's Gate placement still said only "the release-metadata allowlist … imported from the receipt-tree-binding predecessor", and ambiguous "boundary constant" phrasing survived in the Out bullet and Promotion mechanics — contradicting the R3 board's "named specifically at both import sites" claim.

Disposition

All three shape.md sites now name releaseMetadataAllowlist literally and state the allowlist-vs-full-exclusion-set distinction; no ambiguous "boundary constant" phrasing survives in the promotion contract.

+
+ +

Attestation table

+
+ + + + + + + + + + + + +
R3 dispositionAttestation
R3-1 task DAG completion + reciprocityLANDED-EXACT (reciprocity ok, acyclic, congruent with Sequencing)
R3-2 collapse-set definitionLANDED-EXACT (three phrasings congruent)
R3-3 buffer removal + anchor semantics + scaffoldsLANDED-EXACT (all citations byte-accurate)
R3-4 CI-green fixture + rc-cut warning coverageLANDED-EXACT
R3-5 no-impact tokenLANDED-EXACT (token byte-identical in all three documents)
minor: reciprocal blocksLANDED-EXACT
minor: vacuous cross-check replacedLANDED-EXACT (suite confirmed absent at f33db023)
minor: formula-script claimLANDED-EXACT (verified against the script source)
minor: resolveReleaseSnapshot wayfindingLANDED-EXACT (declaration confirmed at change_release_gate.go:199)
minor: releaseMetadataAllowlist namingDRIFTED → fixed this round (R4-1)
+
+ +

Sequence state

+
+

Four rounds: R1 (2B+5M+1m) → R2 (2B+2M+1m) → R3 (2B+3M+5m) → R4 (0B+0M+1m, fixed in-round). Every finding across all rounds confirmed, dispositioned, and attested by a later round; the R4 fresh sweep found nothing new. Rounds 1–2 carry provenance corrections (Claude-native wrapper executions, discovered and corrected 2026-07-29); rounds 3–4 are correctly attributed from birth, and R4 is the first round verified against the Codex runtime's own job registry at boarding time.

+
+ +
Authored snapshot — gpt-5.6-sol xhigh attestation pass; the single finding fixed and amended into the local shaping commit same round. Finding IDs: release-promotion-model/20260729-013606/R4-n.
+
diff --git a/docs/changes/20260728-release-promotion-model/shape.md b/docs/changes/20260728-release-promotion-model/shape.md new file mode 100644 index 000000000..618de9f1f --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/shape.md @@ -0,0 +1,177 @@ + + +# Release Promotion Model — Channels, Designation-Commit Promotion, Gate at RC + +## Problem + +The release machinery has exactly one channel concept: the `releaseVersionIsPrerelease` predicate (`release_dry_run.go:876`). Everything a real prerelease cycle needs is missing or wrong: + +1. **The label can never transition.** `bumpReleaseVersion` preserves the prerelease label verbatim — there is no alpha→beta→rc path, and no stable→prerelease entry at all (`bumpReleaseVersion("2.0.0","prerelease")` returns `""`), so opening 2.1.0-alpha.1 after 2.0.0 stable is impossible. +2. **Nothing controls what an rc contains.** The cohort gate fires only when the candidate is stable; every prerelease bypasses (`change_release_gate.go:43-48`). An rc — whose semantic contract is "this exact content intends to be the stable" — is cut with zero evidence checks, so stabilization never formally opens. +3. **The changelog is channel-blind.** Every release, rc included, must splice a `## [version]` section (post-merge guardrails 5/6), forcing curated changelog ceremony onto disposable candidates and leaving the stable section as just another entry instead of the authoritative rollup. +4. **Distribution is channel-blind.** CI updates the Homebrew formula unconditionally on any `v*` tag and omits `--prerelease` on its create-fallback; the moment a stable exists, the next alpha would point the tap at a prerelease and `brew upgrade` would hand stable users an alpha. +5. **Guidance is stale.** The release skill predates the gate entirely (never mentions `--post-merge`, `target_release`, receipts), and the `--bump` help text has three-way drift including retired lineage-freeze language (`cli_reference.go:90`). + +## Hypothesis + +Encoding channels as alpha→beta→rc→stable-with-stable-as-a-promoted-rc gives the release the same evidence discipline the change work model gave the work: the gate opens stabilization at rc cut, promotion is a designation event whose byte-identity is mechanically enforced on the code tree, the stable changelog becomes the authoritative rollup users actually need, and stable consumers can never be handed a prerelease. If this ships before 2.0.0, the first stable in the project's history ships through the promote path — the model proves itself on the release it was designed for. + +## Scope + +**In** + +- Rung model in the version primitives: parse prerelease label + numeric suffix, ladder ordering alpha < beta < rc, cycle entry, rung advance, promotion math. +- CLI surface: `--promote [rung|release]`, `--channel` as outcome assertion, `--bump release` retirement, flag validation matrix, help-surface sync across all three copies (including removing stale lineage-freeze text). +- Gate placement: cohort gate fires on rc candidates (mapped to the cohort by core literal) and at promotion; alpha/beta remain the valve; post-merge gates by prepared rung. +- Designation-commit promotion: diff-since-last-rc restricted to version files + CHANGELOG, stable rollup derivation seeded from cohort `shape.md` lines, collapse of the cycle's prerelease sections, mechanical changelog validation. +- Changeset-pattern release notes: each change authors its user-facing fragment as `release-notes.md` in its own folder (never deleted, archived with the change); consumption derives from the release range plus execution provenance; CHANGELOG.md becomes a cut-time projection; the `[Unreleased]` buffer retires; the `workflow-pre-pr` hook re-points to per-change note presence with a no-user-facing-impact escape. +- Per-channel changelog behavior, write path included: alpha/beta cuts aggregate ranged notes into their sections, rc cuts touch no changelog (the writer is rung-aware, not just the guardrails), feat-during-rc preflight warning, rc GitHub Release notes auto-derived from the commit range. +- Channel-routed distribution: CI derives the channel from the tag, `--prerelease` on the create-fallback, `loaf` formula tracks stable only, `loaf-dev` formula tracks the latest prerelease. +- Release skill rewrite: channel ceremony, `/promote` flow, human channel choice with agent suggestion, current CLI vocabulary (gate, receipts, post-merge). + +**Out** (deferred, not rejected) + +- Receipt content binding — standalone predecessor Change (`docs/changes/20260728-receipt-tree-binding/`, shaped from `INTENT-20260728-receipt-freshness-must-survive-squash-merges-bind-to-tree-content-not-commit-sha`, gating scope decided by council 2026-07-28): receipts bind to a masked root-tree digest whose exclusion set includes the release-metadata allowlist, so designation-legal ⟺ receipt-neutral. This Change imports its exported `releaseMetadataAllowlist` for the designation check — the allowlist alone, never the full digest-exclusion set, which also masks receipts/ and reports/. +- Capability-evidence decoupling — its own Intent (`INTENT-20260728-capability-evidence-decouples-from-binary-hashes-and-exact-client-versions`). +- Concurrent maintenance branches and overlapping cycles (a 1.x patch train while 2.x stabilizes) — revisit when a maintenance need is real; the model assumes one active cycle on main. +- npm dist-tag channel routing — Loaf does not publish to npm today; becomes an Intent if that changes. + +**Cut** (explicitly rejected) + +- Backward channel moves (rc→beta). Semver orders `beta < rc`, so a later backward cut sorts older than what is already released; blocked hard, no override. +- Tag-only promotion and artifact promotion. Rejected in interview: both make some surface lie about the version (guardrail 4, CI's tag-vs-package.json verify, the Homebrew formula's `loaf --version` assertion, or the binary's self-report). +- CI-authored promotion commits. The gate reads committed evidence; a bot commit to main outside the PR flow relocates the authorship question without answering it. +- A full semver-precedence engine. Only rung-rank comparison on the recognized ladder is needed. +- Prerelease-expressible `target_release` literals. Cohorts target stables only, by design; rc gating maps the candidate to the cohort, never the cohort to a prerelease. + +## Observable Workflow + +``` +# open the next cycle from stable 2.0.0 +loaf release --bump minor --channel alpha # → 2.1.0-alpha.1 + +# iterate within the channel +loaf release --bump prerelease # → 2.1.0-alpha.2 + +# advance the ladder +loaf release --promote # → 2.1.0-beta.1 +loaf release --promote # → 2.1.0-rc.1 ← cohort gate fires here +loaf release --promote rc # from alpha: deliberate skip, warns + +# promote to stable (explicit, rc-only) +loaf release --promote release # designation commit: version files + CHANGELOG rollup + regenerated build outputs +loaf release --post-merge # tags v2.1.0, publishes, `loaf` formula updates + +# guarded automation +loaf release --promote --channel beta # asserts the result lands on beta, else refuses before mutating + +# every change authors its user-facing fragment in its own folder; cuts aggregate them +docs/changes/20260801-some-feature/release-notes.md # [Unreleased] buffer retired; CHANGELOG is a cut-time projection +``` + +An rc cut touches version files only — its GitHub Release notes are auto-derived from the commit range, CHANGELOG.md is untouched, and cutting an rc whose range contains `feat:` commits warns "new development during rc; consider cutting a beta." At promotion the CHANGELOG diff shows the cycle's alpha/beta sections collapsing into one authoritative `## [2.1.0]` rollup. `brew install levifig/tap/loaf` only ever resolves stable; `loaf-dev` tracks the latest prerelease for dogfooding. + +## Rabbit Holes and No-Gos + +- **Do not grow rung math into semver precedence.** The ladder is three known labels and a numeric suffix; unknown labels iterate with `--bump prerelease` but refuse `--promote`. Full identifier-by-identifier comparison is rejected ceremony. +- **Do not let the rollup become generation.** The CLI seeds material (the cycle's `release-notes.md` fragments, the prerelease sections, and `shape.md` Problem/Hypothesis lines only as a warned fallback for note-less changes) and validates the result; the agent judges and writes. Auto-generating the stable changelog from commit subjects is the failure mode Common Changelog curation exists to prevent. +- **Do not relax guardrail 4 anywhere.** Tag equals version files stays absolute on every path; the promotion design was chosen specifically so no surface needs to lie. +- **Do not redefine receipt freshness here.** The receipt-tree-binding predecessor owns the digest, the predicate, and the evidence-boundary constant; this Change imports the boundary for its designation check and never forks it. If the two sets ever need to diverge, the digest exclusion stays the strictly smaller one (council: Correctness). + +## Decisions + +Provenance: owner-settled design in `INTENT-20260728-release-promotion-model-alpha-beta-rc-and-stable-as-a-promoted-rc` (2026-07-28), refined in the shaping interview of the same day (journal `decision(shape)` entries); code facts from the release-surface survey of this session. + +1. **Promotion is a designation commit.** One commit touching only release metadata — version files, CHANGELOG, and the tracked regenerated build outputs the version injection rewrites — tagged and published on existing rails; the preflight blocks if the diff since the last rc touches anything else. Byte-identity attaches to the source tree; artifacts rebuild deterministically with the honest stable version string. Forecloses tag-only and artifact promotion, and any guardrail-4 relaxation. +2. **The gate fires at rc cut and re-checks at promotion.** An rc candidate gates the cohort matching its core literal; alpha and beta remain the valve. Stabilization formally opens when the cohort is executed and receipt-verified. +3. **Changelog policy is per-channel, authored as changeset-pattern release notes.** Each change carries its user-facing fragment as `release-notes.md` in its own folder — authored while context is hot, never deleted, archived with the change; consumption is derived from the release range plus execution provenance, so a note ships only when its work actually landed. CHANGELOG.md becomes a cut-time projection: alpha/beta cuts aggregate the ranged notes into their sections, rcs touch no changelog, and the stable rollup re-aggregates the cycle's notes at promotion, collapsing the prerelease sections. The `[Unreleased]` buffer retires, and `workflow-pre-pr` re-points to per-change note presence; the no-user-facing-impact escape's carrier is the note file itself — a change with nothing user-facing still writes `release-notes.md`, and the declaration's machine-readable form is exact: the note's entire content is the single line `No user-facing impact.`, recognized mechanically by the collection helper and skipped by aggregation. Grounded in industry practice (the stable document is authoritative, prerelease notes are disposable delta notes) and in the buffer's real failure mode: a single global section every PR edits is pure merge contention. +4. **The surface is `--bump` iterates, `--promote` advances, `--channel` asserts.** Bare `--promote` walks one rung; a named rung is a warned skip; `release` must be named explicitly and requires the current version to be an rc. `--channel` never selects a move — it guards the outcome, refusing before mutation when the computed result lands elsewhere; it is required only at cycle entry. `--bump release` retires with an error pointing at `--promote release`. +5. **Direct stable bumps survive as the hotfix path.** `--bump patch` from 2.0.0 producing 2.0.1 stays legal (and gates its cohort as today); the promoted-rc rule governs how cycles end, not whether every stable needs a cycle. +6. **Forward-only is enforced hard.** Backward channel cuts sort older than existing releases under semver precedence; there is no override flag. +7. **Distribution routes by channel with two formulae.** `loaf` tracks stable only; `loaf-dev` in the same tap tracks the latest prerelease; CI routes by the tag's channel. Stable users can never be upgraded onto a prerelease; brew-based dogfood continues via `loaf-dev`. +8. **Alpha is kept over dev.** Semver prerelease identifiers compare ASCII-alphabetically; `dev` would sort after `beta` and mis-order in any range-resolving tooling. (Owner decision, restated from the Intent.) +9. **Agent judges, CLI performs.** Channel choice is human with agent suggestion (cohort states, criteria green-ness, dogfood time — surfaced via AskUserQuestion in the release skill); the rollup is judged agentically and validated mechanically (section exists, version matches, non-empty, sections collapsed). Entry-level tracing stays H-tier review. +10. **Receipt tree-binding is a predecessor, not a passenger.** It must land before the sweep carrier verifies on its branch, independent of this Change's timeline; bundling would couple an urgent correctness fix to a design change. Its council settled the gating scope (masked root-tree digest, exclusion set = receipts ∪ reports ∪ release-metadata allowlist), and this Change consumes the exported boundary for the designation check. +11. **Tag creation requires CI green at HEAD.** Adopted council rider (Release Engineering): rc cut and promotion refuse to tag unless a successful CI run exists for the exact HEAD SHA (`gh run list --commit`), blocking with a named remedy when absent. Closes the tag-before-test ordering hole; the gate still executes nothing itself — a CI conclusion keyed by commit SHA is committed-adjacent evidence read at cut time. + +## Planning Contract + +### Rung model + +Extend the hand-rolled version primitives (`release_dry_run.go:865-902`): split the opaque prerelease string into label + numeric suffix at the last dot (dotless or non-numeric suffixes keep today's reset-to-`.1` behavior on iteration). Recognized ladder: `alpha` (1) < `beta` (2) < `rc` (3); unknown labels are not rungs — `--bump prerelease` still iterates them, `--promote` refuses with a named reason. New moves: cycle entry (core bump + `alpha.1`), rung advance (next or named rung, counter resets to `.1`), promotion (rc → bare core). `releaseSemverLess` stays core-only for cohort warnings. + +### Surface and snapshot + +`releaseSnapshot` gains the move kind and asserted channel; `--promote` is mutually exclusive with `--bump`, composes with `--dry-run`/`--yes`/`--base`/`--tag`/`--no-gh`; `--channel` composes with `--bump` and `--promote` and is validated after candidate computation, before any mutation. `--promote` and `--channel` are added to `--post-merge`'s incompatibility list (`release_dry_run.go:163-200`) alongside `--bump` — post-merge takes no version-shaping input; it reads the prepared version and derives its rung. All three help surfaces (`release.go`, `agent_help.go`, `cli_reference.go`) rewritten together, retiring the stale lineage-freeze text. + +### Gate placement + +The trigger set becomes: candidate is stable (byte-equal cohort match, as today) OR candidate's rung is rc (cohort = candidate's core literal). Alpha/beta candidates keep the valve semantics with warnings. Post-merge keys the same logic on the prepared version's rung: rc and stable gate, alpha/beta flow. Promotion preflight re-checks the cohort against the same committed evidence; because receipts bind to the masked root-tree digest whose exclusion set includes receipt-tree-binding's `releaseMetadataAllowlist` alongside its receipts and reports masks, release-metadata commits are receipt-neutral — the rc release commit and the designation commit cannot expire cohort receipts, so promotion re-check passes by construction. Verification's rhythm follows: a receipts sweep at rc cut (receipts are masked, so N receipts land in one commit without staling each other), a re-sweep after rc bugfixes, nothing at promotion. + +### Promotion mechanics + +Locate the last rc: highest `vX.Y.Z-rc.N` tag for the candidate core (promotion refuses when none exists — stable is a promoted rc). Designation check: `git diff ..HEAD --name-only` must stay within receipt-tree-binding's exported `releaseMetadataAllowlist` (version files, CHANGELOG.md, and the tracked regenerated build outputs that version injection rewrites — the allowlist alone, never the full digest-exclusion set), consumed here so designation-legal and receipt-neutral can never drift apart — else block with the offending paths and the remedy "code changed since rc.N; cut another rc". Both rc cut and promotion additionally require a successful CI run at the exact HEAD SHA before tagging (Decision 11). Content-level inspection of regenerated outputs is rejected ceremony: source-path changes are caught by their own paths, and the existing drift check proves generated outputs match source. Rollup: the CLI computes seed material — the `release-notes.md` fragments of every change whose work landed in the cycle (range plus execution provenance; `shape.md` Problem/Hypothesis lines as a warned fallback for note-less changes) and the cycle's prerelease sections — and emits it for the ceremony; the agent writes the rollup section; mechanical validation confirms `## [X.Y.Z]` exists with items and the cycle's prerelease sections are gone. The collapse set is defined exactly: sections whose label is a recognized rung (alpha, beta, rc) and whose core equals the candidate — pre-model history survives untouched, so 2.0.0's promotion collapses `2.0.0-alpha.*` (and any beta/rc) while the legacy `2.0.0-dev.*` and `2.0.0-pre.*` sections stay. The exact emission surface (stdout block vs file under the change folder) is the implementing task's tactical choice. + +### Per-channel guardrails + +Post-merge guardrails 5 and 6 key on the prepared version's rung: alpha/beta sections must exist with items as today, now produced by the notes projection rather than a hand-curated buffer; rc requires the release commit to stay within the metadata allowlist minus CHANGELOG (version files + regenerated outputs, no changelog section), with GitHub Release notes falling back to an auto-generated commit list for the range; promotion validates the rollup and collapse per above. The rc-cut preflight warns when the range since the last rc/beta contains `feat:` commits. The write path itself is TASK-008's: `writeReleaseChangelog` and the dry-run preview become rung-aware, so an rc cut writes nothing for the guardrails to reject. + +### CI and tap + +`release.yml` derives the channel from the tag: prerelease tags pass `--prerelease` on the create-fallback and update `Formula/loaf-dev.rb`; stable tags update `Formula/loaf.rb`. `update-homebrew-formula.mjs` already takes a required `--formula` path; the remaining work is the hardcoded Ruby class name (`class Loaf < Formula`, `:27`) becoming parameterized so the dev formula emits `LoafDev`. The initial `loaf-dev.rb` is seeded manually in the tap repo (H-tier — the tap is a separate repository). + +### Skill + +The release skill rewrite brings the document to the shipped CLI: channel state in context detection, the ladder and its ceremonies, the `/promote` flow, channel suggestion via AskUserQuestion fed by mechanical signals (`loaf change list --target`, receipt states, time since last release), and the post-merge steps that are currently absent. `loaf-reference` and the CLI reference regenerate from the build. + +### Sequencing + +Rung model first (everything reads it), then the flag surface, then gate placement (TASK-003) and the notes projection (TASK-008) — promotion mechanics (TASK-004) follows the projection, because its seed emission calls TASK-008's collection helper (declared `blocked-by`) — then per-channel guardrails (TASK-005, verifying what the projection produces), distribution (needs only the rung predicate), skill last (documents what shipped). TASK-004 does not start until the receipt-tree-binding predecessor lands on main — the dependency is carried by prose and cohort order because cross-change task relations are deliberately forbidden in the work model. Gate-at-rc also pulls the sweep carrier's whole lift ahead of stabilization: `spec-conversion-and-guidance-sweep` is captured-only today, so under the new trigger set 2.0.0-rc.1 cannot cut until the carrier is shaped, converted, executed, and receipt-verified — earlier than the stable-only gate demanded. That is the discipline working as designed (an rc claims stable intent; alphas and betas flow regardless), named here so the rc block is never read as a regression. + +## Implementation Units + +- **TASK-001 — Channel rung model.** Label/suffix parsing, ladder ordering, cycle entry, advance, and promotion math in the version primitives, forward-only enforcement, unit tests. +- **TASK-002 — Promote and channel flags.** `--promote [rung|release]`, `--channel` assertion, `--bump release` retirement, flag matrix validation, snapshot fields, three-way help sync. +- **TASK-003 — Gate fires at rc.** Trigger-set extension (rc candidates map to core cohort), valve retention for alpha/beta, post-merge rung keying, fixtures for every rung. +- **TASK-004 — Designation-commit promotion.** Last-rc location, designation diff check, rollup seed emission, collapse + rollup validation, promotion preflight gate re-check. +- **TASK-005 — Per-channel changelog guardrails.** Guardrails 5/6 rung keying, rc notes auto-derivation, feat-during-rc warning, negative fixtures. +- **TASK-006 — Channel-routed distribution.** `release.yml` channel derivation, `--prerelease` fallback, two-formula routing, formula-script parameterization. +- **TASK-007 — Release skill rewrite.** Channel ceremony, `/promote` flow, agent-suggested human channel choice, release-notes authoring discipline, current CLI vocabulary, rebuilt outputs. +- **TASK-008 — Release notes and the changelog projection.** Per-change `release-notes.md` convention, range-plus-provenance note collection, the rung-aware CHANGELOG write path (`writeReleaseChangelog` + dry-run preview: rc writes nothing, alpha/beta aggregate notes, promotion consumes TASK-004's validation), `[Unreleased]` retirement, `workflow-pre-pr` re-point with the no-user-facing-impact escape. + +## Verification Contract + +- **V1.** Rung math is correct and forward-only: entry, iterate, advance, skip, promote, and every backward move refused. Command: `go test ./internal/cli -run 'TestReleaseRung' -count=1`. Expect: exit 0. +- **V2.** The promote surface behaves: bare walks one rung, named rung warns on skip, `release` requires an rc base, `--channel` refuses a mismatched outcome before mutation, `--bump release` errors with the pointer. Command: `go test ./internal/cli -run 'TestReleasePromote' -count=1`. Expect: exit 0. +- **V3.** The gate fires for rc candidates and at promotion, and alpha/beta keep the valve. Command: `go test ./internal/cli -run 'TestReleaseGateRC' -count=1`. Expect: exit 0. +- **V4.** Designation-commit promotion: diff restriction, rollup validation, prerelease-section collapse per the defined collapse set, CI-green-at-HEAD refusal. Command: `go test ./internal/cli -run 'TestReleaseDesignation' -count=1`. Expect: exit 0. +- **V5.** Per-channel guardrails and the rc-cut feat warning hold with negative fixtures per rung. Command: `go test ./internal/cli -run 'TestReleasePostMergeChannel' -count=1`. Expect: exit 0. +- **V6.** The notes projection behaves: rc cuts leave CHANGELOG untouched, alpha/beta sections aggregate exactly the ranged-and-landed notes and land above the previous top release with no `[Unreleased]` anchor present, note-less changes fall back with a warning, the pre-PR hook accepts note presence and the exact no-impact declaration. Command: `go test ./internal/cli -run 'TestReleaseNotesProjection' -count=1`. Expect: exit 0. +- **V7.** The full suite is green. Command: `go test ./...`. Expect: exit 0. + + + +- **H1.** A promotion dry-run transcript on a fixture repo reads as the intended ceremony: gate re-check, designation check, seed material, validation verdicts. +- **H2.** The rewritten release skill names only flags and commands the shipped CLI accepts. +- **H3.** Observed at first post-landing releases: a prerelease updates `loaf-dev.rb` and not `loaf.rb`; the 2.0.0 stable updates `loaf.rb`; the create-fallback marks prereleases. +- **H4.** The 2.0.0 rollup, seeded from its cohort, reads as one coherent story since the last stable — judged against the reaction artifact in `research/`. Acknowledged: this first rollup is largely fallback-sourced (the note convention is newer than most of its cohort), so H4 judges the fallback path exactly where it will dominate. + +## Definition of Done + +- All V-entries green at HEAD via `loaf change verify` with the receipt committed. +- `loaf change check` reports zero violations and the change derives executable. +- `npm run build` regenerates the skill and reference surfaces; rebuilt `plugins/` and `dist/` outputs committed with their source. +- The three release help surfaces agree, and no retired vocabulary (lineage freeze, `--bump release`) survives outside the deprecation error itself. + +## Durable Outputs + +- ADR: the release promotion model — designation-commit promotion, gate-at-rc, per-channel changelog policy, and the rejected alternatives (tag-only, artifact promotion, CI-authored commits). +- `docs/knowledge/work-model.md`: release section updated from valve-only semantics to the channel model. +- Release skill references: channel ceremony and promotion flow (lands with TASK-007). + +## Open Questions + + + +- [UK] What a good cohort-seeded stable rollup reads like → reaction artifact in `research/`: a mock 2.0.0 rollup written from the live cohort when TASK-004's seed format lands, judged before TASK-007 encodes the ceremony. +- [KU] Exact seed-material emission surface (stdout block vs file) → TASK-004's tactical choice; recorded in its Verification notes when made. diff --git a/docs/changes/20260728-release-promotion-model/tasks/TASK-001-channel-rung-model.md b/docs/changes/20260728-release-promotion-model/tasks/TASK-001-channel-rung-model.md new file mode 100644 index 000000000..be4ee82aa --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/tasks/TASK-001-channel-rung-model.md @@ -0,0 +1,46 @@ +--- +change: release-promotion-model +id: TASK-001 +title: Channel rung model in the version primitives +blocks: + - TASK-002 + - TASK-003 + - TASK-004 + - TASK-006 +--- + +# TASK-001 — Channel rung model in the version primitives + +## Objective + +The version primitives understand the ladder: a prerelease splits into label + numeric suffix, `alpha < beta < rc` orders rungs, and three new moves exist — cycle entry (core bump + `alpha.1`), rung advance (next or named rung, counter reset to `.1`), and promotion (rc → bare core). Forward-only is enforced at this layer: any computation whose result would sort at or below the current version's rung refuses with a named reason. + +## Scope boundaries + +**In:** `internal/cli/release_dry_run.go` version primitives (`releaseSemver`, `parseReleaseSemver`, `bumpReleaseVersion`, `releaseVersionIsPrerelease`) and new rung helpers beside them; their unit tests. + +**Out:** Flag parsing, snapshot fields, help text (TASK-002); gate logic (TASK-003); promotion ceremony (TASK-004); anything under `.github/` or `content/`. + +## Context pointers + +- Contract: `shape.md` — Planning Contract → Rung model; Decisions 4, 5, 6, 8. +- Current primitives: `release_dry_run.go:865-902` (parse), `:827-863` (bump), `:876-879` (prerelease predicate). + +## Acquisition + +```bash +loaf journal log "skill(implement): TASK-001 — channel rung model" +# Read internal/cli/release_dry_run.go:820-910 and internal/cli/release_test.go before editing. +``` + +## Steps + +- [ ] Add rung parsing: label + numeric suffix split at the last dot; recognized ladder alpha(1) < beta(2) < rc(3); unknown labels are non-rungs. +- [ ] Add the three moves: cycle entry, rung advance (next/named, `.1` reset), promotion (requires rc base, returns bare core); each refuses backward or invalid transitions with a named reason. +- [ ] Keep `--bump prerelease` iteration semantics byte-compatible for unknown labels (reset-to-`.1` behavior preserved); `releaseSemverLess` untouched. +- [ ] Unit tests under a `TestReleaseRung` prefix covering entry, iterate, advance, named skip, promote, refusal of every backward move, and unknown-label behavior. + +## Verification + +- `go test ./internal/cli -run 'TestReleaseRung' -count=1` green. +- `go test ./internal/cli -count=1` green (no regression in existing bump/parse tests). diff --git a/docs/changes/20260728-release-promotion-model/tasks/TASK-002-promote-and-channel-flags.md b/docs/changes/20260728-release-promotion-model/tasks/TASK-002-promote-and-channel-flags.md new file mode 100644 index 000000000..f9f80da24 --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/tasks/TASK-002-promote-and-channel-flags.md @@ -0,0 +1,50 @@ +--- +change: release-promotion-model +id: TASK-002 +title: Promote and channel flags on the release surface +blocked-by: + - TASK-001 +blocks: + - TASK-005 + - TASK-007 + - TASK-008 +--- + +# TASK-002 — Promote and channel flags on the release surface + +## Objective + +`loaf release` speaks the settled surface: `--promote [alpha|beta|rc|release]` (bare walks one rung; a named rung is a warned skip; `release` explicit and rc-only), `--channel ` as a post-condition assertion validated after candidate computation and before any mutation (required only at cycle entry), and `--bump release` retired with an error pointing at `--promote release`. All three help surfaces agree and the stale lineage-freeze text is gone. + +## Scope boundaries + +**In:** Flag parsing and validation in `internal/cli/release_dry_run.go:95-202`, snapshot fields (`releaseSnapshot` at `release_dry_run.go:40`; `resolveReleaseSnapshot` at `change_release_gate.go:199` — a file TASK-003 also claims: this task touches only that function's move wiring, TASK-003 owns the gate trigger set), help text in `internal/cli/release.go`, `internal/cli/agent_help.go`, `internal/cli/cli_reference.go`; tests. + +**Out:** Rung math itself (TASK-001); gate trigger changes (TASK-003); promotion preflight/ceremony beyond flag routing (TASK-004); guardrails (TASK-005); skill prose (TASK-007). + +## Context pointers + +- Contract: `shape.md` — Planning Contract → Surface and snapshot; Decisions 4, 5. +- Flag matrix today: `release_dry_run.go:95-202`; post-merge exclusivity `:163-200`. +- Help drift: `cli_reference.go:90` still says "during a lineage freeze"; `release.go:99` and `agent_help.go:253` differ. + +## Acquisition + +```bash +loaf journal log "skill(implement): TASK-002 — promote and channel flags" +# Read internal/cli/release_dry_run.go:40-210, release.go, agent_help.go:240-270, cli_reference.go:80-110. +``` + +## Steps + +- [ ] Parse `--promote` with optional rung/`release` value and `--channel `; extend `releaseSnapshot` with the move kind and asserted channel; `--promote` mutually exclusive with `--bump`, composes with `--dry-run`/`--yes`/`--base`/`--tag`/`--no-gh`; `--post-merge` exclusivity unchanged. +- [ ] Wire moves through `resolveReleaseSnapshot` using the TASK-001 primitives; `--channel` assertion checked against the computed candidate before any mutation; cycle entry requires a `--bump major|minor|patch --channel alpha` pair from a stable base. +- [ ] Add `--promote` and `--channel` to `--post-merge`'s incompatibility list (`release_dry_run.go:163-200`) alongside `--bump` — post-merge takes no version-shaping input; rejection test included. +- [ ] Retire `--bump release`: error with pointer to `--promote release`; remove it from the valid-bump set and every help surface. +- [ ] Rewrite the three help surfaces together — flags, moves, one shared vocabulary; delete the lineage-freeze sentence. +- [ ] Tests under a `TestReleasePromote` prefix: bare walk, named skip warning, `release` from non-rc refused, `--channel` mismatch refused pre-mutation, `--bump release` pointer error, flag-matrix rejections. + +## Verification + +- `go test ./internal/cli -run 'TestReleasePromote' -count=1` green. +- `grep -rn "lineage freeze" internal/cli/` returns nothing; `grep -n "bump release" internal/cli/release.go internal/cli/agent_help.go internal/cli/cli_reference.go` shows only the retirement error text. diff --git a/docs/changes/20260728-release-promotion-model/tasks/TASK-003-gate-fires-at-rc.md b/docs/changes/20260728-release-promotion-model/tasks/TASK-003-gate-fires-at-rc.md new file mode 100644 index 000000000..97707f843 --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/tasks/TASK-003-gate-fires-at-rc.md @@ -0,0 +1,42 @@ +--- +change: release-promotion-model +id: TASK-003 +title: Cohort gate fires at rc cut +blocked-by: + - TASK-001 +--- + +# TASK-003 — Cohort gate fires at rc cut + +## Objective + +The cohort gate's trigger set becomes: candidate is stable (byte-equal cohort match, unchanged) OR the candidate's rung is rc, in which case the cohort is every change whose `target_release` equals the candidate's core literal. Alpha and beta candidates keep the valve semantics with warnings. Post-merge keys the same logic on the prepared version's rung: rc and stable gate, alpha and beta flow. + +## Scope boundaries + +**In:** `internal/cli/change_release_gate.go` (trigger set, cohort selection), the post-merge gate keying in the snapshot path, fixtures in `change_release_gate_test.go`. + +**Out:** Rung math (TASK-001); promotion preflight and its re-check plumbing beyond reusing the gate function (TASK-004); guardrails 5/6 (TASK-005); receipt freshness semantics (predecessor Change — do not touch `change_verify.go` freshness logic). + +## Context pointers + +- Contract: `shape.md` — Planning Contract → Gate placement; Decisions 2, 5. +- Current bypass: `change_release_gate.go:43-48`; cohort selection `:50-57`; post-merge keying `:232-241`. + +## Acquisition + +```bash +loaf journal log "skill(implement): TASK-003 — gate fires at rc" +# Read internal/cli/change_release_gate.go and the gate sections of change_release_gate_test.go. +``` + +## Steps + +- [ ] Extend the trigger set: rc candidates gate the cohort of their core literal; alpha/beta return valve-with-warnings as today; stable path unchanged. +- [ ] Key post-merge gating on the prepared version's rung (rc and stable gate; alpha/beta flow) without altering post-merge's no-version-input contract. +- [ ] Fixtures for every rung: alpha flows, beta flows, rc blocks on an unverified cohort member and passes on a verified one, stable unchanged, direct stable bump still gates. + +## Verification + +- `go test ./internal/cli -run 'TestReleaseGateRC' -count=1` green. +- `go test ./internal/cli -run 'TestReleaseCohort|TestReleasePostMerge' -count=1` green (existing gate and post-merge suites unregressed). diff --git a/docs/changes/20260728-release-promotion-model/tasks/TASK-004-designation-commit-promotion.md b/docs/changes/20260728-release-promotion-model/tasks/TASK-004-designation-commit-promotion.md new file mode 100644 index 000000000..7aef3c7c0 --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/tasks/TASK-004-designation-commit-promotion.md @@ -0,0 +1,50 @@ +--- +change: release-promotion-model +id: TASK-004 +title: Designation-commit promotion mechanics +blocked-by: + - TASK-001 + - TASK-008 +blocks: + - TASK-005 + - TASK-007 +--- + +# TASK-004 — Designation-commit promotion mechanics + +## Objective + +`--promote release` runs the full promotion preflight and produces the designation commit path: locate the highest `vX.Y.Z-rc.N` tag for the candidate core (refuse when none exists — stable is a promoted rc), verify `git diff ..HEAD --name-only` stays within the release-metadata allowlist (version files, CHANGELOG.md, regenerated build outputs — see the Planning Contract), re-check the cohort gate, emit rollup seed material (the cycle's landed `release-notes.md` fragments per TASK-008's collection, `shape.md` Problem/Hypothesis lines as warned fallback, the cycle's prerelease sections), and validate the resulting CHANGELOG mechanically: `## [X.Y.Z]` present with items, the cycle's prerelease sections collapsed away. + +## Scope boundaries + +**In:** New promotion preflight beside `internal/cli/release_post_merge.go` / `release_dry_run.go`, seed-material emission, CHANGELOG validation helpers, tests with fixture repos. + +**Out:** The gate function internals (TASK-003 — call it, don't change it); guardrails 5/6 for non-promotion channels (TASK-005) — except the CI-green-at-HEAD preflight helper, which this task owns as one shared check covering BOTH rc cut and promotion (Decision 11; TASK-005 consumes, never reimplements); the notes projection and changelog write path (TASK-008); skill ceremony prose (TASK-007); receipt freshness (predecessor Change). + +## Context pointers + +- Contract: `shape.md` — Planning Contract → Promotion mechanics; Decisions 1, 2, 3, 9; Rabbit Holes (rollup is judged, never generated). +- Changelog plumbing today: `release_dry_run.go:904-970` (read), `:1127-1166` (write); guardrail 6 extraction `release_post_merge.go:242-300`. + +## Acquisition + +```bash +loaf journal log "skill(implement): TASK-004 — designation-commit promotion" +# Read internal/cli/release_post_merge.go, release_dry_run.go changelog sections, change_release_gate.go entry points. +``` + +## Steps + +- [ ] Last-rc location by tag for the candidate core; refusal with a named reason when no rc exists. +- [ ] Designation diff check: the release-metadata allowlist since the last rc tag — importing receipt-tree-binding's `releaseMetadataAllowlist` specifically (the allowlist alone, never the full digest-exclusion set, which also masks receipts/ and reports/), never redefined here — else block listing offending paths with the "cut another rc" remedy. +- [ ] CI-green-at-HEAD assertion for rc cut and promotion: refuse to tag without a successful CI run at the exact HEAD SHA (`gh run list --commit `), blocking with a named remedy (Decision 11). +- [ ] Promotion preflight composes: designation check + cohort gate re-check + rollup seed emission (decide stdout block vs file here and record the choice in this file's Verification notes). +- [ ] Mechanical rollup validation: stable section present with items, cycle prerelease sections absent; wire into the promotion apply path and post-merge guardrail path for stable-prepared versions. +- [ ] Write the reaction artifact: a mock 2.0.0 rollup seeded from the live cohort, committed under `research/` for H4 judgment. +- [ ] Tests under a `TestReleaseDesignation` prefix: diff restriction (positive + offending-path negative), no-rc refusal, rollup validation positives and negatives, collapse detection against the defined collapse set (rung-labeled sections of the candidate core; dev/pre history untouched), CI-green-at-HEAD refusal (no successful run at HEAD → block with the named remedy). + +## Verification + +- `go test ./internal/cli -run 'TestReleaseDesignation' -count=1` green. +- The seed-emission choice is recorded here once made, resolving the shape.md open question. diff --git a/docs/changes/20260728-release-promotion-model/tasks/TASK-005-per-channel-changelog-guardrails.md b/docs/changes/20260728-release-promotion-model/tasks/TASK-005-per-channel-changelog-guardrails.md new file mode 100644 index 000000000..9b702e08a --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/tasks/TASK-005-per-channel-changelog-guardrails.md @@ -0,0 +1,46 @@ +--- +change: release-promotion-model +id: TASK-005 +title: Per-channel changelog guardrails +blocked-by: + - TASK-002 + - TASK-004 + - TASK-008 +--- + +# TASK-005 — Per-channel changelog guardrails + +## Objective + +Post-merge guardrails 5 and 6 key on the prepared version's rung: alpha and beta unchanged (version files + CHANGELOG section required); rc requires the release commit to stay within the metadata allowlist minus CHANGELOG (version files + regenerated build outputs), skips the changelog-section requirement, and GitHub Release notes fall back to an auto-generated commit list for the range; promotion (stable prepared from a cycle) validates via TASK-004's rollup checks. The rc-cut preflight warns when the range since the last rc or beta contains `feat:` commits. + +## Scope boundaries + +**In:** `internal/cli/release_post_merge.go` guardrails 5/6 (`:209-300`), rc notes fallback in the gh-release action, the feat-during-rc warning in the rc-cut preflight, fixtures. + +**Out:** Rollup validation internals (TASK-004 — call them); CI workflow (TASK-006); the `workflow-pre-pr` and `validate-push` hooks unless a fixture proves they block a legal rc flow (if so, the minimal rung-aware carve-out only). + +## Context pointers + +- Contract: `shape.md` — Planning Contract → Per-channel guardrails; Decision 3. +- Guardrail 5: `release_post_merge.go:209-240`; guardrail 6: `:242-300`; gh notes: `:357-418`. +- The CI-green-at-HEAD preflight helper (Decision 11) is owned by TASK-004 as one shared check for rc cut and promotion — consume it, never reimplement it. The alpha/beta changelog sections this task validates are produced by TASK-008's notes projection. + +## Acquisition + +```bash +loaf journal log "skill(implement): TASK-005 — per-channel guardrails" +# Read internal/cli/release_post_merge.go and its test file end to end. +``` + +## Steps + +- [ ] Key guardrails 5/6 on the prepared rung; rc path: metadata-allowlist diff without CHANGELOG, no changelog-section requirement. +- [ ] rc GitHub Release notes: auto-generated commit list for the range since the previous tag. +- [ ] feat-during-rc preflight warning ("new development during rc; consider cutting a beta") on rc cuts whose range contains `feat:` commits. +- [ ] Negative fixtures per rung: rc commit touching CHANGELOG blocks; beta missing its section blocks; promotion missing collapse blocks (delegating to TASK-004 validators). + +## Verification + +- `go test ./internal/cli -run 'TestReleasePostMergeChannel' -count=1` green. +- Existing post-merge guardrail suite unregressed. diff --git a/docs/changes/20260728-release-promotion-model/tasks/TASK-006-channel-routed-distribution.md b/docs/changes/20260728-release-promotion-model/tasks/TASK-006-channel-routed-distribution.md new file mode 100644 index 000000000..95f42c7b2 --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/tasks/TASK-006-channel-routed-distribution.md @@ -0,0 +1,43 @@ +--- +change: release-promotion-model +id: TASK-006 +title: Channel-routed distribution +blocked-by: + - TASK-001 +--- + +# TASK-006 — Channel-routed distribution + +## Objective + +CI routes by the tag's channel: prerelease tags pass `--prerelease` on the `gh release create` fallback and update `Formula/loaf-dev.rb`; stable tags update `Formula/loaf.rb`. The formula-update script takes the target formula (and derives the Ruby class name) as a parameter instead of assuming one file. + +## Scope boundaries + +**In:** `.github/workflows/release.yml`, `cli/scripts/update-homebrew-formula.mjs`, any script-level tests that exist for the formula updater. + +**Out:** Go code entirely; the tap repository itself (seeding `Formula/loaf-dev.rb` there is a manual H-tier step recorded in shape.md H3, not a commit in this repo). + +## Context pointers + +- Contract: `shape.md` — Planning Contract → CI and tap; Decision 7. +- Workflow today: `.github/workflows/release.yml` (create-fallback `:88-98` has no `--prerelease`; tap update `:100-130` unconditional). + +## Acquisition + +```bash +loaf journal log "skill(implement): TASK-006 — channel-routed distribution" +# Read .github/workflows/release.yml and cli/scripts/update-homebrew-formula.mjs end to end. +``` + +## Steps + +- [ ] Derive the channel from the tag in the workflow (prerelease = tag contains `-`); pass `--prerelease` on the create-fallback for prerelease tags. +- [ ] Route the tap update: prerelease → `Formula/loaf-dev.rb`, stable → `Formula/loaf.rb`; commit message names the formula touched. +- [ ] Parameterize the Ruby class name in `update-homebrew-formula.mjs` — `--formula` already exists and is required (`:9`); the class is hardcoded as `class Loaf < Formula` (`:27`) and must emit `LoafDev` for the dev formula, since Homebrew derives the class from the file name. +- [ ] Guard the workflow against a missing `loaf-dev.rb` with a clear failure message naming the manual seeding step. + +## Verification + +- `node cli/scripts/update-homebrew-formula.mjs` invoked against a fixture formula file for both names produces correct version/sha/class updates (script-level check or documented manual run in the delivering commit). +- Workflow YAML parses (`gh workflow view` or actionlint if available); channel routing visible in the diff for review. diff --git a/docs/changes/20260728-release-promotion-model/tasks/TASK-007-release-skill-rewrite.md b/docs/changes/20260728-release-promotion-model/tasks/TASK-007-release-skill-rewrite.md new file mode 100644 index 000000000..7a6b40f5c --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/tasks/TASK-007-release-skill-rewrite.md @@ -0,0 +1,47 @@ +--- +change: release-promotion-model +id: TASK-007 +title: Release skill rewrite for the channel model +blocked-by: + - TASK-002 + - TASK-004 + - TASK-008 +--- + +# TASK-007 — Release skill rewrite for the channel model + +## Objective + +`content/skills/release/SKILL.md` documents the CLI that actually ships: channel state in context detection, the ladder and its ceremonies (`--bump` iterates, `--promote` advances, `--channel` asserts), the `/promote` flow with the designation commit and rollup judgment, channel suggestion via AskUserQuestion fed by mechanical signals (`loaf change list --target`, receipt states, time since last release), and the post-merge steps that today's skill omits entirely — including the cohort gate, `target_release`, and receipts, none of which the current skill mentions. + +## Scope boundaries + +**In:** `content/skills/release/SKILL.md` and its references/templates; `content/skills/loaf-reference/SKILL.md` release lines; rebuilt outputs under `plugins/` and `dist/` via `npm run build`. + +**Out:** Go code; CI; any other skill beyond the loaf-reference release lines. Prose must not reimplement CLI logic — describe judgment and ceremony, point at commands. + +## Context pointers + +- Contract: `shape.md` — Planning Contract → Skill; Decisions 3, 4, 9; Observable Workflow for the canonical command flows. +- Current skill: `content/skills/release/SKILL.md` (291 lines, predates the gate); reaction artifact from TASK-004 in `research/` for the rollup ceremony's shape. + +## Acquisition + +```bash +loaf journal log "skill(implement): TASK-007 — release skill rewrite" +# Read content/skills/release/SKILL.md and the shipped release help (loaf release -h) before writing. +``` + +## Steps + +- [ ] Rewrite the skill: context detection includes current channel and cohort state; steps cover cycle entry, iteration, advance, rc cut (gate fires), promotion ceremony (designation commit, rollup judgment against seed material, mechanical validation), post-merge finalize, and distribution expectations per channel. +- [ ] Channel suggestion pattern: mechanical signals gathered first, human choice via AskUserQuestion with the agent's recommendation — never an automatic channel decision. +- [ ] Release-notes authoring discipline: every change writes `release-notes.md` while context is hot; the skill teaches the fragment's register (user-facing, grouped bullets) and the exact no-impact form TASK-008 defines — a note whose entire content is `No user-facing impact.` — as the escape's one carrier (never a change.json field or PR-body text; the skill teaches, never redefines). +- [ ] Update `loaf-reference` release lines; no flag or command appears that the shipped CLI does not accept. +- [ ] `npm run build`; commit rebuilt `plugins/` and `dist/` outputs with the source. + +## Verification + +- Every command and flag named in the skill exists in `loaf release -h` output (H2 review). +- `loaf build` and `npm run build` succeed; rebuilt outputs committed. +- The journal self-logging instruction survives in the rewritten skill's Critical Rules. diff --git a/docs/changes/20260728-release-promotion-model/tasks/TASK-008-release-notes-and-changelog-projection.md b/docs/changes/20260728-release-promotion-model/tasks/TASK-008-release-notes-and-changelog-projection.md new file mode 100644 index 000000000..363ed2676 --- /dev/null +++ b/docs/changes/20260728-release-promotion-model/tasks/TASK-008-release-notes-and-changelog-projection.md @@ -0,0 +1,50 @@ +--- +change: release-promotion-model +id: TASK-008 +title: Release notes and the changelog projection +blocked-by: + - TASK-002 +blocks: + - TASK-004 + - TASK-005 + - TASK-007 +--- + +# TASK-008 — Release notes and the changelog projection + +## Objective + +CHANGELOG.md becomes a cut-time projection fed by per-change release notes, and the write path is rung-aware. Each change may carry `release-notes.md` in its own folder (user-facing fragment, never deleted, archived with the change); note collection derives from the release range plus execution provenance, so a note participates only when its change's delivering commits landed in the range. `writeReleaseChangelog` and the dry-run preview key on the rung: rc cuts write nothing, alpha/beta cuts splice a section aggregated from the ranged notes (note-less landed changes fall back to `shape.md` Problem/Hypothesis lines with a warning), promotion's rollup is validated by TASK-004. The `[Unreleased]` buffer retires: the heading and stub disappear from the projection, and `workflow-pre-pr` re-points from buffer emptiness to per-change note presence. The no-impact escape's carrier is the note file itself: a change with nothing user-facing writes `release-notes.md` containing the explicit no-user-facing-impact declaration — one surface, auditable in the folder, skipped by aggregation. Touched change folders are detected per-PR via `git diff ..HEAD --name-only` (the `isReleaseOnlyPR` pattern in `check.go`). + +## Scope boundaries + +**In:** `internal/cli/release_dry_run.go` changelog read/write plumbing (`releaseChangelogSection`, `extractReleaseUnreleasedBody`, `writeReleaseChangelog`, `insertReleaseChangelog`, `createReleaseChangelog`, the preview at `:288-294` and apply at `:481-484`), the `loaf init` CHANGELOG template (`internal/cli/init.go:274-283`), the live CHANGELOG.md buffer removal, the note-collection helper (range + provenance), `runNativeWorkflowPrePR` in `internal/cli/check.go:834-869`, tests. + +**Out:** Guardrail verification of the produced sections (TASK-005); the promotion rollup validation and seed emission surface (TASK-004 — this task provides the collection helper it calls); flag parsing (TASK-002); skill prose teaching the authoring discipline (TASK-007). + +## Context pointers + +- Contract: `shape.md` — Decision 3; Planning Contract → Per-channel guardrails (the write-path sentence); Observable Workflow. +- Provenance grade for "landed in the range": `change_provenance.go` — reuse the existing execution-provenance derivation, never a new heuristic. +- Current buffer machinery: `release_dry_run.go:904-970` (curated content wins verbatim today — that precedence dies with the buffer). + +## Acquisition + +```bash +loaf journal log "skill(implement): TASK-008 — release notes and changelog projection" +# Read internal/cli/release_dry_run.go:280-300,470-490,900-1170 and check.go:830-880 before editing. +``` + +## Steps + +- [ ] Note collection: changes with delivering commits in the release range contribute their `release-notes.md`; note-less landed changes contribute the `shape.md` fallback with a warning naming the folder. +- [ ] The no-impact declaration's recognized form is exact and defined HERE (TASK-007 teaches it, never redefines it): a note whose entire content is the single line `No user-facing impact.` — the collection helper recognizes it mechanically and aggregation skips it. +- [ ] Rung-aware write path: rc writes no CHANGELOG section (preview says so explicitly); alpha/beta splice the aggregated section; stable path delegates to the promotion rollup. +- [ ] Retire the `[Unreleased]` buffer physically in this task's own delivering commit: remove the heading, stub, and preamble sentence from the live CHANGELOG.md; rewrite `insertReleaseChangelog`'s anchor semantics (new sections land directly above the previous top release; a missing anchor is the new normal, never the current silent bottom-append fallback at `release_dry_run.go:1136-1138`); update both scaffolds that still emit the buffer — `createReleaseChangelog` (`release_dry_run.go:1167-1181`) and the `loaf init` CHANGELOG template (`internal/cli/init.go:274-283`). +- [ ] Re-point `workflow-pre-pr`: detect touched change folders via `git diff ..HEAD --name-only` (reuse the `isReleaseOnlyPR` pattern); block when a touched change folder lacks `release-notes.md`; the no-impact declaration inside the note satisfies the check and aggregation skips it; keep the existing release-flow escape untouched. +- [ ] Tests under `TestReleaseNotesProjection`: range-and-provenance collection (landed vs merely-authored), rc writes nothing, alpha/beta aggregation, fallback warning, hook accept/block/escape cases. + +## Verification + +- `go test ./internal/cli -run 'TestReleaseNotesProjection' -count=1` green, including the placement assertions (sections land above the previous top release; no `[Unreleased]` anchor anywhere in the output). +- TASK-005's later `TestReleasePostMergeChannel` suite builds its fixtures on the sections this task produces — congruence is proven there, after both land (the suite does not exist yet at this task's completion).