Skip to content

release skill ships codeArbiter's own release process to every consumer repo #563

Description

@SUaDtL

/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:

  1. 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.
  2. .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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. .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:

  • A guard scans 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 patterns
  • All three contaminated skills fixed, not only release
  • A consumer repo with one artifact runs /ca:release end to end with no file outside the plugin payload
  • This repo still releases all four plugins with per-series isolation and payload scoping intact, proven by a resolution trace against a recorded pre-change run covering ca and ca-pi
  • No repo-local variant of the skill exists
  • No commit leaves a CI consumer broken; the migration ordering is stated and pinned
  • commands/release.md matches the skill it routes to
  • The docs-site guide distinguishes the general lane from this repo's configuration

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdecisionNeeds a user decision / ADR (not a straight fix)sev:highTribunal/triage: high severity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions