Canary mode detection (the real 18161 defect):
- isfile() is not a valid LIVE/ISOLATED discriminator: run 18141's shim
writes left a zombie 1-entry ledger in the runner's persistent HOME, which
flipped the naive gate to LIVE with wrong data. The discriminator is now
the repo's own CI-sandbox convention (RUNE_HOME=/tmp/rune-ci-<id> — the
workflow's own cleanup guard matches exactly that).
- The isolated battery seeds a PER-RUN temp ledger (module-attr override on
the shim's LEDGER_FILE) instead of writing the runner's persistent home;
binary §5 gets HOME=<temp> so binary and shim resolve the same file.
Both modes: 74/74 locally and under simulated runner topology (zombie
ledger present, RUNE_HOME set). Rust canary §4 gets the same dual-mode
treatment.
Henxels holds cleared:
- plans/: three phase-g files renamed to kebab-case; references updated in
lib.rs, ep_loader.py, canary docstring, Cargo.toml, supersession ledger.
- ffi_query_memory_smoke coverage: smoke repaired (was stranded — imported
the deleted src/rune vintage; now drives shared/ shim, 41/41 green) and
tests/test_ffi_query_memory_smoke.py runs it, skipping honestly when the
cdylib isn't built (CI has no cargo step).
Type ratchet — instrument repair (§16V), not reading-management:
- scripts/type_check_ratchet.py now pins pyright --pythonpath to the
INVOKING interpreter. Ambient-interpreter dependency resolution was the
residual non-determinism: runner (py311, no rich/Pillow) vs desktop (3.14
system site) produced 460 vs 454 on the identical tree, and any import
ignore flipped to unnecessary-ignore in the other environment.
- pyrightconfig extraPaths now covers src/rune + shared + both crate dirs
(bare intra-repo imports resolve under the pinned environment).
- Pillow declared in pyproject (genuine lazy dep of the diamond-frames
surface; was undeclared) — lock regenerated, dep-lock checker green.
- Measured under the pinned instrument: 115 errors / 439 warnings
(CI-equivalent locked 3.11 venv) and 115/436 (local 3.14 venv) — both
below the 117/454 baseline; baseline unchanged. My canary's 4
reportOptionalSubscript errors fixed by real narrowing, not suppression.
Lint baseline: BLE001 346 -> 345 (bulk_issue_guard.py's retry catch gains a
reasoned noqa; the file stays untracked per checkpoint hygiene, but the
honest-ledger test counts disk state — the noqa keeps local == CI).
Local wall: full pytest 2251/0 (incl. the new smoke test); canaries 74+66
both modes; parities 33/24/13/16/17; henxels exit 0; umbrella gate exit 0;
dep-lock green.
Old src/rune/query_memory.py deleted (zero importers verified by sweep).
Coverage-audit registry remapped: shared/query_memory.py (ctypes shim) is
canonical; query_memory out of SRC_A/SHARED_D, into SHARED_B. Supersession
ledger written to plans/. Steps 1-11 evidence in ledger + issue #2448.
The henxels allowed_filetypes rule polices top-level ./docs/* for
markdown/yaml only; machine-generated JSON artifacts are not docs.
Moving rebase-inventory.json and rebase-wave-coverage.json under
docs/rebase/ (outside the single-segment glob) rather than widening
the gate's except list to fit new files. All consumer references
updated (rebase_inventory.py, rebase_issue_factory.py, wave0 report).
Replaces two blind except-Exception-pass handlers in _arg_str/_signature
with (AttributeError, RecursionError, ValueError) and documents the
degraded-metadata contract: a pathological annotation node must never
abort the whole inventory walk.
Reality-wins audit of the accepted Scaffolding V1 system against the live
repo/runtime found three genuine wiring defects (no architecture invented):
1. The MiMo edge was DEAD. `~/.mimocode/hooks/builder-bridge.ts` hardcoded
the ephemeral interpreter `/tmp/ci-venv/bin/python`, which no longer
exists, so every bridge call failed and the hook silently degraded —
MiMo received NO Rune Core preflight at all (zero fire-log lines). The
hook was also UNVERSIONED (no canonical copy), which is how it rotted.
Repair: canonical source `config/mimo-hooks/builder-bridge.ts` with
durable interpreter resolution (env override > repo venv > user venv >
/usr/bin/python3), installed + hash-verified + drift-checked by
`install_mimo_hook.py`, which now manages a hook SET rather than one file.
2. The native path skipped the canonical meaning layer. `construction_intent`
(the §17J/§17L/§17M intent resolver: interrogative structure, negation,
request modals, referent/proposal resolution) existed but was imported
ONLY by tests. `rune_operator._construction_activation` went straight to
`classify_mode`, so a pure question carrying a target reference
("explain how this module works") classified EXISTING_PROJECT_CHANGE and
recorded a construction activation for a discussion turn. Repair: the
intent layer now resolves MEANING before the ONE classifier decides mode
(the boundary the intent module documents). DISCUSSION maps to
NOT_CONSTRUCTION — the canonical vocabulary is unchanged, an explicit
user override (/scaffold-mode) outranks the gate, and an unavailable
intent layer preserves the previous behaviour exactly rather than
silently changing governance.
3. The shared Construction Context reported a TERMINAL contract as active.
`construction_context()` selected on `status == "open"` alone, so 8
contracts with phase `abandoned` (terminal) were reported as the active
construction. Repair: terminal phases are now DERIVED once from the
canonical transition table (`scaffold.TERMINAL_PHASES`), and a contract
is active only when its phase is non-terminal.
Also: the bridge capsule now surfaces the active Scaffolding construction
(contract/phase/scope/obligations/next-action) to the builder as a
read-only projection of the shared Construction Context — the MiMo edge of
state Scaffolding already owns. Inject-nothing is preserved when no
contract is active; the projection never mutates and degrades to [].
Spine projection refreshed (import graph legitimately gained importers).
Honest boundary: no byte-level enforcement point exists. `tool_executor.py`
never consults construction state, so the 19A invariant gates only the
/build-start route. Establishing MiMo-surface enforcement is NEW
architecture (a tool.execute.before governance hook) and was NOT built
here — it is recorded as the exact remaining blocker.
Verification: 511 passed across the construction/operator/bridge/hook/
structural/parity suites; native-default matrix 12/12; real operator-path
proof with fresh-process readback (EXISTING_PROJECT_CHANGE activated,
question -> NOT_CONSTRUCTION, PROJECT_BUILD bootstraps scaffold + registry);
installed hook invoked directly -> capsule injected + fire log written.
LOCAL_MODEL_COMPUTE_EXECUTED = NO.
A just-merged master commit was invisible to branch --contains until refs
were fetched: the local clone's stale master outranked repo truth and a
master_ci operation classified as branch_ci. derive_op_type now refreshes
remote-tracking refs (bounded by GIT_TIMEOUT, fail-open) and containment
checks all refs. Generic fix; no SHA special-casing.
Issues were mash-of-text: unreadable for Nick, unparsable for the pipeline,
and the auto-label workflow guessed 'bug' from keywords — misclassifying
honest prose and fighting the T15 label audit. Changes:
- feature/bug templates: add verifiable close conditions, version-impact
block (declared at submission, verified at merge), why-still-open field;
plain English stays first and is the contract
- research template: question, falsifier, budget, durable findings path;
new 'research' label kind
- custodian create_issue(kind, version_intent): assembles from the
canonical template, forces the kind label, pre-ticks version intent
- auto-label.yml: type label comes from the declared spine kind; keyword
guessing only as pre-template fallback
- reconciliation checker: align live-sync bug-label rule with the
documented open-only exemption
- reconciliation artifact: #158/#162 SUPERSEDED (by #315/#316),
#324 FIXED_VERIFIED (no longer reproduces), #163 revalidated
The two catch-all handlers added in dac6e97 (r8_fault.record evidence
write, notify_event_bridge seam probe) tripped the #126 silent-handler
ratchet (S110 75 -> 77), which cascaded to the lint-baseline contract
test. Both handlers now log to stderr/stdout and continue — the seam
stays fail-open but is no longer silent. S110 back to 75=75.
REAL_ASYNC_BRIDGE R8: narrowest reversible fault for lost-event
failure-recovery. scripts/r8_fault.py consults
$RUNE_WR_ROOT/r8_suppress_rules.json keyed by EXACT operation_id;
notify_event_bridge.py records the suppression decision (no .done file,
no bridge POST) and returns, so the waiter's watchdog/authority
recovery path is the only route to terminal truth. Fail-open design:
absent/corrupt rule file means normal delivery. Evidence appended to
r8_fault_log.jsonl. Healthy-path delivery semantics untouched
(guard clause only). Tests: tests/test_r8_fault_injection.py (7).
CI lint's format-check on added files (base-range dependent) failed on
files that predate the format gate's effective coverage. Formatting-only:
no semantic changes; integration tests 15/15, ruff check clean, ratchet
PASS, daemon convergence re-verified.
- notify_event_bridge: writes durable resumes/<operation_id>.done BEFORE the
bridge POST — delivery no longer depends on the daemon being alive (R3);
best-effort on the same terms as the POST, never fails CI
- completion_file: honors both WR_ROOT (daemon) and RUNE_WR_ROOT (repo)
conventions, WR_ROOT wins — daemon, waiter, and test roots can no longer
disagree about delivery location
- tests/test_completion_delivery: notify-writes-when-bridge-down, write
failure never fails CI, root resolution under both conventions
REAL_ASYNC_BRIDGE gates covered: R3 (partial — daemon side proven in
mimo-wait-resume tests/test_wr_v04_completion.py: 7/7 incl. R4 7ms wake,
R9 durability, R10 byte-identical duplicates)
- Every cycle checks: wr status (daemon may transition to COMPLETED),
durable event log (find_matching_event), completion file (inotify)
- Removed debug print, relaxed test timing for inotify+disk I/O path
- All 11 ci_event_wait + 32 other wait tests green
ROOT CAUSE (Defect A, 9db239f + 51416aa deadline_pending->success):
ci_event_wait exited at deadline without releasing the daemon lease.
The orphaned lease blocked the daemon from writing resume tokens to
the FIFO for re-registered waits — so completion events that WERE
persisted (10:43:01 for 9db239f, 12:01:49 for 51416aa) could never be
delivered to a later waiter. Additionally: internal re-arms were
timer-blind (no durable-event recheck), and _fetch_runs used a single
50-run page (scheduled merge-robot floods push older SHAs past it).
FIXES:
- lease release in run() finally block (orphan prevention)
- durable-event recheck on every internal re-arm
- _fetch_runs paginates up to 4 pages, collecting all matches
- deploy_qualify --sha flag (explicit sha, env masked)
- model sha = hint; stored target wins on mismatch
- behavioral tests: terminal-before-deadline, nonterminal-never-returned
Defect A root cause: Gitea runs-list page flood (scheduled merge-robot
runs push relevant SHAs past the limit=50 first page) + collect-only-
first-page pagination -> terminal commit read as pending -> parked until
deadline. FIX: _fetch_runs paginates up to 4 pages collecting all matches
before verdict.
Defect B root cause: authority_verdict spawned deploy_qualify reading
DEPLOY_SHA from ambient env. FIX: explicit --sha arg (env masked when
sha is explicit); _ci_runs_verdict/op_ci_alive take sha as parameter
only; no env read remains in the authority path.
Defect C root cause: model sha treated as lookup key — a typo derived a
ghost operation. FIX: model sha is a HINT; unresolved sha + single
pending op -> stored target used with model_sha_mismatch logged; bound
op always wins over hints. deploy_qualify gained --sha flag (env kept
for workflow backward-compat only).
Tests: H-matrix (identity, prefix collision, env staleness,
monotonicity, aggregator lag, operation-id-first) + full 2179 suite.
Gitea's combined commit-status endpoint can 404 for the newest commit
(observed live on auto-version bump commits: status object materializes
late). When deploy_qualify returns pending AND the status endpoint is
unreadable, fall back to the operation-scoped Actions-runs verdict (same
evidence the two legs read). Also export DEPLOY_SHA in run() for python
callers. Found via E10 fresh-process proof.
- PendingOperation = durable continuation object: continuation_state
(ACTIVE_REASONING/WAITING_EXTERNAL/RESUMABLE/RECOVERY_REQUIRED),
canonical op_type/authority, task_id — parked cognition is a first-class
durable state a fresh process can read
- ci_event_wait stamps cognitive-state transitions (WAITING_EXTERNAL on
park, RESUMABLE/RECOVERY_REQUIRED on terminal)
- DEPLOY_SHA export fix: CLI/python run() consumers previously got a
silent 'pending' authority verdict forever — the authority subprocess
never received the sha (found via E10 fresh-process proof)
- await_intent_cli route-bash: Bash commands expressing external wait
intent compile to the canonical continuation (new bash.ts harness tool
routes through it); plain local sleeps execute normally
- tests/test_await_intent.py: E-matrix (derivation truth table,
provenance order, observability)
MODEL OUTPUT = INTENT; RUNE CONTROLLER = EXECUTION SEMANTICS.
scripts/await_intent.py: deterministic resolver that derives canonical
op_type from stored normalized records / open PRs / git ancestry — model
supplied op_type and deadline_secs are recorded but NEVER used as policy
(git/Gitea truth outranks even legacy wait records). Sleep/check/poll
intent aimed at a known pending operation compiles to
ContinuePendingOperation; plain legitimate sleeps are untouched.
Timing provenance: USER_EXPLICIT/EXTERNAL_CONTRACT authoritative,
MODEL_INVENTED discarded. Ambiguity fails safe with structured
candidates. await_intent_cli.py = tool surface; compact JSONL
observability per normalization. await-operation.ts rewired to normalize
before dispatch (needs commit of harness tool separately).
The await_operation first-class tool (and any local deploy_qualify.py
caller) never exported GITEA_SERVER/GITEA_REPO, so main() printed
'pending' unconditionally — the first-class wait could never resolve
success even against a green, converged deployment. CI behavior is
unchanged (env always exported there); the fallback only fires when
the caller omitted the vars. Root-caused live in T21.4 G10 dogfood.
The push-convergence path synthesizes PR titles as '<kind>: converged
release' where kind comes from version_convergence.highest_warranted_bump
(minor|patch). semver_bump then re-derived the kind from the TITLE TEXT
and 'minor:' matched nothing → patch. Every feature tranche downgraded:
T18/T19/T20 feat ranges bumped 0.66.77-83 instead of 0.67.0. Fixed:
MINOR_PREFIX_RE carries the convergence decision through. Held-out
proves minor-title→0.67.0 and patch-title→0.67.1 (no accidental
re-minor). This is the 0.66.x problem the mandate named, root-caused.
- event_wait.find_matching_event(): replays matching completion events
from the durable bridge event log at registration. Root-caused from
TWO production cycles: on a serialized single-runner machine the CI
wake always lands before/around deploy registration — a forward-only
FIFO rendezvous can never deliver it (both cycles fell back to the
900s deadline, truthfully). The event log exists precisely for this;
the wake is still never the verdict.
- ci_event_wait: event_replayed mode short-circuits to the authority
read; 2 new held-outs (9 total).
- drift_resume.py (16P): claim-vs-observation reconciliation with
EXPECTED/BENIGN/MATERIAL/CONFLICTING/UNKNOWN classes; MATERIAL/
CONFLICTING/UNKNOWN block dependent work; user-locked claims escalate
to CONFLICTING (reality never silently overrides user-locked
authority). Held-outs driven by the REAL 16O fixture: the VAT
regression blocks resume; the checkpoint commit resumes clean.
- 16O benchmark persisted (store/evidence/t16/16O_benchmark.md): honest
result — on a tiny fixture cold and scaffold-assisted re-entry cost
about the same (8 vs 9 files); the scaffold's advantage is drift
classification, recorded next actions, and intent, not raw step
count. No fabricated win.
Suite 1884 green; ratchets PASS.
- provider_context.py: require_model() resolves the model from the
ambient provider context; missing context fails LOUDLY
(PROVIDER_UNRESOLVED, exit 3) — no silent default provider anywhere.
- t14/t15 builder drivers, evidence_probe, research executor/relay:
hardcoded 'Verboo Code/glm-5.3-flash' defaults REMOVED (6+ sites);
provider/model selection now belongs exclusively to the provider
layer via the ambient context. No caller anywhere set MIMO_MODEL —
every run silently used the code-chosen provider until now.
- failure_recovery: PROVIDER_UNAVAILABLE class (context-forced; real
status/API code preserved verbatim in evidence; policy
block_branch_preserve_error, 0 retries, no fallback — 16G-D).
- tests/test_provider_ownership.py: structural guard (provider names
in Scaffolding code lines fail the suite; module list live-discovered)
+ 16G held-outs (verbatim codes, zero fallback attempts, resumable
after block, provider preference never mutates scaffold truth).
- 16U root-cause completion: pyright skew 117⇄124 driven by ambient
VIRTUAL_ENV/PATH leakage into pyright's interpreter resolution —
the ratchet now neutralizes ambient-venv signals (PYTHONNOUSERSITE
was necessary but not sufficient; reproduced and closed).
- spine reconciliation: T15 modules (event_wait, stage_guards,
failure_recovery, programme_continuation) were missing belongs_to
relationships in the tracked store while the generated manifest had
them — registered with SCAFFOLD-TRANCHE-015 evidence; provider_context
registered under capability; manifest regenerated via export-umbrellas.
Suite 1869 green; ratchets PASS; Henxels rc=0.
- deploy.yml sleep-30 polling loop REPLACED: registers a gitea-identity
PROCESS_EVENT wait, yields in select() on the FIFO, resumes on the
ci.yml notify-event-bridge WAKE, then re-reads the canonical Gitea
verdict (deploy_qualify.py). Event payload is never completion truth.
- ci.yml gains notify-event-bridge (always(), best-effort): a red CI
wakes the waiter as loudly as a green one; bridge unreachable ->
watchdog recovery covers the lost wake.
- event_wait.py: register_process_wait gains provider/event_type/sha
identity (16E: ONE bridge primitive; builder + gitea are adapters);
open_fifo_reader + caller-owned fifo_fd close the 16Y#10 race (event
before waiter in select()); await_resume_event returns eof instead of
busy-looping on a caller-owned fd.
- ci_event_wait.py watchdog: failure DETECTOR only — liveness-gated
re-arm, one-shot authority recovery on lost events, truthful failure
when no verdict exists; timestamps emitted for the 16D latency proof.
- tests: 8 held-outs on the REAL daemon (shared wr_daemon fixture in
conftest — no duplicated helpers). Daemon-side: /process-event passes
through provider/event_type/sha (defaults preserve T15); lease_path
sanitizes slash-bearing session ids (resume delivery was impossible
for them — found live, fixed in daemon snapshot 79434e0).
Suite 1861 green; ratchets PASS; Henxels rc=0.
ConstructionInterface.discover_active_contract: ONE canonical home for
fresh-builder resume discovery (per-project scaffolds + main store;
most-recent contract; pure readback). T14/T15 driver helpers are now
shims. capabilities() advertises the op. Held-out: empty store invents
nothing; seeded store discovers the right contract.
15S: breaking-change lifecycle held-out — old API + consumer governed
before change; verifying→migrating→superseding enforced by the phase
map; the old path survives as governed history (supersession is
lifecycle state, not erasure); BUILD NEW never destroys before migration.
15T: src/rune/failure_recovery.py — deterministic classification
(TRANSIENT/INTERMITTENT/KNOWN_PROCESS_DEFECT/PERMANENT/APPROVAL_
REQUIRED/UNKNOWN) with explicit retry budgets; PERMANENT/APPROVAL
never auto-retry; no infinite loops possible. 2 held-outs.
scripts/t15_builder_session.py: real model builder (mimo run/Verboo) under
the event notifier; driver YIELDS on the bridge FIFO (no polling on the
normal path); readiness canary before launch; exit canaries + 15G test
truth after; evidence-gated closure.
Live findings (recorded honestly):
- real model builds run 10-65 min — the 900s watchdog misfired twice as a
false PROCESS_LOST; root cause: (a) deadline < real build time, (b)
node builders re-group/detach children so pgid AND cmdline liveness
scans can miss the work. Repair: await_resume_event re-arms bounded on
liveness; the driver consults CANONICAL WAITSTORE STATE after watchdog
expiry — a recorded verdict means completion, process-table absence is
not truth (§15E canonical-state-wins).
- an orphaned T14-era builder process (old .mjs-scope prompt, quota-stuck)
was discovered polluting process checks — killed and recorded.
Result: contract closed from evidence; 10/10 node tests; both builder
exit events processed by the bridge (verdict success both runs).
Evidence: store/evidence/t15/.
15N: capabilities() now exposes full argument schemas (types, required
fields, per-item shape, notes), lifecycle phase map + legal transitions,
and scope-amendment authority — a fresh builder DISCOVERS the
report_observation contract instead of trial-and-error (the T14
dict-vs-list + missing-entity mistakes become impossible). Held-out:
schema-guided first report succeeds; schema-violating payload rejected.
15O: audit_scope is PROVENANCE-AWARE — residue paths present before the
builder ran (or explicitly runtime-declared) stay exempt and recorded;
NEW residue-looking paths appearing mid-session (.mimocode/evil,
.git/config) are SCOPE VIOLATIONS. The hostile lookalike can no longer
hide behind the T14 prefix rule.
15P: PROJECT_BUILD bootstrap twice = one project identity (no
duplication); event replay after restart = readback-only recovery
(idempotent by construction, contract-inspected).
Root cause (proven by controlled experiment): pyright resolves the python
environment for import analysis; a developer's user-site packages
(~/.local/lib/python3.14/site-packages) leak into resolution. Same
checkout, same lock-pinned venv: nick 124/429, runner user 117/437.
PYTHONNOUSERSITE=1 pins both contexts to identical arithmetic (verified:
exact 117-error match both sides).
Fix: type_check_ratchet.py runs pyright with PYTHONNOUSERSITE=1 — local
preflight now PREDICTS CI. Residual 437-vs-443 warning delta noted
honestly (warnings are capped, not matched); error arithmetic identical.
'CI is the gate' was not the answer; the environments now agree.
t14_builder_session.py: real builder session driver.
- 14D: 'mimo run' (Verboo Code/glm-5.3-flash, PL1 provider policy default;
primary Xioami/mimo-v2.5-pro endpoint quota-exhausted 429) as a SEPARATE
OS process consumes the Construction Interface projection (scope,
obligations, phase, verification requirement, next action) and implements
a JS practice scheduler. Builder files: package.json, scheduler.js
(validated createPracticeItem/listItems/completeItem), scheduler.test.js
(11 node:test cases). Scope audit + independent node --test by the
driver; closure from evidence only (§14X.8).
- 14E: real SIGKILL to the builder process mid-session; fresh builder
process resumed from canonical state only (no transcript): recovered
project/contract/phase/scope, inspected-not-overwrote, zero duplication,
verification still required, evidence-gated closure.
- Runtime residue (.mimocode session scaffolding) classified and recorded
explicitly — not silently whitelisted, not a builder scope violation.
- Requirement spec governed as contract on project surface; Need references
it (REUSE of specification); BUILD_NEW silent-choice gate respected.
Evidence logs: store/evidence/t14/.
14B: the out-of-lock publisher (publish_out_of_lock) no longer trusts the
originating job's SHA. It re-reads CURRENT deployment authority immediately
before publishing and constructs the note from the authority's identity.
Adversarial race held-out: A promotes → B supersedes → B publishes → A
resumes → final history says B, A's SHA never enters. Unreadable authority
→ no publication (never fabricate an identity).
Body parsers (summary_from_body / sha_from_body) moved to release_notes.py
as the single authority; release_history_cli.py delegates.
14C: .convergence-intent.json ELIMINATED (1 writer, 0 readers, non-atomic
write, bogus recorded_at). Pending convergence is derived from the
deployment authority itself (atomic symlink + build identity): restart-
readable, corruption-immune, monotonic, idempotent by construction.
T11 stale-refusal invariant preserved (CLI revalidation unchanged).
§12C design insight from the 0.66.51 deploy (run 7721): the workflow-step
refresh fired (steps.promotion_truth gate worked) but the authority
readback disagreed — an overlapping deploy had superseded the candidate
between promotion-check and refresh-step. Correct refusal, but the WINNER
never converges because its own refresh can hit the same race.
DEPLOY_PROMOTION.PROMOTE now accepts on_promoted: invoked INSIDE the flock
critical section immediately after the symlink swap — only the promotion
WINNER converges, atomically with the swap. Failure degrades to STALE
(software valid, history lags, retry converges). deploy_stage passes a
convergence hook that builds evidence-derived notes and publishes to Gitea.
Tests: 4 new (hook fires on promotion, NOT fired for stale, failure
degrades not crashes, A→B race winner-converges-inside-lock); suite 1687
green; race tests all green.
The deploy workflow's promotion gate (steps.promotion_truth.outputs) is
necessary but not sufficient: two overlapping deploys can both observe
promoted=true at their own moments, and the STALE one's refresh could
overwrite the newer converged identity.
release_history_cli.py refresh now REVALIDATES against the deployment
authority at refresh time: if current's build identity differs from the
candidate, the stale refresh is REFUSED (T11 invariant preserved) and the
WINNING deploy's refresh converges the release to the final truth.
12C test plan: clean promotion refresh, already-current idempotence,
A→B race final-B convergence, candidate/authority disagreement refusal,
no-duplicate, missing-release explicit failure, evidence-derived notes.
§1 HELD-OUT CONVERGENCE DEFECT confirmed live: v0.66.45 release recorded
deployed ce328a80 while the deployment authority had converged to
e3263ef9/0.66.45 — repaired via the new refresh path; the release now
states BOTH honestly (tag commit ce328a80 · final deployed e3263ef9).
§2 audit answers: NO workflow invoked release_notes.py; release creation
used PR title/body paste (empty '##' bodies on direct pushes); nothing
refreshed after same-version deploys. T9 proved the mechanism, not
automatic adoption — recorded, history not rewritten.
§4 WIRING (one note-generation authority, no YAML logic):
- auto-version.yml: old PR-paste release step REMOVED (§6); replaced by
release_history_cli.py init — evidence-derived notes from the governed
scaffold store, fail-closed on publication failure (§5/§16: Rune cannot
silently create another empty historical release)
- deploy.yml: convergence refresh after successful promotion — same
release PATCHed with the converged deployed identity + newly-governed
facts; visible non-blocking failure (documentation failure never rolls
back valid software; retry converges)
- release_notes.py: auto_initial_note (no hand-written prose, §7) +
convergence_refresh (evidence accumulates, tag SHA never changes,
idempotent) + build_from_deployment
- §11 ledger-only: maintenance:builder-interaction-ledger created;
§16: release-history backlog entry amended (health indicator), no
duplicates
Tests: 10 new (§13 A-L incl. 0.66.45 convergence fixture, idempotence,
failure visibility, retry recovery, no-duplicate guarantee, unsupported-
claim exclusion through the auto path); suite 1620 green.
Observed live on deploy 7449 (SHA 535c2909): a delayed older candidate's
checkout cannot contain commits newer than itself — its local object
store lacks the deployed commit entirely, so the ancestry classification
returned 'divergent' and fail-closed refused. Correct refusal, wrong
reason: the candidate IS an ancestor of the deployed build.
Repair: on divergent classification, fetch origin's canonical branch
(best-effort, credentials already configured in the workflow checkout)
and re-classify. Still divergent afterwards = genuine divergence, refusal
stands. Regression test reproduces the 7449 shape with a depth-1
checkout that genuinely lacks the deployed commit's objects.
Release-path invariant unchanged: unknown/unprovable ordering still
refuses; the fetch only lets the checker SEE the newer reality before
refusing.