# `/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.

## 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 the stale `*.Custom.cs` partial fragment, 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. |
| `business.todo-br` | `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` | `grep -rn "// TODO\[UC-"` under same path | Read `<section>/use-case.md` main + alternative flows, write the service body. Retry. |
| `business.todo-screen` | `grep -rn "// TODO\[SCREEN-"` under Controllers/Screens/ | 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/<Module>/Acceptance/ | 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}` | Run `audit-dev-api --mode apply` for that finding. 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` — deep-merge writer adds missing locale entries. Retry. |
| `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. |
| `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. |
| `dotnet.config-mismatch` | EF Core Configuration class missing or wrong | Re-run `scaffold-entity` for the entity (idempotent — respects `// @customised`). Retry. |
| `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: if the URL starts with `/api/screens/` → re-invoke `scaffold-screen-controller` for the entity (refresh the pagespecs). If the URL starts with `/api/v1/integration/` → re-invoke `scaffold-controller`. If the URL canonicalises to `unknown` stratum (orphan template artifact) → re-invoke `scaffold-api-client` (legacy `apiBasePath` got removed; the api-client now derives the URL deterministically). Retry. |
| `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. 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). |
| `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/<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. |

## 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 |
| `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` | Implement what the PRD specifies; skip / stub the gap. | 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 |
| `smoke.boot` | Skip the runtime probe, keep the static wire audit. | medium |
| `scaffolder.missing` | Skip that artifact (never improvise a scaffolder). | high |
| `cli.runtime-error` | Skip that one artifact (never hand-write `.tsx`). | 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

Each retry pushes one entry to `.smartstack/ba/<APP>/<MODULE>/_dev/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/ba/<APP>/<MODULE>/_dev/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.
