/ca:release is the one shipped skill that is not portable. It encodes this repository's release mechanics as skill logic rather than reading them from project state, so a repo that installs codeArbiter gets a lane that cannot execute.
Scope note. This body was rewritten 2026-07-30 after design work materially widened the change. The original body described only the release-skill fix. See the earlier correction comment for the measurement errors it contained, which are fixed below.
Spec: .codearbiter/specs/release-portable-fixture.md (rev 3, after two adversarial review passes)
Decisions: DECISION-0034 in .codearbiter/decisions/decision-log.md (supersedes DECISION-0033)
The hard failure
Pre-flight's first resolution step is:
TAG_PREFIX=$(python3 .github/scripts/_releaselib.py tag-prefix $TARGET)
That helper is not in the plugin payload. Neither are the other two files the skill depends on:
| referenced by the skill |
actual location |
under plugins/ca/? |
_releaselib.py |
.github/scripts/_releaselib.py |
no |
build-host-packages.py |
tools/build-host-packages.py |
no |
published-tags.json |
.github/published-tags.json |
no |
In a consumer repo those paths do not exist. The likely agent response is not a clean error but an improvised substitute, which in a gate skill is the worse outcome: the whole point of release being the single permitted path to a version tag is that it does not get improvised.
Measurement rule
Contamination counts are pattern-dependent, and the original body quoted a number without stating its tokenizer. The rule, not the count, is what matters: a reference is contaminating when it names a path belonging to this repository that the skill executes or reads.
Bare substring matching is explicitly rejected. plugins/ca/skills/context-creation/SKILL.md:49 has a scout read .github/workflows/, .gitlab-ci.yml, and Jenkinsfile — these describe the consumer's repository and are correct as written. A naive guard would flag them forever.
Under the reference-form rule, three shipped skills are contaminated:
release — heavily, and the subject of this issue
subagent-driven-development:45 — dispatches tools/farm.js, which ships at plugins/ca/tools/farm.js, so the bare path resolves in neither form
decision-lifecycle:70 — names .github/scripts/check_adr_identity.py, a CI-only script that is not shipped
When it happened
Not at the four-plugin change, which is the intuitive suspect:
6a45173 Phase 7a: relocate plugin into plugins/ca/ (portable, 51 lines)
261374b feat(release): publish the GitHub Release in Phase 3
c12b1a3 Release-skill hardening + test-debt paydown (#125) <-- inflection
29ab107 fix(release): harden /ca:release with tested guards
3d5f6c9 feat(ci): detect published-tag drift (#468)
c20c2d0 feat(release): targets any of the four plugins (#497)
Independently verified: 6a45173 shipped a 51-line skill with zero repo-path references, and c12b1a3 is an ancestor of c20c2d0. Portability was lost five commits before any multi-plugin work; #497 inherited an already-repo-specific skill.
None of those commits were careless. The original portable version resolved the last tag with bare git describe --tags --abbrev=0, which the current skill correctly condemns as the thing that silently bases a release on a sibling plugin's tag. Every hardening commit fixed a real failure. The defect is that each fix was written as a hardcoded fact about this repository instead of a parameter read from project state.
The maintainer path is healthy
This is a portability defect, not an active breakage. All four subcommands the skill invokes are implemented and run (tag-prefix ca returns v, last-tag v returns v2.8.13), CI is green on main, and sync-core --check passes across 53 files x 3 plugins.
Design
Full detail in the spec. In summary:
- Split
_releaselib into portable mechanism — which moves to core/pysrc/ and generates byte-identically into all three governance plugins via the existing ADR-0011 mechanism — and repo-specific data, which moves to project state. Repo-specific defaults inside the mechanism (MERGE_READINESS_CHECK, the last_tag_select prefix, select_release_target's target list) become required parameters.
.codearbiter/release-targets.md carries one sub-block per target with a defined grammar and an explicit parser contract, parsed stdlib-only per ADR-0004 using the HTML-comment delimiter convention _scopelib already recognizes. It lives outside CONTEXT.md on context economy: CONTEXT.md is read every session, release configuration only when tagging.
- Pre-tag commands are declared per row and check-only (DECISION-0034). They assert and exit non-zero on drift; they may never mutate the tree, and the clean-tree assertion is unconditional. Reconciliation stays a separate operator action routed through
commit-gate.
- A new protected-write class guards the declaration file, since it is the only executable file in
.codearbiter/ and no existing class fits — context admits any write preserving arbiter: enabled frontmatter, marker blocks outright, audit is append-only, and decisions requires a marker only /adr mints.
- Onboarding owns elicitation.
context-creation scouts and writes full rows; decompose elicits intent only, since it runs before any manifest or tag exists. Release-time detection is a back-fill path requiring explicit confirmation.
- CI reads the same declared source, and target selection becomes name-keyed rather than positional — today
select_release_target zips confirmations positionally against RELEASE_TARGETS while release.yml passes them in hardcoded order, so moving row order into an editable file would let a reorder remap which input drives which contents: write publisher.
.github/scripts/_releaselib.py becomes a permanent, data-free shim rather than being deleted, because six CI sites shell out to that path (release.yml:135,171 and publish-release/action.yml:125,164,180,228).
Acceptance
42 criteria across six slices in the spec. The load-bearing ones:
Review provenance
Two adversarial passes against the spec. Pass 1 found the criteria set literally unsatisfiable (the portability guard forbade the .github/ path the tag-provenance hard rule requires), the slice ordering CI-breaking, the provenance drift trigger decorative, the golden test's oracle circular, and the positional target selection a reorder-to-mispublish hazard. Pass 2 verified 11 of 15 repairs sound and found four defects, two introduced by the repairs themselves. A third narrow pass on the new protected-write class is in progress.
/ca:releaseis the one shipped skill that is not portable. It encodes this repository's release mechanics as skill logic rather than reading them from project state, so a repo that installs codeArbiter gets a lane that cannot execute.Spec:
.codearbiter/specs/release-portable-fixture.md(rev 3, after two adversarial review passes)Decisions: DECISION-0034 in
.codearbiter/decisions/decision-log.md(supersedes DECISION-0033)The hard failure
Pre-flight's first resolution step is:
That helper is not in the plugin payload. Neither are the other two files the skill depends on:
plugins/ca/?_releaselib.py.github/scripts/_releaselib.pybuild-host-packages.pytools/build-host-packages.pypublished-tags.json.github/published-tags.jsonIn a consumer repo those paths do not exist. The likely agent response is not a clean error but an improvised substitute, which in a gate skill is the worse outcome: the whole point of
releasebeing the single permitted path to a version tag is that it does not get improvised.Measurement rule
Contamination counts are pattern-dependent, and the original body quoted a number without stating its tokenizer. The rule, not the count, is what matters: a reference is contaminating when it names a path belonging to this repository that the skill executes or reads.
Bare substring matching is explicitly rejected.
plugins/ca/skills/context-creation/SKILL.md:49has a scout read.github/workflows/,.gitlab-ci.yml, andJenkinsfile— these describe the consumer's repository and are correct as written. A naive guard would flag them forever.Under the reference-form rule, three shipped skills are contaminated:
release— heavily, and the subject of this issuesubagent-driven-development:45— dispatchestools/farm.js, which ships atplugins/ca/tools/farm.js, so the bare path resolves in neither formdecision-lifecycle:70— names.github/scripts/check_adr_identity.py, a CI-only script that is not shippedWhen it happened
Not at the four-plugin change, which is the intuitive suspect:
Independently verified:
6a45173shipped a 51-line skill with zero repo-path references, andc12b1a3is an ancestor ofc20c2d0. Portability was lost five commits before any multi-plugin work; #497 inherited an already-repo-specific skill.None of those commits were careless. The original portable version resolved the last tag with bare
git describe --tags --abbrev=0, which the current skill correctly condemns as the thing that silently bases a release on a sibling plugin's tag. Every hardening commit fixed a real failure. The defect is that each fix was written as a hardcoded fact about this repository instead of a parameter read from project state.The maintainer path is healthy
This is a portability defect, not an active breakage. All four subcommands the skill invokes are implemented and run (
tag-prefix careturnsv,last-tag vreturnsv2.8.13), CI is green on main, andsync-core --checkpasses across 53 files x 3 plugins.Design
Full detail in the spec. In summary:
_releaselibinto portable mechanism — which moves tocore/pysrc/and generates byte-identically into all three governance plugins via the existing ADR-0011 mechanism — and repo-specific data, which moves to project state. Repo-specific defaults inside the mechanism (MERGE_READINESS_CHECK, thelast_tag_selectprefix,select_release_target's target list) become required parameters..codearbiter/release-targets.mdcarries one sub-block per target with a defined grammar and an explicit parser contract, parsed stdlib-only per ADR-0004 using the HTML-comment delimiter convention_scopelibalready recognizes. It lives outsideCONTEXT.mdon context economy:CONTEXT.mdis read every session, release configuration only when tagging.commit-gate..codearbiter/and no existing class fits —contextadmits any write preservingarbiter: enabledfrontmatter,markerblocks outright,auditis append-only, anddecisionsrequires a marker only/adrmints.context-creationscouts and writes full rows;decomposeelicits intent only, since it runs before any manifest or tag exists. Release-time detection is a back-fill path requiring explicit confirmation.select_release_targetzips confirmations positionally againstRELEASE_TARGETSwhilerelease.ymlpasses them in hardcoded order, so moving row order into an editable file would let a reorder remap which input drives whichcontents: writepublisher..github/scripts/_releaselib.pybecomes a permanent, data-free shim rather than being deleted, because six CI sites shell out to that path (release.yml:135,171andpublish-release/action.yml:125,164,180,228).Acceptance
42 criteria across six slices in the spec. The load-bearing ones:
core/surface/skills/**and fails on any reference naming a this-repo path the skill executes or reads, by the stated rule, permitting scout scan-target patternsrelease/ca:releaseend to end with no file outside the plugin payloadcaandca-picommands/release.mdmatches the skill it routes toReview provenance
Two adversarial passes against the spec. Pass 1 found the criteria set literally unsatisfiable (the portability guard forbade the
.github/path the tag-provenance hard rule requires), the slice ordering CI-breaking, the provenance drift trigger decorative, the golden test's oracle circular, and the positional target selection a reorder-to-mispublish hazard. Pass 2 verified 11 of 15 repairs sound and found four defects, two introduced by the repairs themselves. A third narrow pass on the new protected-write class is in progress.