feat: cut a release with abcd launch ship and a derived changelog#129
Open
REPPL wants to merge 2 commits into
Open
feat: cut a release with abcd launch ship and a derived changelog#129REPPL wants to merge 2 commits into
REPPL wants to merge 2 commits into
Conversation
Phase 3 of the derived-versioning programme (itd-67 / spc-11). The write-verb the whole programme hangs on did not exist: `launch` was dry-run-only and Ship() stopped at WouldPublish. This builds it, plus the generation flow and the preview. TWO ENTRY POINTS, MIRRORING DISEMBARK `abcd launch ship` with no flag is the deterministic EMIT step: the record set, the derived version, the guardrail result, and any refusals. With --changelog-json <file|-> it INGESTS the host-composed payload, runs the completeness bijection, and writes the dated heading. The host runs the composer agent between the two, exactly as it does between `disembark`'s emit and ingest. THE COMPLETENESS BIJECTION The cited record ids must EQUAL the cut's records minus the `internal` ones — no omission, no invention. This is deliberately NOT the cite-or-be-dropped semantic of SynthesizePrinciples: an omitted record is a lie by omission in a durable release record, so a mismatch refuses the WHOLE payload and writes nothing, the way ComposePressRelease refuses an uncited document. The payload guards do come from the principles path (size cap, unknown-field rejection, sanitisation). The agent controls wording and the Keep-a-Changelog section only. The version and the inclusion set are deterministic — a four-value impact cannot express Security/Deprecated granularity, so the section is the agent's judgement while the bijection guards id-completeness alone. THE REFUSALS, ALL FAIL-CLOSED Six kinds, typed rather than prose-matched: no-release-tag, release-in-flight, unlabelled-record, stale-intent, surface-guard, empty-cut. A refused cut carries NO derived version — refusing and still emitting a number would invite someone to use it. The stale-intent refuse is the itd-94 case: an intent still in planned/ whose spec is closed is invisible to the tree diff, so the cut would silently under-bump; it now refuses and names the record. THE COMPOSER A new host-delegated agent, release-changelog-composer — named distinctly because the CHANGELOG and README agent slots are occupied by two documents the loader mis-registers as agents (iss-110). It carries prompt_version 0.1.0, an injection-canary fixture, and the untrusted-data contract: everything it reads is data, never instruction. `abcd changelog` is the deterministic-only preview: next version, deciding impact, record list, guardrail status. No agent, no prose, and a full-tree digest test proves it writes nothing. Reuse over reinvention: the in-flight detection is Derive's, now classified by a typed RefusalKind rather than reimplemented; the stale-intent check consumes a ScanSpecLinks index extracted from the lint's existing spec-lifecycle traversal rather than adding a third walk of the same trees. Assisted-by: Claude:claude-opus-4-8
…aunch-ship-changelog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 3 of the derived-versioning + auto-changelog + plugin-distribution programme
(plan:
.abcd/development/plans/2026-07-21-derived-version-and-changelog.md; spec: spc-11).Fourth of five stacked PRs. Based on
feat/itd-73-p2-surface-guardrail(#127).The write-verb the whole programme hangs on did not exist:
launchwas dry-run-only andShip()stopped atWouldPublish. Verified before building:abcd launch ship→unknown command "ship",abcd changelog→unknown command.It runs, on this repo, today
changelogexits 0 (a status render).launch shipexits 1 (the cut refuses).CHANGELOG.mdis untouched by both. The refusal is the phase-2 fail-closed baseline rulefiring correctly — it clears when a release is cut that contains the baseline.
Two entry points, mirroring
disembarkabcd launch shipwith no flag is the deterministic emit step. With--changelog-json <file|->it ingests the host-composed payload, runs the bijection andwrites. The host runs the composer between them — the same shape as
disembark … --principles-json.The completeness bijection
Cited record ids must equal the cut's records minus the
internalones. No omission, noinvention. This is deliberately not
SynthesizePrinciples' cite-or-be-dropped semantic:an omitted record is a lie by omission in a durable release record, so a mismatch refuses the
whole payload and writes nothing, the way
ComposePressReleaserefuses an unciteddocument. The payload guards do come from the principles path (size cap, unknown-field
rejection, sanitisation).
The agent controls wording and the Keep-a-Changelog section only. Version and inclusion
are deterministic.
Refusals — six kinds, all fail-closed
no-release-tag,release-in-flight,unlabelled-record,stale-intent,surface-guard,empty-cut. Typed rather than prose-matched, so a caller acts on the kind instead ofpattern-matching English. A refused cut carries no derived version — refusing while still
emitting a number invites someone to use it.
stale-intentis the itd-94 case: an intent still inplanned/whose spec is closed isinvisible to the tree diff, so the cut would silently under-bump. It now refuses and names it.
What the two independent reviews caught — both
FIX_FIRST, both fixedinsertSectionasserted the new heading's shape but never its position. In aCHANGELOG.mdwhose## [Unreleased]anchor sits below an older dated section, the newheading lands below an already-tagged one — and
auto-release.yml'sgrep -m1matches theolder heading. The cut reports success and the release is silently never tagged.
Now the assembled bytes are scanned and the write refuses unless the new heading is the
first dated one, naming the heading the workflow would otherwise tag.
removed side whose blob at the base tag carries no valid
impactwas dropped from therequired set — so a supersession would never reach the release record. The security
reviewer added the sharper half: an honest composer that cited it would then be refused
for "inventing" it. Fixed at the canonical home (
Record.InChangelog()), so the CLIrender, the composer prompt's stated rule, and the bijection all read one predicate.
Plus two hardening nits: trailing data after the JSON document is now refused, and an
over-long cited id is described by length rather than echoed.
The composer agent
release-changelog-composer— named distinctly because theCHANGELOGandREADMEagentslots are occupied by two documents the loader mis-registers as agents (iss-110).
Carries
prompt_version: 0.1.0,reads_untrusted_input: true, an injection-canary fixture,and the untrusted-data contract. Logged in
agents/CHANGELOG.md(the agent prompt log — notthe repo changelog).
Reuse over reinvention
The in-flight detection is
Derive's, now classified by a typedRefusalKindrather thanreimplemented. The stale-intent check consumes a
ScanSpecLinksindex extracted from thelint's existing spec-lifecycle traversal, which was rewritten to consume it — no third walk
of the same trees.
Verification
gofmt -l .silent ·go vet ./...·go build ./...·go test ./...·go run ./cmd/record-lintexit 0 · both new verbs exercised on the real repo.Tested under CI's actual condition before pushing: the
checkjob checks out with notags, which is what turned #125 and #127 red. The full suite passes in a clone with its tags
deleted. That fix is merged forward into this branch.
CHANGELOG.md,go.mod,go.sumand both.github/workflows/*.ymlare untouched —ADR-37's release contract is preserved, which is the entire point of writing the heading the
existing grep already matches.