# `/ba-develop` — Auto-healing + never-halt protocol

> Loaded on demand when a gate fails. The SKILL.md just mentions "the gate
> enters the auto-healing loop" — the full mechanics live here.

## Default behavior: never halt — auto-heal, else defer the item

The orchestrator **never halts the run and never asks the user mid-flight.**
Every phase wraps its gate in a retry loop. The subagent observes the failure,
classifies it, applies a targeted fix, and re-runs the gate. **Up to 25 retries
per item**, with two additional safety nets so the loop never spins forever.
When a failure is genuinely unhealable, the item is **deferred** — a blocker is
recorded, the safest best-effort action is taken — and the run continues to the
end of the module.

## Hard safety rule — destructive cleanup is FORBIDDEN

A heal/cleanup step may delete ONLY a specific file it can name by full path
(e.g. one stale `<Entity>.Custom.cs`). It must NEVER run `rm -rf`,
`Remove-Item -Recurse`, `git clean`, `git reset --hard`, or delete a DIRECTORY
(especially `src/` or any project folder) to "start clean" or "remove
wrong-location files".

If a scaffold wrote to the wrong place you cannot reach that state silently:
every frontend scaffold HARD-FAILS (`assertWebProjectRoot`) when handed a
.NET/backend or repo root instead of the web app directory. On that error, FIX
the `projectPath` (point it at `<webRoot>` / `findWebProjectFolder()`) and re-run
the scaffold — never `rm` to "fix" it. A broad delete not followed by a successful
regenerate is unrecoverable: this once destroyed a generated backend.

## Per-item retry loop (defers, never halts)

```
attempt = 1
while attempt <= 25:
    runPhase()
    gate = runGate()
    if gate.ok:
        commit()
        return SUCCESS

    failure = classifyFailure(gate.errors)

    if failure.kind in DEFER_KINDS:                       # see table below
        deferItem(blocker(failure)); bestEffort(failure)  # skip / keep / stub safely
        return DEFERRED(failure.kind, attempt)            # ← item deferred, RUN CONTINUES

    if same fingerprint already seen 2 times before:
        deferItem(blocker(kind='infinite-loop')); bestEffort(failure)
        return DEFERRED('infinite-loop', attempt)

    applyFix(failure)                                     # ← the auto-heal
    rememberFingerprint(failure)
    attempt += 1

deferItem(blocker(kind='retry-budget-exhausted')); bestEffort(lastFailure)
return DEFERRED('retry-budget-exhausted', 26)
```

`SUCCESS` and `DEFERRED` are the only two outcomes — there is no `HALT`.
`DEFERRED` records a blocker and lets the orchestrator move on to the next
item / sub-phase / phase. The run as a whole always reaches the end of the
module and reports `blockers[]`.

The **fingerprint** is `hash(failure.kind + sorted(errorPaths) + first 200
chars of stderr)`. Two retries with the same fingerprint = the fix did not
change anything; a third retry would loop forever — defer the item early.

The **retry budget** is per-item (25). It is a loop terminator, NOT a run
terminator: exhausting it defers exactly that one item and the run carries on.

## Failure classification → auto-fix

| Failure kind | Detection | Auto-fix |
|--------------|-----------|----------|
| `compile.missing-using` | `dotnet build` CS0246/CS0234 + namespace in entité.md / DTO inventory | Insert the missing `using <Ns>.<Layer>.<Module>.<X>;` at top of offending file. Retry. |
| `compile.missing-type` | CS0246 with no matching declaration | Identify the missing layer (DTO? Service? Interface?), re-invoke its scaffolder. Retry. |
| `compile.partial-method-mismatch` | CS0759 / CS0762 | Delete ONLY the exact stale partial file named in the error (`<Entity>.Custom.cs`, by full path — never a glob, never a directory), log the removed lines to `heal.log` for manual re-add. Retry. |
| `migration.naming-collision` | `dotnet ef migrations add` "name already exists" | Bump the sequence (`_v1.0.0_002_…` → `_v1.0.0_003_…`) via `cli/lib/migration-name.ts`. Retry. |
| `migration.destructive` | `up.cs` contains `DROP COLUMN`, `DROP TABLE`, `RENAME COLUMN`, or non-nullable col without `defaultValue` | **DEFER** (blocker `migration.destructive`, severity `critical`). Keep the generated migration but **never auto-apply** it (`database update`); embed the `.sql` in the blocker for user review before any apply. Continue. (This scan runs BEFORE the sanctioned apply step — the apply policy has no destructive axis, an ADDITIVE migration is applied autonomously via `skills/efcore/cli/apply`, gates.md After Phase 1.) |
| `business.todo-br` | `audit-dev-api DEV-API-008 err` (deterministic gate) — a surviving `// TODO[BR-…]`, or a `linkedBusinessRules` id with no `RuleFor`/`// BR-…` trace (equiv. `grep -rn "// TODO\[BR-"` under `src/<Ns>.Application`) | Read `règles-métier.md` for BR-NN expression + invalidExamples, implement as `RuleFor(…).Must(…)` on Create+Update validators OR a service guard. Retry. |
| `business.todo-uc` | `audit-dev-api DEV-API-009 err` (deterministic gate) — a `NotImplementedException`/`// TODO[UC-…]` in a service (equiv. `grep -rn "// TODO\[UC-"` under same path) | Read `<section>/use-case.md` main + alternative flows, write the service body. Retry. |
| `data-scope.unenforced` | `audit-dev-api DEV-API-020 err` — an entity whose `rbac.md` plans a scoped read (own/assigned) is missing one of the four legs (policy / `ApplyDataScopeFilter` / DI / ctor `ICurrentUserAccessor`), OR a controller carries the Core-only `[RequireDataScope(typeof(<ext entity>),…)]` | Run the data-scope cascade for the entity: `scaffold-entity` (`dataScope` ownership columns + markers) then `scaffold-data-scope` (policy + `ApplyDataScopeFilter` + DI). For the reversed case, remove the `[RequireDataScope]` attribute (extension scoping is filter-based, not attribute-based). Retry. If still red 2× → **DEFER** (blocker `data-scope.unenforced`, `high`); `userAction`: the DbContext ctor predates the `ICurrentUserAccessor` seam — upgrade it. Continue. |
| `permission.unseeded-constant` | `audit-dev-api DEV-API-021 err` — a `{Mod}Permissions.{Section}.cs` constant absent (verbatim) from the same app's `*CorePermissionsSeedDataProvider.cs`; the platform permission match is EXACT, so the constant can never be granted (role-based users 403) | Route on `params.reason`: `missing-app-segment` (3-segment app-less value — the permissionPrefix fallback bug) → re-run `scaffold-controller` for the entity WITHOUT `permissionPrefix` (the fallback derives `{applicationCode}.{module}.{section}`); `absent-from-seed` / `no-seed-provider` → re-run `scaffold-core-seed` for the app with the module's RBAC. Never hand-edit the constants file or the provider. Retry. |
| `business.todo-screen` | `grep -rn "// TODO\[SCREEN-"` under Controllers/ (screen controllers live at Controllers/<App>/<Module>/<Section>/) | Add the missing method to `I{Entity}Service` + impl in `{Entity}Service`. Reuse Phase 2a Linq primitives. Retry. |
| `acceptance.todo-ac` | `grep -rn "// TODO\[AC-"` under Tests/ (acceptance tests live at Tests/<App>/<Module>/<Section>/) | Read the AC bullet from `<section>/use-case.md`, fill the Fact body using WebApplicationFactory + controller route + DTO from Phase 2. Retry. |
| `audit-dev-api.applicable` | `audit-dev-api --mode audit` emits a finding with `{autoApplyable: true}` | **`audit-dev-api --mode apply` rewrites NOTHING** — it is read-only, and this row invoked it as if it healed. The real remedy is to re-run the finding's `fixSkill` (that is the `scaffold` lane of `/audit-fix`), then re-audit. Retry. |
| `audit-dev-frontend.applicable` | Same shape, frontend audit | Run `audit-dev-frontend --mode apply` for that finding. Retry. |
| `validate-page.import-resolve` | `violations[].rule === 'imports-resolve'` | Re-invoke `scaffold-api-client` for the entity, then re-`scaffold-component`. Retry. |
| `validate-page.i18n-keys-resolve` | `violations[].rule === 'i18n-keys-resolve'` | Re-invoke `scaffold-component` with `priorErrors` — the self-merged emission re-fills the locale entries (all 4 locales, siblings preserved). Retry. |
| `audit-dev-frontend.i18n-catalogue` | `audit-dev-frontend DEV-UI-038 err` — a bare `t()` key missing from a locale bundle, a called key resolving to an OBJECT (label/children collision), or fr/en/it/de key-set drift in a business namespace | Re-invoke `scaffold-component` for the offending entity × view (`params.file`; the emission re-fills all 4 locales in parallel and preserves sibling entities), then `aggregate-component-registry`. Re-run the gate. Still red 2× → **DEFER** (blocker `i18n.catalogue-gap`, `high`); `userAction`: author the missing locale values in the pagespec `i18nKeys` (or `/ba-translate-prd`), or delete the orphan hand-added key. Continue. |
| `validate-page.pagetemplate-wrapping` | `violations[].rule === 'pagetemplate-wrapping'` | Re-invoke `scaffold-component`; emitter always wraps. Retry. |
| `registry.phantom-import` | `aggregate-component-registry` exit 1 "import does not resolve" | Either drop the registration (deferred page) or re-`scaffold-component` (wrong path). Retry. |
| `registry.componentkey-collision` | `aggregate` exit 1 "componentKey collision" | **DEFER** (blocker `registry.componentkey-collision`, `high`). Keep the first registration, skip the colliding one, finish 3c. `userAction`: two BAs claim `{app}.{module}.{section}` — pick one. Continue. |
| `registry.legacy-monolith` | `aggregate-component-registry` / `scaffold-routes` exit 1 "registry.legacy-monolith"/"registry.mixed-layout", or `audit-dev-pwa` DEV-PWA-011 err | **DEFER** (blocker `registry.legacy-monolith`, `high`). NEVER re-run `scaffold-routes`/`aggregate-component-registry` on this layout — re-aggregating would blank the app. `userAction`: run `split-component-registry` (frontend-routes skill, routing-identical), then optionally the canonical regeneration flow ("Migrating a legacy monolithic registry"). Continue. |
| `build.vite.broken-import` | `npm run build` "Could not resolve `@/…`" | Re-aggregate the registry (Phase 3c) — phantom slipped past. Retry. |
| `tests.business.fail` | `dotnet test Category=Business` fail with stub still in place | Treat as `business.todo-br`. Retry. |
| `tests.business.fail-real` | Same, but no stub; the rule is implemented and still rejects a valid example | **DEFER** (blocker `tests.business.disagreement`, `high`). Keep the implemented rule — never weaken the test — and leave it red (the red test IS the signal). `userAction`: BA `validExamples` and impl disagree; reconcile `règles-métier.md` or the rule. Continue. |
| `tests.acceptance.fail` | `dotnet test Category=Acceptance` fail | Stub still in place → auto-fix as `acceptance.todo-ac`. No stub → **DEFER** (blocker `tests.acceptance.disagreement`, `high`); leave the test red (it is the signal), continue. |
| `test-data.incoherent` | `derive-test-data --mode derive` (Phase 1, § « Jeu de test ») reports `totals.err > 0` — a `jeu-de-test.md` cell the model, the actors or the cited module's dataset cannot resolve (`fk-unresolved` names the OWNER module) | **DEFER** (blocker `test-data.incoherent`, `high`) — the dataset is OPTIONAL: skip `testData[]` for this module, seed the reference data only, continue. `userAction`: fix `jeu-de-test.md` (or the owner module's) via `/ba-create-test-data`, re-run Phase 1. Never a halt. |
| `seed.create-signature` | `dotnet build` CS1739 / CS7036 / CS1503 whose file is a `*SeedDataProvider.cs` — the flat named-args `Entity.Create(…)` call the seed emits does not match the entity's factory (an entity generated before the Create-honnête contract, a hand-edited factory, a lifecycle-PHASED column passed as an argument, an enum value that is not the generated member) | Route on the argument: a phased column → drop it from the dataset row (never a Create parameter; the capturing action sets it) and re-run `scaffold-seed`; an enum member → pass the generated member via `types.<prop>` / `cs:<Type>.<Member>`; a factory that simply differs → **DEFER** (blocker `test-data.create-mismatch`, `high`), DELETE the named `{Module}TestDataSeedDataProvider.cs` (a specific file, never a directory) so the build goes green, continue. `userAction`: regenerate the entity (`scaffold-entity`, idempotent) or align the factory, then re-run Phase 1. Reference data (`referenceData[]`) follows the same route with `reference-data.create-mismatch`. |
| `dotnet.config-mismatch` | EF Core Configuration class missing or wrong | Re-run `scaffold-entity` for the entity (idempotent — respects `// @customised`). Retry. |
| `job.unwired` | `audit-dev-api DEV-API-028 err` (a scheduled UC misses its service method / Program.cs RECURRING-JOBS line / POST jobs/{slug}/run trigger) | Run `derive-job-specs` for the module, splice `report.jobs` as `scheduledJobs[]` into the owning entity's scaffold-business AND scaffold-controller specs, re-run both. Retry. Persistent (the CLI warns `ba.gap` — no emission entity modelled) → **DEFER** (blocker `ba.gap`, `high`); `userAction`: model the emission entity in `entité.md`. |
| `ba.gap-persistent` | Same auto-fix attempted 2× and gate still red with same fingerprint | **DEFER** (blocker `ba.gap`, `high`). Skip / mark-stub the affected item so downstream phases proceed. `userAction`: BA file lacks data (rule expression empty, AC bullet with no entity ref) — edit `règles-métier.md` / `use-case.md`. Continue. |
| `wire.frontend-orphan` | `audit-dev-wire` emits `DEV-WIRE-001 err` with `params.calls` referencing a frontend service URL not matched by any backend route | Read the params + the finding's `fixSkill`: if the URL starts with `/api/screens/` → re-invoke `scaffold-screen-controller` for the entity (refresh the pagespecs). If the URL is on the dead `/api/v1/integration/` literal (fixSkill `frontend-api-client`) → re-invoke `scaffold-api-client` (it derives `API_PATH` from the entity's navRoute via `buildNavApiPath`; the platform rewrote that literal away). If the URL is a plausible `/api/{module}/{section}` the backend never emitted → re-invoke `scaffold-controller`. Retry. **If still orphaned after the re-scaffold retry** — the backend genuinely does not serve it (a phantom api-client for a non-entity dashboard/board section, or an entity whose backend hard-failed) → **DEFER (`wire.frontend-orphan`, high) AND disable/remove the unbacked frontend service** (a specific named file — never a directory) so the app never ships a guaranteed-404 call; the module status becomes `failed` for that entity. NEVER leave the broken call in place, and NEVER hand-write a backend route to satisfy it. |
| `wire.strata-mismatch` | `audit-dev-wire` `params.stratum === 'integration'` AND only `{EntityPlural}ScreenController.cs` exists for that entity | Re-invoke `scaffold-api-client` for this entity with `useScreens: true` AND `screenColumns` freshly extracted from pagespecs. Retry. |
| `wire.verb-mismatch` | Frontend calls VERB X on /api/.../path; backend exposes VERB Y on /api/.../path | **DEFER** (blocker `wire.verb-mismatch`, `high`). Pagespec is the source of truth; leave the call as-is and continue (does not block Phase 4). `userAction`: align the controller or the pagespec on the path. (Future: ACTION-DRIFT-005 will auto-pick once AST-safe-apply lands.) |
| `smoke.4xx` | `run-smoke` returns a 4xx (other than 401/403) on a probed endpoint | Treat as `wire.frontend-orphan` — the route most likely exists but on a different verb. Retry. |
| `smoke.5xx` | `run-smoke` returns a 5xx on a probed endpoint | The route exists but the handler throws. FIRST run `audit-dev-api --rules DEV-API-026`: an err on the entity's `I{E}Service` means the container leg is missing → treat as `di.service-unregistered` (below). Otherwise treat as `business.todo-uc` if a TODO marker is on the matching service method, otherwise **DEFER** (blocker `smoke.5xx`, `high`) — leave it, continue (the real bug surfaces in the report). |
| `di.service-unregistered` | `audit-dev-api DEV-API-026 err` (a controller-injected `I{X}Service` has no AddScoped, or client Handlers/Validators exist without an assembly scan) OR the generated `DiResolutionTests` theory is red for a controller | Re-run `scaffold-business` for the entity: it lands the `AddScoped` line in the `BUSINESS-SERVICES-DI` marker block AND the `CLIENT-APPLICATION-ASSEMBLY-DI` scan idempotently (a hand-written registration outside the block is honoured). Re-run the gate. Retry. |
| `smoke.action-body` | `run-smoke` axis 4b returns a 415 (or a body-required 400) on a custom-action endpoint fired with a SYNTHESIZED VALID body | The action's body/content-type contract is broken (the empty-body/415 class). Treat as `custom-action-missing`: re-run `derive-action-specs` for the entity, then re-`scaffold-controller` (`[FromBody(EmptyBodyBehavior.Allow)] {payloadDto}?`) + re-`scaffold-api-client` (typed payload). Re-run the gate. Retry. Persistent → **DEFER** (blocker `smoke.action-body`, `high`). |
| `smoke.interaction-unavailable` | `run-smoke` axis 4b ran but could not authenticate (no admin token — every custom-action endpoint answered 401/403) | Not a hard failure — the axis surfaces it as a `medium` note. Provide an admin token (e.g. from `/uat provision` → `uat-users.json`) via `adminToken` to actually validate the body contract; otherwise record and continue. |
| `smoke.boot` | `run-smoke` returns `passed: false` with reason `backend boot timeout` / `frontend boot timeout` | **DEFER** (blocker `smoke.boot`, `medium`). Skip the runtime probe, keep the static `audit-dev-wire` result. Infra failure — inspect dotnet/npm logs and re-run smoke manually. Continue. |
| `fk.cross-module-missing` | Phase 3a generated page imports `use<TargetEntity>Lookup` from `@/features/<app>/<targetModule>/...`, but the upstream module hasn't been scaffolded (file missing OR file exists but does not export the lookup hook). | Auto-scaffold upstream: invoke `scaffold-api-client` on the target entity (lookup hook is the primary deliverable), then `scaffold-routes` to wire the upstream URL helpers. Continue Phase 3a. If retry budget exhausts, **DEFER** (blocker `fk.cross-module-missing`, `high`) — best-effort skip/guard the unresolved import so the rest of the page compiles; `userAction`: run /ba-develop on <targetModule> first. Pre-flight Phase 3a.0 catches this proactively; the failure kind covers the case where a pagespec FK gets added mid-flight. |
| `custom-action-missing` | A pagespec `kind:"api"` non-CRUD action did not reach the wire: Phase 2a gate / `DEV-API-010` reports a missing `[HttpVerb]` route, OR Phase 3 gate / `audit-dev-actions-alignment` reports `ACTION-DRIFT-006` (no `use<…><Entity>` hook on any page) or a `001/002/005` URL drift. Root cause is always a SKIPPED derivation/splice — never a mis-mapped field (the projection in `lib/page-spec-actions.ts` owns the mapping). | **Deterministic re-derive**: run `derive-action-specs --spec '{"moduleRoot":…,"entity":…}'`, then splice its output and re-scaffold — `controller`+`business` arrays into `scaffold-controller`/`scaffold-business` (backend miss), or `apiClient` array into `scaffold-api-client` + the WHOLE pageSpec into `scaffold-component` (frontend miss). Re-run the gate. Retry. If still red 2× same fingerprint → **DEFER** (blocker `ba.gap`, `high`): the pagespec action is malformed (the CLI warned-and-skipped it) — `userAction`: fix the `actions[]` entry in `<MODULE>/pagespecs/<Entity>.<view>.md`. Continue. |

## Defer conditions (never abort the run)

When one of these is hit, the item is **deferred**: the orchestrator takes the
best-effort action below, records a blocker (see § Blocker log), and continues.
None of them stops the run or asks the user mid-flight — they all surface in the
final `blockers[]` instead.

| Defer kind | Best-effort action | Severity |
|------------|--------------------|----------|
| `migration.destructive` | Keep the migration, **never auto-apply**; embed `.sql` in the blocker. | critical |
| `migration.not-applied` | Additive migration created but the sanctioned apply CLI returned 🔴 (remote/unknown DB) — keep it, surface the CLI's `reason`; the app may still auto-migrate at boot. | high |
| `migration.incomplete-changeset` | Commit CLI excluded an incomplete migration changeset (`excludedFiles`) — re-scaffold the migration for a complete Migration+Designer+Snapshot triple. | high |
| `tests.business.disagreement` | Keep the rule, leave the `Category=Business` test red. | high |
| `tests.acceptance.disagreement` | Leave the `Category=Acceptance` test red (it is the signal). | high |
| `registry.componentkey-collision` | Keep the first registration, skip the colliding one. | high |
| `wire.verb-mismatch` | Leave the frontend call as-is (pagespec = source of truth). | high |
| `ba.gap` | Skip / mark-stub the affected item so downstream phases proceed. | high |
| `prd.gap` | Skip / stub the gap — **NEVER hand-write the backend "directly from `prd.api.md`"**. A malformed pagespec action (the kind `derive-action-specs` rejects) that blocks `scaffold-controller`/`scaffold-business` **hard-fails that entity**: skip its frontend too (front+back are a coupled pair) and record the blocker. A hand-written backend drifts off the contract the deterministic frontend is generated against → 404 lookups + empty tables. | high |
| `prd.not-dev-ready` | Proceed with the build anyway. | high |
| `fk.cross-module-missing` | Skip/guard the unresolved import; the page still compiles. | high |
| `smoke.5xx` | Leave the throwing handler; record the failing probe. | high |
| `wire.frontend-orphan` (persistent) | Re-scaffold first; if still orphaned, disable/remove the unbacked frontend service (a named file) so the app never ships a guaranteed-404. The entity's module status becomes `failed`. NEVER hand-write a backend route to satisfy it. | high |
| `smoke.boot` | Skip the runtime probe, keep the static wire audit. | medium |
| `scaffolder.missing` | Skip that artifact (never improvise a scaffolder). If it blocks an entity's backend, hard-fail that entity + skip its frontend (coupling). | high |
| `cli.runtime-error` | Skip that one artifact — **never hand-write code (`.tsx` OR `.cs`)**, and **never edit the deployed CLI under `~/.claude/`** (installed copies; the skills-guard hook blocks it). The backend is as scaffolder-owned as the frontend: a hand-written controller/service drifts off the wire contract. If the failing artifact is an entity's backend, hard-fail that entity + skip its frontend (coupling). Before deferring, run the **`/support-report`** protocol with the captured stderr (verify the failure is real, check versions, write the dedup'd report to `.smartstack/support/`) and put the returned `reportDir` in the blocker's `artifacts[]` — a refused report (usage-error/unverified/flaky) defers without one. | high |
| `customised-file-conflict` | **Skip the overwrite** — preserve the user's `// @customised` file; embed the diff in the blocker. | medium |
| `infinite-loop` | Stop retrying this item (fix is broken); skip / stub it. | high |
| `retry-budget-exhausted` | Stop retrying this item after 25 attempts; skip / stub it. | high |
| `git.cannot-commit` | Skip commits (branch is `main` / not a repo / other git error); code still lands on disk. | medium |

## Heal log

Run artefacts live under **`.smartstack/runs/<APP>/<MODULE>/`** — OUTSIDE the
BA tree (`.smartstack/ba/` is the business specification and carries nothing
else; the historical `_dev/` folder inside it mixed two lifetimes and its
convention was propagating). At run start, ensure the project's `.gitignore`
contains a `.smartstack/runs/` line (append it if missing): these files date
from one run and are stale at the next. UAT keeps its own run artefacts under
a DIFFERENT root (`.application-test/uat/<app>/runs/`,
`uat/cli/lib/run-id.ts`) — only the gitignore discipline is shared, not the
location.

**This section is the ONE definition of these paths.** `output-contract.md`
and `commit-checkpoints.md` cite the file names and point here; never restate
the root elsewhere (that restatement-by-analogy is exactly how `_dev/` spread).

Each retry pushes one entry to `.smartstack/runs/<APP>/<MODULE>/heal.log.json`:

```json
{
  "phase": "api.2a",
  "attempt": 4,
  "failureKind": "business.todo-br",
  "fingerprint": "f7a3c2…",
  "fix": "Implemented BR-005 in BudgetService.Validate() from règles-métier.md L42",
  "outcome": "gate-passed"
}
```

The orchestrator's final JSON output includes `healingSummary[]` aggregating
every retry across every phase — the user sees what was auto-fixed without
having to read the log.

## Blocker log

Each deferred item pushes one entry to
`.smartstack/runs/<APP>/<MODULE>/blockers.json`:

```json
{
  "phase": "api.2a",
  "kind": "tests.business.disagreement",
  "severity": "high",
  "summary": "BR-005 rejects a documented valid example",
  "whatWasSkipped": "left the Category=Business test for BR-005 red",
  "bestEffortTaken": "kept the implemented rule (did not weaken the test)",
  "userAction": "reconcile règles-métier.md BR-005 validExamples with the rule",
  "artifacts": ["src/…/BudgetService.cs:42", "Tests/…/BudgetRuleTests.cs:88"]
}
```

`severity` is `critical` (data-loss — only `migration.destructive`), `high`
(needs a human decision before the feature is correct), or `medium` (infra /
environment / cosmetic). The orchestrator's final JSON output includes
`blockers[]` aggregating every deferred item across every phase, and the
`overallStatus` becomes `completed-with-blockers` whenever `blockers[]` is
non-empty (see `references/output-contract.md`). This is the ONLY channel for
surfacing work the user must do — never a mid-run interaction.

## What this changes vs. the prior behavior

**Before Wave G**: any gate failure halted → user fixes → user re-runs. Slow,
manual, broke flow.

**Wave G**: 80%+ of typical failures (TODO markers, missing methods, import
resolution, audit-applyable findings, i18n missing keys, migration name
collision) are healed in 1-3 retries, transparently — but genuinely structural
failures still HARD HALTED and asked the user (regenerate the PRD, edit a BA
file, review a migration). That broke the autonomy contract.

**Now (never-halt protocol)**: nothing hard-halts. Healable failures are healed
as before; genuinely structural failures are **deferred** — the safest
best-effort action is taken, a blocker is recorded, and the run continues to the
end of the module. The user is never interrupted mid-run; everything that needs
a human decision waits in `blockers[]` until the final report. **The run always
completes — `halted` is not a reachable outcome.**

## Anti-patterns specific to the protocol

- **Halting the run, or asking the user mid-flight** (regenerate the PRD, edit a
  BA file, pick a key, review a migration). There is no hard-halt list any more:
  every unhealable failure is a deferred item + a blocker. The orchestrator must
  reach the end of the module unconditionally.
- **Increasing the retry budget beyond 25** to "force it through". The 25-retry
  ceiling exists so a genuinely incorrigible item is deferred (not spun on
  forever). Cranking it to 100 masks a real problem before finally deferring.
- **Swallowing a deferred item silently.** A defer is not a success — it MUST
  push a blocker so the user sees it in the final report. Best-effort ≠ done.
- **Hand-writing the backend** (a controller, DTO, Command/Query, or the
  `{Entity}Service` skeleton) "directly from `prd.api.md`" when a scaffolder is
  blocked. The backend is as scaffolder-owned as the frontend (see `SKILL.md`
  § ABSOLUTE RULE — the backend is scaffolder-owned). A hand-written controller
  drops the `/lookup` route and the paginated list, drifting off the contract
  the deterministic frontend is generated against → 404 lookups + empty tables.
  The ONLY backend code the subagent may author is the body of a generated
  `// TODO[BR-…]` / `// TODO[UC-…]` marker inside a service method. If a
  scaffolder cannot run, hard-fail the entity (skip its frontend — coupling),
  record the blocker, and continue — never improvise the backend.
