ForgeKitFORGEKIT

Build Log

Hardening Human Intervention in the Owner Loop

September 6, 2026

Replaced shape-based Owner Loop review triggers with an evidence-backed consequence classifier, calibrated so ordinary owner requests stop over-triggering human review — then found and fixed a real bug that had silently defeated it.

AlloyMedinaCHFOwner LoopConsequence Classifier
5
consequence-manifest domains (authority, policy, privacy, destructive, recovery)
6
new/edited scripts wiring the classifier into real production consumers
1
live production bug found and fixed by an adversarial audit before deploy
6
new, network-free regression tests passing against the real classifier

Timeline

Start
Received the Charge via Crucible council review
Two rounds of GPT council refinement, then a 13-section finalized Charge document
Recalibration 1
Zeb corrected the calibration target
None of the 6 real historical MedinaCHF candidates should have required review — the classifier was preserving over-intervention, not fixing it
Recalibration 2
unguarded-authority-mutation promoted to a Gate A invariant
Must never depend on a model call as the final safety lock over an already-proven unbounded privilege grant
Build
Wired classifier into derive-and-accept-candidate.mjs and promote-app.mjs
Fail-closed contract: any classifier error routes to human intervention, never silent auto-proceed
Build
Built the Alloy Human Intervention Queue
List page, detail page, Approve/Deny form, dispatched GitHub Actions workflow
Audit
Ran the Adversarial Build Audit Gate
Found a live production bug (empty request id silently defeating the whole classifier), a request-id-derivation ambiguity, and a missing auth check on the new Server Action
Fix + verify
Fixed all three findings, re-typechecked, re-ran the new test suite
Wrote real access-control evidence, committed, pushed, deployed via npm run deploy:alloy

What shipped

forgekit-os/scripts/consequence-manifest.mjs — the consequence manifest classifier (5 domains, Gate A / Gate B)

forgekit-os/scripts/request-resolution.mjs — real OpenAI call comparing a domain claim against the owner's raw request text

forgekit-os/scripts/owner-request-provenance.mjs — fetchOwnerRequestText, reading the resolver DB credential correctly (fixes the original broken email provenance)

forgekit-os/scripts/tests/consequence-manifest.test.mjs — new, real, network-free test suite for the Gate A invariants and fail-closed contract

Wired the classifier into forgekit-os/scripts/derive-and-accept-candidate.mjs and forgekit-os/scripts/promote-app.mjs (both real production consumers)

products/alloy/lib/human-intervention.ts — Alloy data access reading pending-acceptance records directly off disk

products/alloy/app/w/[slug]/interventions/page.tsx and .../[appId]/[candidateSha]/page.tsx — the Human Intervention Queue list + detail pages

products/alloy/components/InterventionDecisionForm.tsx + products/alloy/lib/intervention-actions.ts — Approve/Deny, dispatching .github/workflows/owner-loop-manual-accept.yml

products/medinaCHF/components/coordinator/AdminIntentPanel.tsx — 'Ready, waiting for review' requester-facing state

Fixed (adversarial audit): forgekit-os/scripts/promote-actor-state.sh was passing an empty request id, silently defeating the classifier in production

Fixed (adversarial audit): forgekit-os/scripts/promote-app.mjs's deriveRequestIdFromCommit silently first-matched on ambiguous multi-request commits

Fixed (adversarial audit): products/alloy/lib/intervention-actions.ts had no auth check of its own — added requireWorkspaceContextBySlug via the same pattern every other mutating Alloy Server Action uses

An independently observed unguarded-authority-mutation should be a Gate A invariant rather than depending on request-resolution to return prohibited_regardless... The model should interpret whether owner intent resolves a bounded authority change. It should not be the final safety lock on an already-proven unguarded privilege assignment.

Zeb, recalibration correction mid-session