Skip to content

fix(release): harden /ca:release with tested guards (v2.release.0002-0006)#138

Merged
SUaDtL merged 1 commit into
mainfrom
feat/release-skill-hardening
Jun 26, 2026
Merged

fix(release): harden /ca:release with tested guards (v2.release.0002-0006)#138
SUaDtL merged 1 commit into
mainfrom
feat/release-skill-hardening

Conversation

@SUaDtL

@SUaDtL SUaDtL commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

What

Hardens /ca:release against five red-team findings (v2.release.0002–0006) by moving load-bearing, prose-only steps onto tested mechanical helpers.

New stdlib-only .github/scripts/_releaselib.py (+ test_release_lib.py), following the _metricslib/_taskboardlib lib+test pattern, with a thin CLI the skill shells out to:

Helper Finding What it guards
last_tag_select 0006 baseline tag — highest vX.Y.Z, excludes -beta/-rc/-alpha and ca-sandbox-v*
notes_heading_matches 0005 the notes-file's first ## vX.Y.Z heading equals the tag before gh release create
release_dates_consistent 0004 changelog — DATE == tag Released-at: DATE (date derived once via date +%F)
classify_publish_state 0003 publish_fresh / resume_publish / already_published / abort_mismatch — resumes a half-finished publish instead of dead-ending on "tag exists"

release/SKILL.md now invokes these, and (0002) rebuilds farm.js unconditionally with the CI tools job named as the mechanical backstop — the old form gated the rebuild on an in-window farm.ts change and missed a pre-stale bundle. Scope stays ca-only; sandbox.js freshness is left to casandbox.release.0001.

Why this shape

0002 stayed a prose + structural-test fix rather than a new guard because CI's tools job already rebuilds-and-diffs farm.js on every plugins/ca/** PR — a duplicate guard would add nothing. The other four findings have no CI coverage (release-time logic), so their tested helpers add genuinely new enforcement.

Tests

  • test_release_lib.py (29 tests: unit + CLI + SKILL.md structural), wired into the CI hooks job and tech-stack.md.
  • Full repo suite green (11 suites + hooks unittest discover).
  • security-reviewer PASS on the CI/infra change (no new permissions, no secrets, no injection).

Incidental (in this PR)

  • Renamed ca-sandbox.release.0001casandbox.release.0001 — a pre-existing board id that violates the dotted-id grammar (hyphen in the group segment). It was latent because a pure .codearbiter/ edit doesn't trigger CI's hooks job; this branch does, so test_taskboardlib's board-conformance check would otherwise fail. (Resolution chosen by the maintainer over loosening the grammar.)
  • Normalized SKILL.md CRLF→LF per coding-standards (it was the lone CRLF .md). Raw diff is 146 lines; real content delta is 5 (git diff --ignore-cr-at-eol).
  • Opened v2.prune.0001 board finding (the prune metric conflates file-bytes with context-tokens) — rides this branch per request.

Follow-up discovered (not fixed here)

The H-09b commit hook false-blocks git commit -F - <<heredoc multi-line messages: commit_pathspecs parses the heredoc body as pathspecs, then git diff HEAD -- <body tokens> errors and the gate fails closed. Heredoc is the recommended multi-line-commit pattern, so this bites legitimate commits. Worth a board finding against pre-bash.py commit_pathspecs (bound the pathspec parse to the actual command line, not heredoc/redirect bodies).

https://claude.ai/code/session_011nnQYBPd5Dt1rmtf9CHNfU

…0006)

Five red-team findings left load-bearing release steps prose-only and
silently failable. Add stdlib _releaselib.py helpers - last_tag_select
(ca-only baseline), notes_heading_matches, release_dates_consistent,
classify_publish_state - plus a thin CLI; SKILL.md now invokes them,
rebuilds farm.js unconditionally (CI tools job named as backstop),
derives the release date once, and branches on publish-state instead of
dead-ending on "tag exists". Wired into CI (hooks job) + tech-stack.

Rename the pre-existing invalid board id ca-sandbox.release.0001 ->
casandbox.release.0001 (failed the dotted-id grammar; latent because
pure-board edits don't trigger the hooks CI job, which this branch does).
Normalize SKILL.md CRLF->LF per coding-standards (68-line EOL delta;
real content = 5 edits). Open v2.prune.0001 (prune metric conflates
file-bytes vs context-tokens), riding this branch per request.

CHANGELOG: /ca:release resolves the baseline tag, notes heading,
release-date consistency, and half-finished-publish recovery through
tested helpers, and rebuilds farm.js unconditionally every release.

Ref: .codearbiter/specs/release-skill-hardening.md

Claude-Session: https://claude.ai/code/session_011nnQYBPd5Dt1rmtf9CHNfU
@SUaDtL
SUaDtL merged commit cb94951 into main Jun 26, 2026
22 checks passed
@SUaDtL
SUaDtL deleted the feat/release-skill-hardening branch June 26, 2026 14:37
SUaDtL added a commit that referenced this pull request Jun 26, 2026
v2.rev.0020 (farm.ts god-module split) landed via #140. The five
release-skill hardening tasks v2.release.0002-0006 landed via #138 but the
board was never updated in that PR. Flip all six to done (2026-06-26).

Claude-Session: https://claude.ai/code/session_0123LEtLuVCEzLMhWdktP7aC

Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
SUaDtL added a commit that referenced this pull request Jun 26, 2026
)

