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.

Activity

  1. added
    bugSomething isn't working
    sev:highTribunal/triage: high severity
    decisionNeeds a user decision / ADR (not a straight fix)
    on Jul 31, 2026
  2. SUaDtL commented on Jul 31, 2026

    @SUaDtL
    CollaboratorAuthor

    Correction to the scan table in the issue body

    An adversarial review of the draft spec re-measured the contamination scan and found the original table wrong. Correcting it here rather than silently editing the body.

    The claim "every other skill | 0" is false. plugins/ca/skills/subagent-driven-development/SKILL.md:45 instructs the agent to "dispatch tools/farm.js". That file ships at plugins/ca/tools/farm.js, so the bare tools/farm.js path resolves correctly neither in a consumer repo nor relative to the plugin root. It is genuine contamination of the same class as release's, and it belongs in the scope of this issue.

    The count of 55 is not reproducible, because the issue never stated the tokenizer. Measured against plugins/ca/skills/release/SKILL.md:

    pattern hits
    plugins/ca|plugins/ca-|_releaselib|build-host-packages|published-tags|arbiterForge|check_badge_consistency|\.github/scripts 55
    \.github/|tools/|core/|plugins/ 48
    the reviewer's independent pattern ~44

    The number varies with the pattern, so any guard built for this issue must define its own matching rule and the issue must quote that rule rather than a bare count. The qualitative finding is unchanged and was independently confirmed: release is contaminated by an order of magnitude more than any other shipped skill, and its three helper dependencies genuinely do not ship.

    A third case is a guard-design problem rather than contamination. plugins/ca/skills/context-creation/SKILL.md:49 has Scout B read .github/workflows/, .gitlab-ci.yml, Jenkinsfile — these describe the consumer's repository and are correct as written. A naive substring guard on .github/ would flag them forever. The guard must match on reference form (an executed or read path belonging to this repo) rather than on raw substrings.

    Unchanged and independently verified by the review: the three helpers are outside the plugin payload; the portability inflection is c12b1a3 (#125), which is an ancestor of c20c2d0 (#497); 6a45173 shipped a 51-line skill with zero repo-path tokens; all four _releaselib CLI subcommands work today; CI is green on main; sync-core --check passes across 53 files x 3 plugins.

    Scope additions that follow from the correction:

    • subagent-driven-development's tools/farm.js reference resolves for a consumer
    • decision-lifecycle's .github/scripts/check_adr_identity.py reference resolves for a consumer
    • the guard matches reference form, not substrings, and its rule is stated in this issue
  3. 27 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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