feat: semantic release gate (iss-35 task 5, VSA, signed via A.1) - #16
Merged
Conversation
Codify the golden rule as a first-class abcd principle: prefer state-of-the-art for design/tooling/practice decisions, but never adopt SOTA that violates the repo's preferences — an adversary reviewer challenges SOTA for repo fit first, then a sota-researcher verdict accounts for that challenge, then the pick. A propagated default for all abcd-managed repos (the "opinionated" in "opinionated configuration"); its managed-repo home is the AGENTS.md conventions section abcd installs. Assisted-by: Claude:claude-opus-4-8
The SOTA-verified design for the semantic (Direction-A) half of the iss-35 graduation, reached via the prefer-sota process (adversary fit-challenge then sota-researcher verdict). The fit-surviving form is a Verification Summary Attestation: a host-run judge emits a signed, sha-keyed PROMOTE/HOLD receipt; a fail-closed record-lint rule verifies it before a tag. Phased build recorded; phases touching signing/release run through security-reviewer first. Assisted-by: Claude:claude-opus-4-8
… 5 ph1) Phase 1 of the semantic release gate (design: plans/2026-07-11-iss35-semantic- release-gate.md). Adds the dev-tier release-gate home: - README.md: the pre-tag runbook. Enumerates both enforcement planes — deterministic (CI, release.yml is the source of truth) and semantic (host-run agent gates) — and owns the semantic half. The not-yet-armed receipt/lockstep machinery (phases 2-4) is marked loudly per loud-staging, never implied done. - brief-surface-crosscheck.js: the iss-35 Direction-A semantic detector, promoted from ephemeral scratch and sanitised — the hardcoded absolute local path is gone (privacy) and the stale skills ground-truth is corrected (abcd ships zero skills; the surface is commands/abcd/). Host-run harness workflow, never CI. Assisted-by: Claude:claude-opus-4-8
…5 ph2) Phase 2 of the semantic release gate. Adds the receipt_gate record-lint rule: the fail-closed, release-time verification of the semantic gates (the LLM passes CI cannot run). Armed with a target commit sha, it asserts every required gate has a receipt at .abcd/work/reviews/<sha>/<gate>.json whose subject digest is that commit, whose verificationResult is PROMOTE, and which pins a judge model. Missing / mismatched / malformed / HOLD / model-less all BLOCK — an un-run semantic pass is never a silent pass. Disabled by default in record-lint.json: it must never fire on ordinary PRs/pushes, only at release time against the tagged commit (arming + signing is phase 4). Adds a VSA-shaped receipt.example.json and updates the runbook's staging note (partially armed) per loud-staging. Security-reviewed (trust boundary): initial BLOCK on a fail-open (enabled + empty required_gates passed vacuously) was fixed with a symmetric fail-closed guard plus Commit/gate-name path-component validation; re-review PASS. The review's Finding 2 (sign receipts AND trust-root the arming config, or signing protects nothing) is recorded as a phase-4 obligation in the design doc. TDD: TestReceiptGate, 11 cases. Assisted-by: Claude:claude-opus-4-8
…sk 5 ph3) Phase 3 of the semantic release gate. Adds the gate_lockstep record-lint rule: the release-gate runbook's deterministic-gate list must stay in lockstep with the CI workflow's verify-job steps (a gate in one but not the other BLOCKS), so the runbook cannot silently drift from what CI enforces. Hand-parsed, no new YAML dependency. Security-reviewed (trust boundary): initial BLOCK found three fail-opens in the hand-parse (the alternate `- uses:`/`name:` step form was invisible, a double-empty parse passed vacuously, a 2-space comment closed a job early). Fixed and re-reviewed PASS: - a non-empty floor (min_gates) — any under-count fails closed, the backstop that makes hand-parsing safe; - step-block parsing captures a step name wherever it appears; - job scoping anchored to the jobs: block + a strict identifier-only header regex (a comment can't be a job boundary); - a missing configured path fails loud, not nil; - symmetric quote normalization. Enabled in record-lint.json (min_gates:9, matching the live 9-gate verify job); watched fire on injected drift. Runbook's staging note updated to present-tense. Follow-up (fail-safe, not fail-open): indentation-anchored step scoping would stop a name: inside a with:/run: block being read as a phantom gate (over-blocks today, never hides drift). TDD: TestGateLockstep, 9 cases. Assisted-by: Claude:claude-opus-4-8
Phase 4 (final) of the semantic release gate — full signed path, no new Go dependency, reusing the actions/attest family + gh attestation verify the repo already uses for binary provenance (investigation confirmed A.1 viable). - ArmReceiptGate (internal/core/lint) + cmd/record-lint --release-gate <sha> / --require-gate <name> flags: a CI workflow arms receipt_gate from the CLI, so the decision to gate, the target commit, and the required-gate list are trust-rooted to the workflow, not the in-tree committer-editable config. The rule stays disabled for ordinary runs. - release.yml (release job, gated `if: !private` like the binary attestation): fail-closed content gate (record-lint --release-gate), signs the receipts with actions/attest (predicate .../semantic-release-gate/v1), verifies with gh attestation verify. Security-reviewed (trust boundary): initial BLOCK found two holes — arming did not force blocker severity (a committed severity:warning would defang the gate) and github.ref_name (a tag name) was interpolated into the signed predicate JSON (injection). Both fixed (force blocker on arming; predicate carries only the immutable github.sha) + an empty-sha fail-closed guard; re-review PASS. Honest scope: signing is auditable release provenance / tamper-evidence, not committer-forgery-proof (a receipt forged+committed pre-tag is signed too) — bounded by the iss-62 identity gate + branch protection; full forgery-prevention would need host-side signing. Dormant on a private repo (attestation is a public-repo feature); activates on the public flip. go.mod unchanged. TDD: TestArmReceiptGate. Assisted-by: Claude:claude-opus-4-8
# Conflicts: # .abcd/development/plans/2026-07-11-iss35-semantic-release-gate.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The semantic (Direction-A) half of the iss-35 brief↔surface graduation — a VSA-shaped semantic release gate. Completes iss-35 alongside the deterministic
surface_coveragerule (PR #14). Zero new dependencies; every phase security-reviewed PASS.Phases
receipt_gate— a fail-closed, release-timerecord-lintrule: blocks a tag unless every required gate has a PROMOTE receipt whose subject digest matches the tagged commit and which pins a judge model. Disabled by default (release-time only).gate_lockstep— the runbook's deterministic-gate list must stay in lockstep withrelease.yml'sverifysteps; a non-emptymin_gatesfloor is the fail-closed backstop. Enabled and enforced.record-lint --release-gate <sha> --require-gate …arms the gate from the CLI (trust-rooted to the workflow, not the in-tree config);release.yml(release job, gatedif: !private) runs the content gate, signs the receipts withactions/attest, and verifies withgh attestation verify— reusing the attest family +ghalready used for binary provenance.Security
Each trust-boundary phase was reviewed; the reviews caught (and this PR fixes) a real fail-open each time: empty
required_gatespassing vacuously; three hand-parser fail-opens; a severity-downgrade defang and agithub.ref_namepredicate injection.Honest scope
Signing is auditable release provenance / tamper-evidence, not committer-forgery-proof (a receipt forged+committed pre-tag would be signed too; bounded by the iss-62 identity gate + branch protection). The whole
release.ymlgate is dormant on a private repo (attestation is public-only) and activates on the public flip. The attestpredicate-typeURI is a placeholder pending confirmation.Verification
make preflight(build, gofmt, vet, test, race) +make record-lintgreen;release.ymlvalidates as YAML;go.modunchanged. New tests:TestReceiptGate(11),TestGateLockstep(9),TestArmReceiptGate.Assisted-by: Claude:claude-opus-4-8