Push-triggered release safety net (convergence checker + shared bump/release scripts + safety-net job), maintenance drift reconciliation, and the full red-first discharge of both gate reviews (11 findings: env-contract BLOCKER, CONVERGED case-mismatch BLOCKER, merge-subject classification, version-behind-tag anomaly, version-surface fatality, label-ID resolution, attribution-guard anchoring, URLError handling, minors).
The PR-closed path remains authoritative when its event fires; the push path only reconciles what slipped past (convergence-only, never re-runs PR-title semver).
Verification
Suite 730 passed + 84 subtests; pyright ratchet 117e/407w (not increased); ruff clean; both workflow YAMLs parse; henxels exit 0.
Workflow↔script env contract now under test (tests/test_workflow_script_contract.py) — the systematic blind spot the spec gate identified is closed.
Live acceptance (two consecutive API-merged no-bump PRs → release runs) verifies on the next real merges post-merge; the push trigger guarantees a run within ~1 minute regardless of closed-event delivery.
## Summary
- Push-triggered release safety net (convergence checker + shared bump/release scripts + safety-net job), maintenance drift reconciliation, and the full red-first discharge of both gate reviews (11 findings: env-contract BLOCKER, CONVERGED case-mismatch BLOCKER, merge-subject classification, version-behind-tag anomaly, version-surface fatality, label-ID resolution, attribution-guard anchoring, URLError handling, minors).
- The PR-closed path remains authoritative when its event fires; the push path only reconciles what slipped past (convergence-only, never re-runs PR-title semver).
## Verification
- Suite 730 passed + 84 subtests; pyright ratchet 117e/407w (not increased); ruff clean; both workflow YAMLs parse; henxels exit 0.
- Workflow↔script env contract now under test (tests/test_workflow_script_contract.py) — the systematic blind spot the spec gate identified is closed.
- Live acceptance (two consecutive API-merged no-bump PRs → release runs) verifies on the next real merges post-merge; the push trigger guarantees a run within ~1 minute regardless of closed-event delivery.
Spec-gate finding 1: create_release.py hard-read LIST_API_URL/RELEASE_API_URL
but auto-version.yml only ever passed RELEASE_SERVER_URL/RELEASE_REPO —
KeyError at runtime on both the PR-closed and safety-net release paths.
The script now derives both URLs from the base coordinates (explicit env
still overrides for direct use), and the workflow env blocks stay unchanged.
Adds tests/test_workflow_script_contract.py: a systematic guard that parses
every workflow env contract (step env + $GITHUB_ENV exports + shell exports)
against each invoked script's hard os.environ reads, plus an honesty check
that derived-default keys are never bare-subscripted.
Red evidence: contract test failed on both create_release invocations
(missing LIST_API_URL/RELEASE_API_URL); honesty test failed on the bare
subscripts. Green: 18/18.
Spec-gate finding 2: classify_subject anchored at string start, so Gitea
merge commits ('Merge pull request <feat: x> (#N)') classified as 'other' —
a no-bump feat PR merged via merge-commit was invisible to the safety net
and to maintenance drift attribution.
Subjects matching the merge-commit form are now pre-processed: the quoted
inner title is classified; anything else classifies as before.
Red evidence: 6 new merge-commit fixtures failed (feat/feat(scope)/fix/
fix(scope)/feat!/perf inner titles all classified 'other'). Green: 64/64.
Spec-gate finding 3: assemble_release_decision returned converged=True when
master's __version__ was BELOW the latest tag — silent convergence for a
state that is drift (a bump merged without riding a release, or a tag moved
backwards). The decision now returns converged=False, warranted_bump=None
(the net must not act: bumping master cannot converge with a tag already
ahead) and a new 'anomaly': 'version_behind_tag' field (None otherwise).
The maintenance reconciler files the issue as before (DRIFT=true) and the
issue body now flags the anomaly explicitly.
Red evidence: 2 new decision tests failed with converged=True; the
drift-body anomaly-flag test failed; real-repo tag-ahead test added.
Green: 91/91 across convergence, drift-issue, and contract tests.
Spec-gate finding 4: a GET failure on src/rune/__init__.py was skipped like
an optional surface, so a bump PR could silently bump nothing. The version
surface is now fatal (exit 1 before any PR is filed); the other surfaces
stay optional (pattern absent — skip). The surface-edit loop is extracted to
apply_surface_edits() with an api callable seam for testing.
Red evidence: 4 new tests failed (no is_surface_fatal/apply_surface_edits;
abort test found no SystemExit). Green: 10/10.
Quality-gate finding 5: the convergence step printed json.load(...)['converged']
verbatim — Python True/False (capitalized) — while the safety-net steps
compared env.CONVERGED == 'false' (lowercase string). Both conditionals were
unreachable: the safety net could never bump and the drift advisory could
never fire. The export now wraps the value in str(...).lower().
Adds a static guard to tests/test_workflow_script_contract.py: every boolean
env export (CONVERGED/AUTO_BUMPED/DRIFT) in the parsed workflow run-steps
must apply .lower() or be built from lowercase literals; indirect echo
exports are resolved to their capture line.
Red evidence: guard flagged the json.load export (and its echo) on the
safety-net job. Green: 29/29 contract tests.
Quality-gate finding 6: POST /issues 'labels' requires integer IDs in Gitea;
the script sent the label NAME string, so the label never attached. Label
names are now resolved server-side via GET /labels (repo convention per
shared/custodian.py), created when missing, and skipped — issue still files —
when resolution and creation both fail. main() gains an injectable api
client so the flow is testable with a fake API.
Red evidence: 3 fake-client tests failed against the pre-fix code (network
URLError — no injectable seam; name-string payload). Green: 13/13.
Quality-gate finding 7a: bump_branch_in_flight matched ANY chore/auto-bump-*
branch, so one stale historical branch (branches are never cleaned up) stood
the safety net down permanently. The guard now matches exactly
chore/auto-bump-{NEW} — the in-flight version — with new_version=None as the
documented legacy escape hatch; wait_out_closed_path passes NEW_VERSION
through.
Quality-gate finding 7b (docs): release-flow.md claimed the closed path was
'genuinely absent, not merely slow' and that the paths 'can never
double-bump'. Rewritten: attribution is best-effort; a >90s delayed closed
path can race into a double-bump, bounded by tag idempotency (duplicate
release creation is a no-op); stale-branch cleanup is a known limitation.
Red evidence: anchored-match tests failed against the prefix-wide guard.
Green: 12/12.
🤖Headless spec gate (non-blocking, #204 first slice)
Spec-gate agent produced no text output — check runner logs.
Deterministic floors still govern the merge
This comment is advisory. Required checks (lint, tests, canaries, type-check, coverage, dependency lock, impact gate) remain the merge authority per docs/adversarial-review.md proportionality.
🤖 **Headless spec gate (non-blocking, #204 first slice)**
Spec-gate agent produced no text output — check runner logs.
<details><summary>Deterministic floors still govern the merge</summary>
This comment is advisory. Required checks (lint, tests, canaries, type-check, coverage, dependency lock, impact gate) remain the merge authority per docs/adversarial-review.md proportionality.</details>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Verification
Spec-gate finding 2: classify_subject anchored at string start, so Gitea merge commits ('Merge pull request <feat: x> (#N)') classified as 'other' — a no-bump feat PR merged via merge-commit was invisible to the safety net and to maintenance drift attribution. Subjects matching the merge-commit form are now pre-processed: the quoted inner title is classified; anything else classifies as before. Red evidence: 6 new merge-commit fixtures failed (feat/feat(scope)/fix/ fix(scope)/feat!/perf inner titles all classified 'other'). Green: 64/64.Quality-gate finding 7a: bump_branch_in_flight matched ANY chore/auto-bump-* branch, so one stale historical branch (branches are never cleaned up) stood the safety net down permanently. The guard now matches exactly chore/auto-bump-{NEW} — the in-flight version — with new_version=None as the documented legacy escape hatch; wait_out_closed_path passes NEW_VERSION through. Quality-gate finding 7b (docs): release-flow.md claimed the closed path was 'genuinely absent, not merely slow' and that the paths 'can never double-bump'. Rewritten: attribution is best-effort; a >90s delayed closed path can race into a double-bump, bounded by tag idempotency (duplicate release creation is a no-op); stale-branch cleanup is a known limitation. Red evidence: anchored-match tests failed against the prefix-wide guard. Green: 12/12.🤖 Headless spec gate (non-blocking, #204 first slice)
Spec-gate agent produced no text output — check runner logs.
Deterministic floors still govern the merge
This comment is advisory. Required checks (lint, tests, canaries, type-check, coverage, dependency lock, impact gate) remain the merge authority per docs/adversarial-review.md proportionality.