A task isn't done until its PR merges, but open-tasks.md is itself versioned,
so a board flip only goes live when its own change merges to main. That created
a trap with no good side: flips drifted into lagging chore(board) PRs or
post-commit edits that depended on cross-session memory, and silently fell off
(#138 never marked v2.release.0002-0006 done; #140 needed the separate #141).

ADR-0008 makes commit-gate the single board-sync chokepoint, co-locating the
flip with the work commit: it is invisible on main until merge, lands atomically
when the work does, and an abandoned PR abandons the flip with it -- so the
"don't mark done before merge" and "don't lose the board update" rules become
the same, self-correcting rule. No task-to-commit inference is introduced; the
human still declares the transition via /ca:task.

Mechanism: a new pure _taskboardlib.classify_board_diff recognizes a clean
done/start/add transition vs an arbitrary board edit; commit-gate Phase 6 retains
a classified transition instead of flagging it as scope creep; Phase 7 stages it
by explicit path; the raise-new harvest moved to Phase 7 (pre-commit) so a
discovered follow-up rides the work commit (contingent default -- a must-survive
follow-up is filed as a GitHub issue). A read-only /ca:standup advisory sweep
(hooks/boardsync.py reconcile, best-effort) surfaces any merged-but-not-flipped
task; the fix still routes through /ca:task. Conflict-hierarchy level:
maintainability/reliability (SMARTS, per ADR-0008).

Folds into the unreleased 2.6.0 (payload already ahead of tag v2.5.2; no
over-bump). All 10 plan tasks shipped test-first; full tech-stack suite green;
security review PASS (one LOW hardened).

CHANGELOG: commit-gate now lands task-board done/start/add transitions in the
same commit as the work (no separate chore(board) PR); /ca:standup gains a
read-only drift sweep for any merged-but-not-flipped task.
Closes #142
Ref: ADR-0008
Claude-Session: https://claude.ai/code/session_018n3e8e2RWU7hLzzJjEAscb

Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
SUaDtL added a commit that referenced this pull request Jun 27, 2026
)

A task isn't done until its PR merges, but open-tasks.md is itself versioned,
so a board flip only goes live when its own change merges to main. That created
a trap with no good side: flips drifted into lagging chore(board) PRs or
post-commit edits that depended on cross-session memory, and silently fell off
(#138 never marked v2.release.0002-0006 done; #140 needed the separate #141).

ADR-0008 makes commit-gate the single board-sync chokepoint, co-locating the
flip with the work commit: it is invisible on main until merge, lands atomically
when the work does, and an abandoned PR abandons the flip with it -- so the
"don't mark done before merge" and "don't lose the board update" rules become
the same, self-correcting rule. No task-to-commit inference is introduced; the
human still declares the transition via /ca:task.

Mechanism: a new pure _taskboardlib.classify_board_diff recognizes a clean
done/start/add transition vs an arbitrary board edit; commit-gate Phase 6 retains
a classified transition instead of flagging it as scope creep; Phase 7 stages it
by explicit path; the raise-new harvest moved to Phase 7 (pre-commit) so a
discovered follow-up rides the work commit (contingent default -- a must-survive
follow-up is filed as a GitHub issue). A read-only /ca:standup advisory sweep
(hooks/boardsync.py reconcile, best-effort) surfaces any merged-but-not-flipped
task; the fix still routes through /ca:task. Conflict-hierarchy level:
maintainability/reliability (SMARTS, per ADR-0008).

Folds into the unreleased 2.6.0 (payload already ahead of tag v2.5.2; no
over-bump). All 10 plan tasks shipped test-first; full tech-stack suite green;
security review PASS (one LOW hardened).

CHANGELOG: commit-gate now lands task-board done/start/add transitions in the
same commit as the work (no separate chore(board) PR); /ca:standup gains a
read-only drift sweep for any merged-but-not-flipped task.
Closes #142
Ref: ADR-0008
Claude-Session: https://claude.ai/code/session_018n3e8e2RWU7hLzzJjEAscb

Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
SUaDtL added a commit that referenced this pull request Jun 27, 2026
* docs(changelog): roll the 2.6.0 release notes

Reconcile the [2.6.0] CHANGELOG section for the release: broaden the intro
beyond the farm work, add context-drift provenance (#145) and file-scoped
context injection (#146) under Added, note the MIT to AGPLv3 relicense (#147,
ADR-0009) under Changed, and add a Fixed section for #143, #139, and #138.
Dated 2026-06-27.

Also flips v2.feature.0001 to done now that file-scoped context injection has
shipped and merged; the flip rides this release commit per ADR-0008.

No version bump: plugin.json is already 2.6.0 and the ca-scoped commit window is
unchanged. The relicense and docs commits touch no plugins/ca/ payload.

Claude-Session: https://claude.ai/code/session_01QaLw7nTrRbHwygFar2R6zu

* docs(license): reserve the dual-licensing right without offering it yet

Soften the README "Commercial licensing" wording. The prior text implied
proprietary licenses were actively on offer; the intent is narrower: the owner
retains sole ownership and reserves the right to dual-license, but is not
offering commercial licenses at this time. Inquiries may still be sent through
GitHub and will be considered if and when a commercial path is established.

The legal substance is unchanged. AGPLv3 plus sole copyright already secures the
right; this only corrects the public statement so it does not overstate an
active commercial offering. ADR-0009, the CLA, and the CHANGELOG already use the
reserves-the-right framing, so no other file changes.

Claude-Session: https://claude.ai/code/session_01QaLw7nTrRbHwygFar2R6zu

---------

Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant