Skip to content

feat(repair): governed promotion lifecycle with campaigns, canary, and rollback (Section 9) - #265

Merged
abrichr merged 1 commit into
mainfrom
feat/repair-promotion-lifecycle
Jul 26, 2026
Merged

feat(repair): governed promotion lifecycle with campaigns, canary, and rollback (Section 9)#265
abrichr merged 1 commit into
mainfrom
feat/repair-promotion-lifecycle

Conversation

@abrichr

@abrichr abrichr commented Jul 26, 2026

Copy link
Copy Markdown
Member

Implements roadmap Section 9 (Govern repair promotion): a proposed repair (healed or taught bundle) is never implicitly active; it moves through an explicit, auditable, fail-closed lifecycle.

State machine

candidate -> reviewed -> replay_passed -> fault_passed -> approved
          -> staged -> canary -> active

with rejected reachable from every pre-staged state, rolled_back from canary/active, and automatic canary -> staged on any regression. Location: openadapt_flow/repair/lifecycle.py (RepairStore, _ALLOWED transition table). Full docs with mermaid diagram + CLI reference: docs/REPAIR_LIFECYCLE.md.

What this adds

  • RepairCandidate (openadapt_flow/repair/candidate.py): the changed binding as per-step field diffs (values digested, never raw), why the old binding failed (evidence refs by path + hash, privacy-safe failure fingerprints with no raw observations), what supports the new binding (campaign results, approval, canary metrics), and full lineage (prior/proposed whole-bundle content digests, environment contract hash as compatibility scope, transition history). Never auto-activates.
  • Campaign gates (openadapt_flow/repair/campaign.py): the replay campaign reuses the existing deterministic drift battery (openadapt_flow/runtime/healing/perturbation.py) over healthy cases (must locate + verify identity on every case); the fault campaign adds a deterministic adversarial battery (ambiguity, wrong entity, stale target, unexpected dialog, verifier failure) where every case must be REFUSED (acting confidently on any fault frame fails the campaign; an unarmed binding that acts always fails). Results recorded on the candidate; approval refuses until both pass.
  • Invariant enforcement (openadapt_flow/repair/invariants.py): diffs the identity / effect / risk / environment / policy contract fields of the two bundles and hard-refuses (fail closed) any weakening unless the proposed bundle carries an explicit new qualification revision (revision strictly advanced + chained via previous_revision_sha256). Checked at registration AND re-checked at approval against the bundles on disk.
  • Human approval + atomic activation: openadapt-flow repair approve <id> requires a human identity; --non-interactive (automation) requires an explicit --approved-by. The approval binds exact content digests; staging copies BOTH bundles into the store by hash and re-verifies byte-exact (prior too, so rollback is always local); activation is an atomic ACTIVE.json pointer swap (os.replace) after the staged copy re-verifies. One-command rollback: openadapt-flow repair rollback.
  • Canary: bounded first-N runs (--max-runs) with per-run verification (repair canary-record, deriving fail-safe observations from report.json); any silent-incorrect or verification regression immediately reverts the pointer to the prior digest and drops the candidate back to staged; N fully verified runs complete to active.
  • Sync + fingerprints: approved/staged/active states are identified by exact whole-bundle content digests (the same BundleManifest.content_digest the sealing/integrity path uses), so Desktop/runners/Cloud can sync deterministically; the candidate record itself is privacy-safe by construction and digestable (record_sha256()).
  • Hard rule: a model actor can never perform ANY lifecycle transition (assert_not_model_actor, ModelActuationError); review/approve additionally require a human. A model_suggestion-sourced candidate flows through the same human-gated path. Asserted in code and tests.

Integration with today's heal path

The in-run heal governance (openadapt_flow/runtime/healing/: identity-never-weakened RegressionGate, quarantine, perturbation harness) is unchanged and still guards each heal during a run. The two places where a proposed bundle used to become implicitly usable now register a detached repair/candidate.json inside it:

  • Replayer.run(..., save_healed_to=...) (openadapt_flow/runtime/replayer.py)
  • teach() promoted bundles (openadapt_flow/learning/teach.py)

Registration failure fails closed by absence: an unregistered proposed bundle cannot enter the lifecycle at all. The in-run gate protects the run; this lifecycle protects the fleet.

CLI

openadapt-flow repair register | list | show | review | campaign --kind replay|fault | approve | stage | canary | canary-record | rollback | status (reference table in docs/REPAIR_LIFECYCLE.md). The production campaign runner wires the real resolver + OCR band sampler; tests inject deterministic fakes through the same seams.

Tests

tests/test_repair_lifecycle.py (28 tests, all passing): full lifecycle happy path; each gate refuses (failed fault campaign blocks approval, weakened identity/risk/effect contracts hard-refused, weakening admitted only with an explicit new qualification revision, unapproved promotion refused, tampered staged bundle refuses activation, digest drift after registration refuses approval); canary silent-incorrect and verification regressions auto-revert to the prior hash; rollback restores the prior digest; model actors refused on every transition; automation cannot review/approve; replayer end-to-end heal registers a candidate and never auto-activates; privacy assertion (no raw band/OCR text in the candidate record).

Gates

  • uvx ruff@0.15.22 check openadapt_flow and format --check openadapt_flow tests: clean (this also ruff-formats openadapt_flow/transaction.py + tests/test_transaction_outcome.py, which were failing the required format gate on main after feat(runtime): explicit transaction outcome taxonomy + effect journal + idempotency (Section 3) #259).
  • mypy (lenient whole-package): clean; mypy strict-safety flags (including runtime/replayer.py, qualification.py + all new repair modules): clean.
  • Full fast unit suite (pytest --ignore=tests/e2e): passing.
  • scripts/check_consistency.py, scripts/check_bundle_phi.py, scripts/validate_claims.py --check: passing.
  • scripts/check_release_consistency.py + --validate-dist-dir on a fresh wheel/sdist build: license/private boundary passed. paper/check_artifacts.py unaffected (no paper changes).

DO NOT MERGE without review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

…d rollback (Section 9)

Replace immediate repair promotion with an explicit, auditable lifecycle:
candidate -> reviewed -> replay campaign -> fault campaign -> approved ->
staged -> canary -> active.

- RepairCandidate artifact (openadapt_flow/repair/candidate.py): changed
  binding as per-step field diffs (values digested, never raw), failure
  evidence refs (path + hash), privacy-safe failure fingerprints, campaign
  results, approval, canary metrics, full transition history. Never
  auto-activates.
- Campaign gates (repair/campaign.py): replay campaign reuses the existing
  deterministic drift battery (runtime/healing/perturbation.py) over healthy
  cases; fault campaign adds an adversarial battery (ambiguity, wrong
  entity, stale target, unexpected dialog, verifier failure) where every
  case must be REFUSED. Results recorded on the candidate.
- Invariant enforcement (repair/invariants.py): a candidate may never
  weaken identity, effect, risk, environment, or policy requirements; any
  weakening hard-refuses (fail closed) unless the proposed bundle carries
  an explicit new qualification revision.
- Human approval + atomic activation (repair/lifecycle.py): approval
  requires a human identity (CLI: openadapt-flow repair approve, with
  --non-interactive requiring an explicit --approved-by); activation is an
  atomic pointer swap by exact whole-bundle content digest with lineage and
  one-command rollback (openadapt-flow repair rollback).
- Canary: bounded first-N runs with per-run verification; any
  silent-incorrect or verification regression automatically reverts the
  pointer to the prior bundle and drops the candidate back to staged.
- Sync + fingerprints: approved/staged/active states are identified by
  exact content digests; candidate metadata is privacy-safe by construction.
- Hard rule: a model actor can never perform any lifecycle transition
  (assert_not_model_actor); model suggestions flow through the same
  human-gated path. Asserted in code and tests.
- Integration: the replayer's save_healed_to bundle and the teach loop's
  promoted bundle now register a detached repair/candidate.json (the entry
  ticket into the lifecycle; failure fails closed by absence).
- CLI: openadapt-flow repair register/list/show/review/campaign/approve/
  stage/canary/canary-record/rollback/status.
- Docs: docs/REPAIR_LIFECYCLE.md (state machine + CLI reference); README
  pointer.
- Also: ruff-format openadapt_flow/transaction.py and
  tests/test_transaction_outcome.py so the required format gate is green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
@abrichr
abrichr merged commit e6fbeb7 into main Jul 26, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant