Build Log · September 22, 2026

The Approval Path Was Never the Approval Path

A stuck MedinaCHF request led to finding the Owner Loop Approve button couldn't complete a fresh deploy, a queue showing five stale items, and a Foreman findings dashboard that was 97% noise — all four traced to the same root shape and fixed at the source.

  • Alloy
  • Owner Loop
  • Foreman
  • MedinaCHF
  • Reliability
commits shipped
10

commits shipped

new tests, all green
75+

new tests, all green

high-severity Foreman findings (projected)
134→9

high-severity Foreman findings (projected)

stale Interventions queue items
5→0

stale Interventions queue items

Timeline

  1. Start

    Fix the most recent stuck MedinaCHF request

    Chelsea Tinter's photo-upload black-screen bug, request 1acb742e

  2. ~1hr

    Manual recovery, live

    base_diverged, missing publication receipt, three chained workflows — resolved, but slow and with a real stop-instruction violation along the way

  3. ~1.5hr

    Charge written, reviewed twice, rewritten

    First draft automated the recovery sequence — correctly rejected as fixing the wrong thing. Rewritten around the real root cause: the Approve button structurally could not deploy.

  4. ~4hr

    Approval path rebuilt

    Credentials-before-script, durable decision-first, resumable, evidence chain, concurrency claim, honest UI, honest diagnostics, notification precision

  5. ~5hr

    Two more real bugs found from user reports

    Foreman showing medinachf 'disabled' when it was the busiest app; Interventions queue showing 5 stale items

  6. ~6hr

    134 open Foreman findings called out as noise

    Traced to a rule treating one legitimate end-state as the only correct one, and an auto-resolve path that had never once been wired in

  7. Close

    Severity-refresh bug found via direct DB check

    Log said success; the data said otherwise. Caught before declaring done.

What shipped

  • Fixed the Owner Loop Approve button's actual failure: owner-loop-manual-accept.yml wrote its DB credential two steps after the script that needed it, and never wrote a Vercel token at all — the accept path could not deploy, structurally, for any app.

  • Human decision now persists and pushes to master BEFORE any failable work runs (--record-decision-only) — a promotion/publish/finalize failure can no longer discard the approval itself.

  • accept-pending-candidate.mjs is now resumable from the last incomplete stage instead of blanket-refusing any non-fresh record; a REJECTED decision is hard-guarded out of the resume-to-deploy path.

  • base_diverged handled in-band: merges the candidate onto current master, runs the app's real tsc/predeploy checks against the INTEGRATED result (a clean merge is not verification), never force-pushes, stops for a human only on a genuine conflict.

  • Fixed a real exit-code bug (D5): the old expression reported success for a run that promoted code, failed to publish, and skipped finalize entirely — a stranded request exiting 0. Reproduced the old bug empirically before shipping the fix.

  • Evidence chain now records approved_candidate_sha, integrated_revision_sha, deployment_id, and publication_evidence_sha as four separate fields, so 'is what shipped what the human approved' stays answerable.

  • Concurrency: owner-loop-manual-accept.yml now serializes per app+candidate (same primitive as actor-spawn.yml's own live-verified concurrency group) so two simultaneous Approve clicks converge on one operation.

  • Alloy's Approve/Deny UI no longer reports 'Approved' on a bare HTTP 204 — states plainly that the run started, not that it finished, and links to the real run.

  • New classify-request-state.mjs + inspect-admin-intent.mjs --stuck-only: a stuck-request diagnostic whose honesty is enforced by tests, not prose — non-terminal is not stuck, block_reason=execution_failure is never reported as 'the Actor failed', ready-to-finalize requires real publication evidence.

  • Notification precision: intermediate refusals that are now handled in-band produce zero emails; a genuinely unfinished approved request produces exactly one actionable notice, naming what's incomplete.

  • reconcile-pending-acceptance.mjs: resolves a pending-acceptance record ONLY on proof (its candidate is an ancestor of master, or a sibling candidate for the same request shipped instead) — cleared all 5 stale Interventions queue items, two of which had sat since 2026-08-31 carrying a recorded approval that was never advanced.

  • Fixed Foreman's medinachf display: it wasn't disabled, it was the busiest app (141 records, 61 findings) — Alloy's readonly role for medinachf was actually named after the wrong Postgres role (owner_loop_resolver, not medinachf_readonly), so its own grant never reached the role that connects.

  • Rewrote projection_divergence (Foreman rule) to recognize 'deployed' and 'terminal' as equivalent legitimate end states instead of flagging every successfully Actor-deployed request as drift forever, and to grade severity by what actually diverged (contradiction=high, never-backfilled=low) instead of hardcoding high for everything.

  • Wired resolveIfInvariantRestored into the sweep for the first time ever — it existed, fully written, with zero callers, so a finding that stopped firing stayed open permanently. Scoped narrowly: only rules that examine every record with no LIMIT/lookback may be auto-resolved against, proven by a test that reads the real rule source.

  • Fixed a second, independent bug in the same area: recordFinding's upsert never refreshed severity or rule_version, so a rule fix could run correctly and still never reach the dashboard. Found by checking the database directly rather than trusting the sweep's own success log.