Development record
One repository, a curated release artifact — no dev→public mirror
Context
abcd shipped through a two-repo mirror: active work in a development repo, a
curated snapshot published to a separate public repo via the launch flow. That
split forced a payload-exclusion policy at the mirror boundary — the launch
payload dropped .abcd/memory/** and scoped a restrictive-licence gate to the
lifeboat (ADR-18)
— and it created two trees to keep version-consistent, which
ADR-19 and
ADR-20 exist to police (dev stays
unversioned; the published snapshot carries the version). A dev→public mirror is
a maintenance liability and a drift source: two histories, two identities, a
copy step that can leak or lag.
Decision
abcd lives in one repository and ships a curated release artifact cut from it — there is no dev→public mirror.
.abcd/**stays in-tree — the design record travels with the code — but is excluded from the release artifact by packaging. Exclusion is a build-time filter over one tree, not a copy between two repos.launchcuts a curated GitHub Release from the single repo. The release artifact is the published surface; the repo is the marketplace.
This retires the mirror, so this ADR supersedes
ADR-18: the
payload-exclusion policy is no longer a mirror-boundary concern but a packaging
filter — .abcd/** (memory included) is excluded from the release artifact by
the same mechanism, on one tree.
It amends ADR-19 and ADR-20 rather than superseding them: their dev-unversioned / release-versioned polarity survives, now realised on a single tree — the working tree stays unversioned, and the version is stamped into the curated release artifact at cut time. The anti-drift intent of ADR-20 still holds; it just no longer spans two repositories.
Alternatives Considered
- Keep the dev→public mirror. Preserves the shipped launch flow. Rejected: two repos is a discouraged anti-pattern here — duplicated history, a copy step that can leak private record or drift, and two identities to keep consistent — for no benefit a packaging filter does not already give.
- One repo, publish everything (no curation). Simplest. Rejected: the design record and local memory are not part of the shipped product; publishing them bloats the artifact and re-opens the exposure ADR-18 guarded against.
- Chosen: one repo,
.abcd/**excluded by packaging,launchcuts a curated GitHub Release. Single history and identity; curation is a build filter, not a repo boundary.
Consequences
- The launch flow reduces from mirror-and-publish to package-and-release: filter
.abcd/**out of the artifact, stamp the version, cut the GitHub Release. - The release-artifact exclusion is the single enforcement point for what stays private; there is no second repo whose contents can diverge from the source.
- The dev-unversioned / release-versioned invariant (ADR-19, ADR-20) now applies within one tree: the version lives only in the cut artifact, and the anti-drift check runs over the paths that get packaged.
- The repo being the marketplace means discovery, install, and the design record share one location; contributors and consumers see the same history.
Frontmatter
| id | adr-28 |
|---|---|
| type | adr |
| status | accepted |
| date | 2026-07-06 |
.abcd/development/decisions/adrs/0028-single-repo-curated-release.md
open on GitHub ↗ · commit history ↗
References1
- adr-18not in the treesupersedes
Referenced by13
- 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-codedrelated
- adr-20The two release manifests stay version-consistent by an anti-drift checker over a pinned path list; the source view stays unversioned; `--allow-dirty` must never bypass itrelated
- adr-31The release version is derived from the intents in it, never authoredrelated
- adr-33Launch phase ownership is tiered; the phase index is the sole ownership sourcerelated
- adr-35The lifeboat is a coverage experiment — read-only, out-of-tree, and proven before it is packed (supersedes adr-4)related
- adr-37Releases are changelog-driven — rolling `[Unreleased]` is the release decision, and automation tags exactly that commitrelated
- adr-47abcdev.app is rendered from this repository alonerelated
- adr-2609231048308186The catalog pins the latest release's plugin archiverelated
- itd-36Knowledge That Compounds, Not Knowledge That Re-Derivesrelated
- itd-65abcd Refuses To Publish A Payload Until The Full Ship-Time Pre-Flight Gate Suite Passes, Not Just The Secret Scanrelated
- itd-66abcd Renders The Exact Public Payload, Proves The Excludes Never Leak, And Smoke-Tests The Installed Surface Before Any Snapshotrelated
- itd-67abcd Is An Installable, Versioned Claude Code Plugin Whose Repo Is Its Own Marketplace And Whose Every Launch Bumps, Tags, And Publishes A New Versionrelated
- itd-72Publishing A Release Is One Atomic, Refusable `ship`related
mentions17
- adr-21Rebuild abcd as a Go binary
- dcn-26080500000000052026-08-05 — iss-43 (v0.5.0 item D): the three-claim corpus is closed as OVERTAKEN rather than fixed — the Status section and the surface list were removed by `73428b6` (2026-07-17, "manual: Revise README with new badges and project details"), a manual revision predating the v0.5.0 plan, so the Phase 0 claim and the native review oracle / spec-task engine / autonomous run claims no longer exist to correct, and re-deriving a corpus against a rewritten document would be inventing findings to justify a ticket. That attribution is itself a correction worth recording: the round first credited the iss-143 tagline commit `48a3524`, which touches ONE README line (the strapline), because it read a SHALLOW clone whose truncation window opened well after 2026-07-17 and therefore could not see the real commit — a truncated history answers a `git log` question confidently and wrongly, and the cheap guard is `git log -S` over the file for the exact removed string, unshallowed first. Two corollaries earned the same way: the corpus's third claim was never `73428b6`'s at all (`git log -S "never shipped"` puts that line in the scaffold commit and in this branch alone), so "all three overtaken" was a second wrong attribution riding on the first; and a whole-phrase probe can miss a claim that was line-wrapped in the original, which is why `"spec/task"` reproduces where `"spec/task engine"` returns nothing. One instance survives, and it is half true rather than false, which is why it needed splitting instead of deleting: the Layout line called `.abcd/` "never shipped", which holds for the released binaries alone (release.yml uploads the four of them and `checksums.txt`, nothing else) and fails everywhere else — `.claude-plugin/marketplace.json` declares `source: "./"`, so a marketplace install takes the whole repository, and GitHub attaches auto-generated source archives to each release, which carry the directory too because `.gitattributes` declares no `export-ignore`. The line now divides on the boundary that is real: every repository checkout against the released binaries, rather than a per-channel split that would have been wrong about the release page. Doc-side deliberately: the marketplace manifest schema has no per-path exclusion, so making "never shipped" true everywhere means building a curated publish path — feature work with its own design questions, not doc repair, and writing the doc to describe a packaging mechanism that does not exist is the phantom-claim failure this item belongs to. The proposed detector (README capability and status claims resolve to wired verbs and to the roadmap) is DROPPED here and re-pointed at iss-181 as a candidate extension of the gate cross-check detector's scope, recorded as a candidate rather than a commitment because it shares that detector's two unsettled scoping questions and a second half-specified scanner would answer neither. The same claim survives across the record — CONTEXT.md's sharp-edges list, AGENTS.md, both `.abcd/` READMEs, `02-constraints/01-platform.md`, `05-internals/03-configuration.md` and `phase-1-ahoy.md`, with adr-0028, the dated plans and the planned/shipped intent bodies exempt as decision and historical records — and it is recorded as iss-183 rather than fixed here, since the disposition scoped this item to one README line. That issue also corrects a claim this round made twice: the exclusion is NOT unimplemented. `internal/core/launch/bundle.go` denies the `.abcd` namespace structurally, with tests, reachable from the wired `abcd launch ship`; it is unwired, because `Ship` stops at `WouldPublish` with no network call and `release.yml` uploads the binaries without ever invoking the verb. "No mechanism exists" and "the mechanism never runs" ask for different fixes, and only the second is true.
- dcn-26082600000000042026-08-26 — Bug-hunt round 8 (bughunt-a, state #368): baseline green; five
- dcn-26110113323065712026-11-01 — The technical facilitator's ruling of 2026-11-01 on publishing schemas: abcd generates JSON Schemas for its boundary documents from the Go types in this repository, record front-matter included and run-state excluded, gated against drift and shipped with each release under a stable location-independent $id namespace; the work folds into itd-113, whose MCP front door needs per-tool input schemas anyway, and the OpenAI-compatible oracle adapter sends a reply's schema with each request so a local model server can constrain its answer. A separate schema repository, superseding adr-28, is reopened only when a non-Go implementation needs its own cadence or a second organisation proposes a schema change. Reverse by superseding adr-28.
- iss-23itd-36 is partially superseded by adr-28 (launch-gate framing rewritten); reconcile body with the ADR before its spec plans
- iss-183The blanket `.abcd/` exclusion claim that iss-43 corrected in the README survives across the record. Recorded rather than fixed there, because the disposition scoped iss-43 to one README line.
- iss-2608220150157498Eight typed cross-references point at targets absent from the tree — adr-22 supersedes adr-14, adr-15 and adr-17; adr-25 supersedes adr-8; adr-27 supersedes adr-16; adr-28 supersedes adr-18; adr-35 supersedes adr-4 (all retired under retire-the-name); itd-3 names spec_id spc-1 which has no file — and nothing checks supersedes targets today. The 2026-08-21 site investigation counted six; the in-session grep found eight. The planned site build arms the detector via the .abcd/site-baseline.json ratchet, seeded with what the build finds, and the tombstones-or-stubs question (itd-136/itd-137) decides how they render
- iss-2608231346137587A full-tier `iss35-brief-surface-crosscheck` at content commit `7a4ee00`
- iss-2608250743421381two slash-command argument-hints advertise sub-verbs the binary refuses, in the one string a user reads before invoking. commands/ahoy.md:4 offers '[status | install | uninstall | doctor | dry-run]': 'abcd ahoy status' exits 2 with unknown command — bare 'abcd ahoy' is the status render — and the real sub-verb identity-check is absent from the hint. commands/launch.md:4 offers '[dry-run | ship | scaffold]': dry-run is a FLAG, so 'abcd launch dry-run' exits 2 while 'abcd launch --dry-run' exits 0; the binary registers only scaffold and ship. Both files' own bodies get it right, so the drift is confined to the frontmatter hint, which is exactly the part no prose reader checks and the part the harness surfaces first. The brief does NOT get it right: 04-surfaces/01-ahoy.md's dispatch sentence carries the same phantom status token, which is brief-tier and tracked under iss-2608231346137587. A first attempt at this fix ALSO added identity-check to the ahoy hint, which is wrong in the other direction: that verb is deliberately CLI-only, the command page carries a scoping note saying it stays a bare-CLI entrypoint rather than a plugin sub-verb, and the page carries a flow section for every other hinted token and none for it. The plugin dispatch set is install, uninstall, doctor and dry-run. This is a shipped surface, not record drift: commands/ is carried in the release bundle, so the adr-28 disposition that covers brief-tier findings does not apply. Found by the iss-35 crosscheck's plugin-command-surface probe at the v0.6.6 gate; the docs-currency pass had not flagged it across three runs, because its scope reads command prose rather than frontmatter.
- iss-2608260944055780Brief self-contradiction on abcd lint's exit grammar: 05-internals/06-lint.md:174 states "Exit code follows the standalone grammar (blocker -> 1, warn -> 2, clean -> 0)", which inverts the two non-zero codes. internal/core/repolint/repolint.go:147 exitCode() returns 2 on any blocker, 1 on warnings only, 0 clean, and its own comment names the Conftest tri-state; 04-surfaces/16-lint.md:41 agrees with the code. Observed: this tree at 19 errors and 3 warnings exits 2. The consequence is worse than count drift, because an exit code is a number a CI job branches on: a job written against 06-lint.md would treat 1 as its failure condition and pass green on a tree full of blockers, a gate that runs and never fires. Same class as iss-142 (the iss-37 phantom-enforcement class) and the same file, different line; iss-181 and iss-251 also sit on this file and cover neither. Brief-tier, so under adr-28 it ships in no release artefact and reaches no installed surface, which is the standing disposition for brief drift and the argument for a lower severity. Refines iss-2608261029074763, which records the wider defect this is one symptom of: 06-lint.md section 3 documents an architecture that was never built, verified as internal/core/lint having no package main so the documented invocation cannot run, --codes-filter, --promote-check, --touched-line and --display-path having zero hits across internal/ and cmd/, no .pre-commit-config.yaml, and IntentLinter, _classify_and_filter, _parse_hunk_lines and _detect_location present in no source file. That record cites this one for the exit grammar rather than restating it. Adjacent and distinct: iss-2608231000561060 covers the gates-CI claim and gate-readiness. Detector: enforcement-claims-are-facts sweep over the brief's exit-code and gating claims. Acceptance corpus: the three lines above.
- iss-2610262032245680Library defers the lifeboat's memory payload to the spec that wires the packer; disembark has the packer shipped with a fixed file set and no memory section
- iss-2610262043303389itd-25 reads every repo listed in .abcd/corpus.json; itd-40 says abcd does not group repos and keeps one registry
- itd-40abcd Knows What Kind of Folder You're In
- itd-108The plugin installs from the curated release artifact, not the repo, and every cut release reaches users automatically
- itd-160Eight typed cross-references point at targets absent from the tree — adr-22 supersedes adr-14, adr-15 and adr-17; adr-25 supersedes adr-8; adr-27 supersedes adr-16; adr-28 supersedes adr-18; adr-35 supersedes adr-4 (all retired under retire-the-name); itd-3 names spec_id spc-1 which has no file — and nothing checks supersedes targets today. The 2026-08-21 site investigation counted six; the in-session grep found eight. The planned site build arms the detector via the .abcd/site-baseline.json ratchet, seeded with what the build finds, and the tombstones-or-stubs question (itd-136/itd-137) decides how they render
- spc-38alice-opens-the-record-explorer-and-reads-the-whole-developm
- spc-52dangling-supersedes-and-spec-targets-nothing-checks-them