Development record
A managed repository that is not a plugin gets the same release gate
Press Release
A repository abcd manages declares what it ships, and the release flow follows the declaration. A managed Go application, a macOS app with its own tag-driven workflow, or any repository that is not a plugin can today only say what it is not by failing, and its operator cuts every release by hand with the release gate never running. With this change the repository declares its artefact kind once.
launch scaffoldlays what that kind needs and refuses to guess the rest.launch --dry-runandlaunch shiprun against the declared kind, so the changelog-driven gate, the deferral read and the derived version reach every managed repository, not only the one that ships a plugin. A repository with its own release workflow keeps it.
Why This Matters
The launch verbs assume a plugin at three places: The payload include config, the lockstep table of plugin manifests, and the source-bundle scan. Two managed repositories hit that wall at v0.9.0, and one of them cut three releases in a single day entirely by hand: A dated changelog heading on a release branch, deferral keys written into open major records by script, a pull request, a tag from the repository's own workflow. The release-cut gate that refuses on an open major captured since the anchor tag never ran there; the session enforced it by reading the ledger. The cost is measured, and the remedy is the capability, not a patch. Graduated from iss-2609061432214212.
Mechanism
We expect the gate to reach every artefact kind once the kind is declared, because the gate's inputs (the ledger, the anchor tag, the version location) are already kind-independent and only the manifest reads bind it to a plugin; it is shown wrong if some kind needs a gate input that no declaration can supply.
Scope Conditions
- A repository abcd manages (a marker block fired), on a forge with a merge queue or branch protection, whose releases are tagged by a workflow abcd can scaffold or call.
- A single artefact per repository; a repository that ships several artefacts of different kinds is outside this claim.
What's In Scope
- Artefact kinds in the first cut: A plugin (the shipped shape), a Go binary, and an application with its own build and publish steps. Any other kind is refused by name.
- The declaration, the kind-shaped scaffold, and the launch verbs reading the declaration.
What's Out of Scope
- The write path for
deferred_afteranddeferral_reason, which staysiss-2609181223260994; this intent delivers the read half of that seam. - A hand-written changelog or version for any kind: The changelog stays derived (adr-37) and the version derived (adr-31).
- What a hand-merged workflow does with the gate it calls: The binary proves the gate, the derivation and the refusals; the runbook asks a human to hold the rest.
Acceptance Criteria
- Given a managed repository with no
.abcd/config/artefact.json, whenlaunch --dry-runruns, then it refuses naming that file as the declaration's home and listing the kinds it accepts, never with a missing-file error; andahoyreports the absence as a gapahoy installcan close. - Given an artefact file declaring a non-plugin kind and a lockstep list, when
launch --dry-runruns, then the lockstep check reads the primary fromversion-location.json(adr-19) and every listed file, refuses a listed path it cannot read, reads no plugin manifest, and requires no payload include config. - Given a declared non-plugin kind and no
CHANGELOG.md, whenlaunch scaffoldruns, then afterwards a changelog holding only the empty[Unreleased]anchor exists, the gate workflow exists as a file of its own with a named empty build job, and the report names every file written. - Given a repository with its own release workflow, when
launch scaffoldruns, then that workflow is byte-for-byte what it was, the gate workflow is written beside it, and the report names the existing file as left alone and states what to add to it to call the gate. - Given a scaffolded gate workflow later edited by hand, when
launch scaffoldruns again, then it refuses naming the file until--confirm, as it does for abcd's own scaffold today. - Given a declared kind whose release cut carries an open major captured since the anchor tag with no deferral, when
launch shipruns, then it refuses naming the record, exactly as it does for a plugin. - Given a declared kind whose only post-anchor major carries
deferred_afternaming the anchor tag and adeferral_reason, whenlaunch shipruns, then the guard passes and the receipt names the deferred record. - Given a declared non-plugin kind, when
launch --dry-runruns, then the identity scan runs over the tree the tag would archive minus the record namespace, and the report says which tree was scanned. - Given a kind the binary does not know, when any launch verb runs, then it refuses naming the kind and the accepted set, and writes nothing.
Decisions
Settled in the planning interview with the product thinker on 2026-09-20; each replaces the open question it answers.
- Declaration home: A new
.abcd/config/artefact.jsonholds the artefact kind and, with it, the lockstep list and later the kind's opt-ins. Not the repo config, not the version-location file. - Lockstep secondaries: The artefact file names the files held in lockstep with the primary; the check reads what was declared and refuses a declared path it cannot read.
- Adoption home: An
ahoy installgap. A managed repository with no declaration is a gap ahoy detects, prompts for and writes;launch scaffoldthen lays the kind's files. - Seeded file class: Drift-checked, as the scaffolded workflows are today; a later hand edit refuses the next scaffold run until
--confirm. - An existing release workflow: Left byte-for-byte; abcd's gate is scaffolded as a separate workflow the repository's own calls before its build step, and the report says what to add.
- Build and asset shape: Gate plumbing only. The scaffold lays the verify, derive, changelog, receipt and tag steps and a named empty build job the repository fills or calls;
iss-2608270559310755stays its own record and is not absorbed. - Changelog seeding with tags and no changelog: An empty
[Unreleased]anchor only; history before adoption is not represented. - Payload for a built artefact: The preview scans the source tree the tag would archive, minus the record namespace, exactly as a plugin payload is scanned; an empty include set is not a refusal for a non-plugin kind.
- The release-rendered site (
itd-2609061543533170): A separate intent built on this one; the artefact file reserves a site opt-in this intent does not act on.
Typed Links
- refines
itd-93(a scaffolded release gate that works on the first try): Widens it from the one artefact kind it assumes to a declared one. - refines
adr-37,adr-31(changelog-driven releases, derived versioning): Every kind keeps both; nothing here adds a hand-written path. - built on by
itd-2609061543533170(the release-rendered site): Reads the artefact file this intent introduces.
Audit Notes
Fidelity review — receipt rcp-97519c4308ad (verifier intent-auditor claude-fable-5-1).
Provenance: intent-auditor@claude-fable-5-1 · rubric_hash sha256:effa65b3e9e88ff29433b443ec2be159522a8b0b71cf1434526514aa61edb13e · prompt_hash sha256:0ba7df9fbbcf7cab4dc9553e264ddd0f97116f904f3f870ce848e360105749ed Input attestations: commit:b52746fb3 (spc-2609202019026366 close, itd-2609150819432059 ships); tree audited at 52c2236a55830421c2af5fa58f5a196c4eb7fdd5@-;
Acceptance rollup: MET 9 · MET_WITH_CONCERNS 0 · NOT_MET 0 · INCONCLUSIVE 0
Per-criterion verdicts:
- ac-1 — MET: LoadArtefact turns an absent file into a PreflightError wrapping ErrNoArtefact whose message names .abcd/config/artefact.json and the three kinds; DryRun surfaces it (TestDryRunWithNoArtefactDeclarationRefusesNamingItsHome); ahoy raises artefact.missing as Required and Resolvable inside the managed-repo branch, and install writes it (TestDetectRaisesArtefactMissingUntilTheKindIsDeclared, TestInstallPromptsForTheKindAndWritesIt); all pass at BASE evidence: internal/core/launch/artefact.go:117 — "return Artefact{}, &PreflightError{msg: noArtefactMessage(), err: ErrNoArtefact}" evidence: internal/core/launch/artefact.go:97 — "this repository declares no artefact kind: " + ArtefactRelPath + " is where it is declared" evidence: internal/core/launch/dryrun_kind_test.go:15 — "func TestDryRunWithNoArtefactDeclarationRefusesNamingItsHome" evidence: internal/core/ahoy/artefact.go:58 — "Required: true, Resolvable: true," evidence: internal/core/ahoy/detect.go:124 — "gaps = append(gaps, detectArtefact(abs)...)" evidence: internal/core/ahoy/artefact_test.go:46 — "func TestDetectRaisesArtefactMissingUntilTheKindIsDeclared"
- ac-2 — MET: CheckDeclaredLockstep reads the primary through version-location.json and every declared file, refuses an unreadable declared path by name, and reads no plugin manifest; the dry-run test for a binary previews with no payload include config and no plugin-only refusal evidence: internal/core/launch/lockstep.go:317 — "No plugin manifest is read." evidence: internal/core/launch/lockstep.go:358 — "declared lockstep file "+f.Path+" not readable" evidence: internal/core/launch/lockstep_declared_test.go:23 — "func TestDeclaredLockstepDevPassesWithEveryKeyAbsentAndReadsNoPluginManifest" evidence: internal/core/launch/lockstep_declared_test.go:47 — "func TestDeclaredLockstepRefusesADeclaredPathItCannotRead" evidence: internal/core/launch/dryrun_kind_test.go:57 — "func TestDryRunForABinaryScansTheArchivedTreeAndChecksTheDeclaredLockstep"
- ac-3 — MET: For a non-plugin kind the scaffold writes CHANGELOG.md as the empty [Unreleased] anchor only, abcd-release-gate.yml as its own file with a build job that is
mkdir -p distand no guessed build, and the report names every written file; TestScaffoldForABinaryLaysTheChangelogAndTheGateWorkflow passes evidence: internal/core/launch/scaffold/kind.go:28 — "ChangelogAnchor = "# Changelog\n\n## [Unreleased]\n"" evidence: internal/core/launch/scaffold/kind.go:23 — "GateWorkflowPath = ".github/workflows/" + GateWorkflowName" evidence: internal/core/launch/scaffold/kind_test.go:105 — "func TestScaffoldForABinaryLaysTheChangelogAndTheGateWorkflow" evidence: internal/core/launch/scaffold/kind_test.go:118 — "run: mkdir -p dist" - ac-4 — MET: An existing release.yml/release.yaml is found by Lstat and never opened, the gate is written beside it, the report marks it kept with 'left alone' and carries the workflow_call stanza; proven at the core (TestScaffoldLeavesAnExistingReleaseWorkflowByteForByte) and at the front door (TestLaunchScaffoldForABinaryWithItsOwnReleaseWorkflowPrintsTheStanza) evidence: internal/core/launch/scaffold/kind.go:34 — "var ownReleaseWorkflows = []string{".github/workflows/release.yml", ".github/workflows/release.yaml"}" evidence: internal/core/launch/scaffold/kind.go:40 — "const gateCallStanza = `jobs: abcd-release-gate:" evidence: internal/core/launch/scaffold/kind_test.go:169 — "func TestScaffoldLeavesAnExistingReleaseWorkflowByteForByte" evidence: internal/surface/cli/launch_kind_test.go:137 — ""[kept] .github/workflows/release.yml", "[written] .github/workflows/abcd-release-gate.yml""
- ac-5 — MET: A hand-edited gate workflow makes the next scaffold return ErrScaffoldBlocked with the file marked refused and the edit kept; Confirm restores the machinery — the same drift class as the plugin scaffold's evidence: internal/core/launch/scaffold/kind_test.go:205 — "func TestScaffoldRefusesAHandEditedGateWorkflowUntilConfirm" evidence: internal/core/launch/scaffold/kind_test.go:214 — "if !errors.Is(err, ErrScaffoldBlocked) {" evidence: internal/core/launch/scaffold/writefiles_test.go:14 — "func TestWriteFilesKeepsASeedAndRefusesDriftedMachinery"
- ac-6 — MET: A declared binary's
launch shipwith an open major captured since the anchor tag and no deferral exits 1 naming the record (TestLaunchShipForADeclaredBinaryRefusesAnOpenMajor); the GuardFindings path is unchanged for every kind evidence: internal/surface/cli/launch_kind_test.go:76 — "func TestLaunchShipForADeclaredBinaryRefusesAnOpenMajor" evidence: internal/surface/cli/launch_kind_test.go:87 — "if !strings.Contains(string(out), "iss-90") {" - ac-7 — MET: The same cut with deferred_after naming the anchor tag and a deferral_reason exits 0 and the report names 'deferred: iss-90' with its reason (TestLaunchShipForADeclaredBinaryPassesAndNamesADeferral) evidence: internal/surface/cli/launch_kind_test.go:94 — "func TestLaunchShipForADeclaredBinaryPassesAndNamesADeferral" evidence: internal/surface/cli/launch_kind_test.go:106 — ""deferred: iss-90""
- ac-8 — MET: For a non-plugin kind the bundle is git archive's view of HEAD minus the record namespace, the dry-run report's ScannedTree carries ArchiveTreeDescription and the CLI prints 'scanned tree: the tree the release tag would archive'; a secret under .abcd/ is not scanned as shipping evidence: internal/core/launch/bundle.go:1053 — "const ArchiveTreeDescription = "the tree the release tag would archive (git archive's view of HEAD, export-ignore honoured), minus the record namespace"" evidence: internal/core/launch/dryrun.go:112 — "report.Bundle, report.ScannedTree = bundle, tree" evidence: internal/core/launch/dryrun_kind_test.go:63 — "report.ScannedTree != ArchiveTreeDescription" evidence: internal/surface/cli/launch_nopayload_test.go:57 — "func TestLaunchDryRunForADeclaredBinarySaysWhichTreeItScanned"
- ac-9 — MET: ParseArtefact refuses an unknown kind naming it and the accepted set, and TestEveryLaunchVerbRefusesAnUnknownKind drives dry-run, ship, scaffold, receipts and archive, asserting the refusal text and that neither the repository nor --out gained a file evidence: internal/core/launch/artefact.go:152 — "names the kind %q, which abcd does not know: the accepted kinds are %s" evidence: internal/surface/cli/launch_kind_test.go:36 — "func TestEveryLaunchVerbRefusesAnUnknownKind" evidence: internal/surface/cli/launch_kind_test.go:64 — "if status := r.Git("status", "--porcelain", "--ignored"); status != "" {"
Gap audit:
- honoured:
- one reader of the declaration shared by every launch verb and ahoy, and ahoy proves what it writes through it before writing evidence: internal/core/launch/artefact.go:9 — "Every launch verb and ahoy go through it, so a kind one of them accepts is a kind all of them accept." evidence: internal/core/ahoy/artefact.go:79 — "if _, err := launch.ParseArtefact(data); err != nil {"
- the shipped plugin shape adopts silently: a plugin manifest declares kind plugin without a prompt evidence: internal/core/ahoy/artefact.go:51 — "it carries a plugin manifest, so install declares kind plugin without asking" evidence: internal/core/ahoy/artefact_test.go:87 — "func TestInstallAdoptsKindPluginSilentlyForAPluginRepository"
- the changelog and the version stay derived for every kind; the site opt-in is read and validated but not acted on evidence: internal/core/launch/artefact.go:81 — "Site is the release-rendered site opt-in (decision 9). It is read and validated here and acted on by itd-2609061543533170, not by this intent."
- a plugin declaring a lockstep list is refused: the plugin keeps the pinned manifest table evidence: internal/core/launch/artefact.go:169 — "declares a lockstep list for kind plugin"
- a repository whose own workflow releases receives no second release chain evidence: internal/core/launch/scaffold/kind_test.go:191 — "a repository whose own workflow releases must not receive a second release chain"
- diverged: (none)
- missing:
- the acceptance rehearsal the spec names — a managed repository's next cut run through launch --dry-run and launch ship with the gate enforced by the binary — is not evidenced in this tree; the Grounds' falsifier (the two managed repositories still cutting by hand) remains unobserved either way evidence: .abcd/development/specs/closed/spc-2609202019026366-abcd-launch-cannot-set-up-the-release.md:63 — "The Gropius repository's own release is the acceptance rehearsal" evidence: .abcd/development/intents/shipped/itd-2609150819432059-abcd-launch-cannot-set-up-the-release.md:85 — "shown wrong if the two managed repositories still cut by hand after this ships"
Scope-condition dispositions:
- cond-2609202019020629 — survived: the artefact gap is raised only inside ahoy's managed-repository branch (marker block fired), and the delivery supplies both halves of the tagging assumption — a gate workflow abcd scaffolds and a call stanza for a workflow the repository already has; nothing delivered contradicts the forge assumption evidence: internal/core/ahoy/detect.go:124 — "gaps = append(gaps, detectArtefact(abs)...)" evidence: internal/core/launch/scaffold/kind.go:40 — "const gateCallStanza = `jobs: abcd-release-gate:"
- cond-2609202019027680 — survived: the declaration admits exactly one kind as a single JSON string and refuses anything else, so a repository can only ever declare one artefact evidence: internal/core/launch/artefact.go:147 — "if err := json.Unmarshal(raw["kind"], &kind); err != nil || kind == "" {" evidence: internal/core/launch/artefact_test.go:112 — "func TestParseArtefactRefusesRepeatedAndCaseFoldedKeys"
Grounds
- pursued: the release gate is abcd's central promise and it runs only in abcd's own repository today; we expect a declared artefact kind to carry the gate into every managed repository because the gate's inputs are already kind-independent; shown wrong if the two managed repositories still cut by hand after this ships, or if a kind needs a gate input no declaration can supply
Frontmatter
| id | itd-2609150819432059 |
|---|---|
| type | intent |
| lifecycle | shipped |
| kind | standalone |
| severity | major |
.abcd/development/intents/shipped/itd-2609150819432059-abcd-launch-cannot-set-up-the-release.md
open on GitHub ↗ · commit history ↗
Referenced by3
- iss-2609061432214212abcd launch cannot set up the release flow for a managed repo that is not a plugin. Observed adopting abcd in a managed Go macOS app (own tag-driven release workflow building on a macOS runner with minisign, no CHANGELOG.md, no .claude-plugin/plugin.json): 'abcd launch --dry-run' fails with 'include config not found: .abcd/config/launch-payload.json'; 'abcd changelog' and 'abcd launch ship' fail reading .claude-plugin/plugin.json; 'abcd launch scaffold' writes the generic ubuntu Go template (verify/build/publish) that replaces the repo's own release workflow, and nothing creates the pieces the flow presupposes: CHANGELOG.md with its empty [Unreleased] anchor, the launch payload include config, a version location for a non-plugin artefact, and (where detectors are configured) the release-gate manifest and receipts directory. Needed: an adoption step (ahoy install or launch scaffold --init) that lays these down for a managed repo, a way to declare a non-plugin version location and payload, and a template extension point for platform-specific build/publish steps so scaffold parity does not fight a macOS build. Until then a managed repo has to hand-port the template.related
- itd-2609221842494980A dependency bump lands without a person re-authoring it, and no machine enters the contributor graphrelated
- spc-2609202019026366abcd-launch-cannot-set-up-the-release-flow-for-a-managed-repimplemented by
mentions12
- adr-19The plugin version lives ONLY in the released artifact; the working tree stays unversioned, and the version location is chosen by a schema-validated decision, not hard-coded
- adr-31The release version is derived from the intents in it, never authored
- adr-37Releases are changelog-driven — rolling `[Unreleased]` is the release decision, and automation tags exactly that commit
- adr-2609151138420062The decisions log is a folder of minted records, not one appended file
- dcn-26091500000000042026-09-15 — The decisions log becomes a folder of individually minted records with a derived index, and `DECISIONS.md` a symlink to that index for now (product thinker, reframing iss-2609100507439414 from "how does adoption propagate abcd's merge-attribute workaround into a managed repository" to "why do the two single append-to-the-bottom files conflict when the five one-file-per-record families did not once across 27 merges", the measurement in iss-2609100508570803). The shape applies to abcd and to every managed repository; the decisions-append gate (DA001–DA004) is retired by the shape, in the same change the shape lands. The rule is adr-2609151138420062, the capability itd-2609151138388536 in `drafts/`; the changelog's conflict class is itd-2609150819432059's and closes separately. The adoption question the product thinker was first asked — point at the conflict, fix it silently, or fix it and take over the changelog — is dissolved by the shape rather than answered. Rejected: keep the file and propagate the union attribute; the folder for abcd only; keep the file and accept the conflicts.
- iss-90abcd docs lint is unusable inside a target repo without a plugin root: with ABCD_PLUGIN_ROOT/CLAUDE_PLUGIN_ROOT unset it reports plugin_root_status missing and cannot run, so the read-only doc-currency check the prepare-this-repo skill implies is unreachable when onboarding an external repo. Detector: docs lint runs against any repo docs/ given only a cwd (no plugin-root prerequisite), or fails loudly with a one-line remedy per loud-staging. Acceptance: abcd docs lint in a repo with docs/ but no plugin root.
- iss-2608270559310755launch scaffold's release.yml.tmpl is hardcoded to abcd-cli's OWN artifact shape, so it calls itself a 'generic bare-repo' workflow but is not generic: it runs 'make build' to cross-compile 'the four binaries', 'sha256sum abcd-* > checksums.txt', and 'gh release create <tag> bin/abcd-* bin/checksums.txt' as literals (only the verify/Run step is a <% %> substitution). A scaffolded repo that is not a Go CLI producing four abcd-* binaries via make build therefore gets a release workflow that builds/publishes the wrong (or no matching) assets, and its install path expects artifacts the release never produces. This is the confirmed 'asset gap' seen in a scaffolded non-abcd-cli repo (its install expected four tarballs + SHA256SUMS the workflow does not publish). Fix requires a product decision on how a non-abcd-cli repo declares/derives its build+artifact shape (parameterise the build command / artifact glob / checksum name via render.go substitutions with a repo-declared shape; or detect Go-CLI vs other; or scaffold only the generic release plumbing by default and make binary build/publish opt-in). Neighbour iss-2608261041218890 (release.yml tag not shape-checked before make build) is a different defect in the same template. Do NOT touch abcd-cli's own .github/workflows/release.yml.
- iss-2609100507439414A managed repository's shared append-only files conflict on nearly every merge, and abcd propagates neither of the two remedies it has already adopted for itself.
- iss-2609181223260994No verb writes the deferral keys a release cut reads. The principle pre-existing-is-not-a-defence names deferred_after and deferral_reason as the one sanctioned way past the release-cut guard for an open major or critical record, and changelog.GuardFindings reads them, but no capture sub-verb writes them: the ledger verbs are disposition, list, mentions, promote, resolve and wontfix, so a deferral is a hand edit of frontmatter that bypasses the validators every other transition carries (the anchor tag's shape, the reason's presence, the record being open and major). Relayed from the Gropius managed-repo session gropiusllm-56 on 2026-09-18 at v0.9.0, which wrote the two keys into six records by script in one day, across three release cuts. Wanted: abcd capture defer <iss-N> --after vX.Y.Z --reason "…", refusing a record that is not open, a tag that is not the current anchor, and an empty reason, and reporting the deferral in the same envelope shape as resolve. Note the gate that reads the keys lives in the launch verbs, which that repository cannot run (iss-2609061432214212), so the write path and the read path are missing together there; this record is the write half.
- itd-93abcd Scaffolds a Release Gate That Works on the First Try
- itd-2609061543533170One verb takes a managed repository's site from the checkout to a live address
- itd-2609151138388536Every Decision Is Its Own Record, and the Log Stops Conflicting