Build Log
Foreman owns recovery; Zeb handles exceptions
October 2, 2026
Foreman now budgets recovery by actions it actually ran, routes every stuck reason by Zeb's named split, writes every handoff onto the request itself, and deploys verified bytes from one isolated checkout - proven live on canvas-2 and on a real canvas-1 owner deploy.
What shipped
forgekit-os/scripts/lib/promotion-blocked-reasons.mjs - the one classification of stuck-promotion reasons by Zeb's split (auto / agent / zeb / unknown), data-driven, byte-identical in all four canvas apps and drift-tested
forgekit-os/scripts/foreman/recovery-ledger.mjs + foreman_recovery_attempts (Foreman schema v2, provisioned and verified live on canvas-1..4) - budgets count EXECUTED actions, per (request, execution, recipe) plus a per-request rebuild cap; due-first fair follow-up
forgekit-os/scripts/foreman/recovery-facts.mjs - three-valued facts (candidate on master via GitHub compare API; retry columns; active pause)
forgekit-os/scripts/foreman/recovery-adapters.mjs - fixed recipes with preconditions actually evaluated; thenRun deleted; only open (needs_review) requests are ever recovered
forgekit-os/scripts/foreman/recovery.mjs - compare-and-set on every request write, handOffToPerson written on the request at decision time, freshDispatchRequest rebuild, exhaustion preserving the original reason
forgekit-os/scripts/foreman/authority.mjs - ACTION_AUTHORIZED_APPS plus RECOVERY_LIVE_APPS staged-rollout gate (canvas-2 first, then all four)
forgekit-os/scripts/dispatch-owner-loop-requests.mjs - fetch_failed joins the bounded promotion retry
forgekit-os/scripts/unblock-after-manual-promotion.mjs, fix-promotion-blocked-desync.mjs - writes now compare-and-set on the inspected state
forgekit-os/scripts/record-promotion-outcome.mjs - no immediate email for reasons Foreman owns where recovery is live
forgekit-os/scripts/foreman/rules/promotion-blocked-unrecovered.mjs - recheckResource id bug fixed; routing and severity from the shared classification
forgekit-os/scripts/foreman/notify.mjs - escalations carry the decision needed; human_notified_at stamped only on confirmed delivery
experiments/canvas-1..4/lib/promotion-blocked-class.ts + request-display.ts (+ canvas-1 RequestPanel.tsx, canvas-1/2 owner-loop-stage.ts) - owners see only on it / alerted / needs someone; none asks the owner to act
forgekit-os/scripts/lib/isolated-checkout.mjs - one detached worktree of the approved revision for verification AND packaging, link-aware dirtiness
forgekit-os/scripts/promote-app.mjs - Actor deploy path verifies and packages from the isolated checkout; upload digest; production host must positively serve the new deployment
forgekit-os/scripts/deploy-with-bundled-deps.mjs - manual deploy path rebuilt the same way; stamps uploadDigest and sourceTreeId
forgekit-os/scripts/provision-foreman-tables.mjs - ledger DDL and grants; skips index creation on tables it does not own
forgekit-os/scripts/foreman/acceptance/{probe-stuck-requests,find-acceptance-subject}.mjs + acceptance-probe expect-interrupted-after-publication / authenticity check; acceptance workflow gains after_publication mode and the GH token it was missing
Tests: promotion-blocked-reasons (11), recovery-bounded-retry (26), recovery-adapters (29), recovery-acceptance (10), promote-isolated-checkout (10), deploy-upload-binding (6), rules-fixtures (25); test:os exit 0 on frozen 692ed0b6
“Notification timing must not determine who supposedly owns the work.”