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.
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 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.
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.
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 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 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.