---
name: uscha-devloop
description: >
  Spec-driven, multi-repo development + QA orchestrator. Plans from an ADR, builds,
  then runs a severity-gated review loop (code-review / judgment-day / improve) that
  converges instead of looping forever, with tests as a guardrail between passes.
  Auto-triggers characterization tests when JaCoCo line coverage is below threshold.
  Records every step in a deterministic ledger and stops at the merge gate for human
  approval. Invoke for "run the dev loop", "QA this feature", "do the full cycle".
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
disable-model-invocation: false
---

# dev-loop orchestrator

**Who this is for:** a single operator driving one non-trivial or risk-bearing change,
kept honest by a deterministic ledger and a human merge gate. **NOT for trivial or
throwaway work** — a one-file fix or config tweak runs build+test only and skips
discovery/ADR/uscha-sysdoc entirely (see the risk-profile table in the playbook, play 03).

You are running a disciplined development + QA cycle across one or more repositories.
The measurement engine is `./.claude/skills/uscha-devloop/qa_ledger.py` per project, or the installed Codex/Claude global engine (stdlib Python 3).
**All metrics come from the ledger — never estimate counts from memory.** Run every
QA tool through the ledger so the final retrospective is real. The ledger contract has
two tiers: **measured** records (snapshots, ingest-gate, log-gate — parsed from real
artifacts; these can block) and **self-reported** agent counts (log-step — narration
recorded for the retrospective; a measured red always overrides a narrated green).

<!-- uscha:orientation-block:begin -->
<!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->

## First contact (show ONCE, then never again)

**Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
`ACCEPTANCE.md`, no `docs/adr/` -- open with this block, then start working. If any of those
exist, the operator already knows the method: skip it entirely and go straight to the
breadcrumb. Repeating it every run would be exactly the ceremony the method forbids.

```
[uscha · devloop · START]
Method: you bring the idea, the method builds the rest. Facts block, guesses advise;
        nothing closes on a checkbox, and the human approves the merge.
Here:   plan → build → a severity-gated QA loop that converges instead of chasing zero → PR.
Output: code, tests, and QA-LEDGER.json (every gate recorded with its evidence)
Next:   the human gate: review the PR and decide the merge. I stop there.
Stop:   say so at any point -- whatever is already written stays.
```

**Bilingual by construction.** The labels (`START`, `Method`, `Here`, `Output`, `Next`,
`Stop`) stay VERBATIM in English -- they are the method's vocabulary and the smoke suite checks
for them mechanically, which is only possible if they never move. The wording after each label
is the canonical English; **render it in the operator's language**. If they are writing to you
in Spanish, the whole block reads in Spanish under English labels. Do not translate the labels,
do not leave the content in English when they are not writing in English.

Unlike the close block, `Next` here MAY name the nominal route: on a first run there is no
measured state to derive from yet, so the nominal path is the honest answer. From the close
block onward, derived state wins.

## Orientation markers (non-negotiable)

The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
They are navigation, not ceremony: one line per turn, one block at the end.

**No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
below.

**Open every turn with a breadcrumb**, then the content:

`[uscha · devloop · <step> → <target>]`

- `<step>` — `Q<n>` for a question, `pass <n>` for a loop iteration, `step <n>` otherwise.
  Count what has actually happened. **Never write a denominator** (`Q4/12`): this phase
  converges, its length is not known in advance, and an invented total is exactly the kind of
  narrated number the method forbids. **When the ledger already measures the count** (the QA
  loop's `loop_count`), use the measured number — never keep a parallel tally of your own.
- `<target>` — the artifact this turn feeds (`SPEC`, `ADR-003`, `ACCEPTANCE`, `LEDGER`,
  `RECEIVED`, ...). Drop `→ <target>` only when the turn genuinely feeds none.

**Close with the close block ONCE, when the skill finishes** — not on every turn. Ending
without it is a defect, even when the phase converged cleanly:

```
[uscha · devloop · CLOSED]
Produced: <files actually written, or "nothing">
Blocks:   <what stands between here and the next phase, or "nothing">
Next:     <the next action, and why it is that one>
Run:      <the exact command or skill to invoke>
```

This is **not** the implementation handoff some skills also emit: that one is a prompt for
whoever implements next, this one is navigation for the human operator, and both can appear.

`Blocks` and `Next` are **derived from the state you just produced** — never copied from a
fixed route, **including any `Flow:` line in this file**. Those lines are the nominal path;
open ADR experiments, an unclosed spike, an unapproved golden or a red gate all change what
genuinely comes next, and the derived answer wins. If the next phase cannot start yet, name it
and say exactly what unblocks it.

Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`, `Produced`,
`Blocks`, `Next`, `Run`) verbatim — they are the method's vocabulary and the smoke checks them.
<!-- uscha:orientation-block:end -->

## Non-negotiable principles

1. **Converge, don't chase zero.** Block only on findings at or above the severity
   gate (`config.defaults.severity_gate`, default BLOCKER/CRITICAL/HIGH). Everything
   below goes to `ISSUES-DEFERRED.md`, never into the loop. Polishing Medium/Low
   findings forever is the failure mode this skill exists to prevent.
2. **Tests are a guardrail, not a finale.** Run the repo's test command (`mvn test` /
   `flutter test` / `pytest` / `npm test` / `go test` / `cargo test` / `dotnet test` / `ctest` / `./gradlew test` / `swift test`) after every tool pass that changed code. A red suite
   stops the loop.
3. **Generating tests is not running tests.** `/improve test` (writing coverage) runs
   ONCE at the end against stabilized code — never inside the loop. Inside the loop you
   only *run* the existing suite.
4. **Stop at merge.** You create the PR and confirm CI is green. You do NOT merge.
   The human owns the merge gate.
5. **Tracked-markdown protocol: READ the file, never ask for it.** Before modifying any
   tracked `.md` (CLAUDE.md, plan/delta docs, docs/adr), READ the current version out of
   the tree. Those files carry real progress (checkboxes, notes): preserve it and edit in
   place, never regenerate from scratch. The current version of a tracked file is
   INFORMATION the tree already holds, so asking the human for it is noise — you ask the
   human for DECISIONS, never for information you can read (ADR-044).
6. **`origin: agent` — a decision the human never made does not enter scope silently.**
   Every acceptance criterion, ADR decision item or HANDOFF rule that YOU introduce
   (rather than one the human answered) carries the marker on its own line:

   ```
   - [ ] AC-12 — when X then Y. (origin: agent)
   - [ ] AC-12 — when X then Y. (origin: agent, confirmed: 2026-09-07)
   ```

   Unmarked items are human-origin by default; nothing existing is retro-tagged. An
   unconfirmed item is **not in scope**: do not implement it, do not gate on it, do not
   quote it as agreed. Ask the human to confirm it ONE BY ONE — a package-level "ok" over
   a twenty-item summary confirms nothing — and only then write
   `confirmed: YYYY-MM-DD` on the same line. List every still-unconfirmed item in the PR
   body under **Agent-origin, unconfirmed**. `spec-check` names them with file:line and
   `readiness` counts them on their own line; both are ADVISORY and neither changes an
   exit code or the score.
7. **The golden is the one artifact you cannot author.** For migration/legacy work,
   `.approved` fixtures are field truth captured from the ORIGINAL code and approved by
   a HUMAN. You emit `.received` and stop; you never write, edit or rename `.approved`
   (a `PreToolUse` hook denies the write — INV-GOLDEN-01).

## Setup (once per run)

```bash
QL="./.claude/skills/uscha-devloop/qa_ledger.py"                      # instalacion por proyecto
[ -f "$QL" ] || QL="$HOME/.codex/skills/uscha-devloop/qa_ledger.py"   # Codex raw-skills install
[ -f "$QL" ] || QL="$HOME/plugins/uscha/skills/uscha-devloop/qa_ledger.py"  # Codex plugin install
[ -f "$QL" ] || QL="$HOME/.claude/skills/uscha-devloop/qa_ledger.py"  # Claude global install
python3 $QL init --config uscha.config.json

# adding a service LATER never re-runs init: that would build a NEW ledger and drop every
# step. --add-repo appends and re-seals the checksum, leaving every existing repo untouched
# (kit 2.2.0). The new repo has no evidence yet, so it reads UNMEASURED and the repo
# averages read lower until its first snapshot — that is absence, not a regression:
python3 $QL init --add-repo <NAME> --path <path> --type <type> --test-command "<cmd>"
```

## Execution policy line (before every phase)

Before you start a phase, print the one-line routing note from the ledger, then do the
work. This tells the human what the methodology is doing and which model/effort policy is
selected for that phase:

```bash
python3 $QL execution-policy --phase <idea|disc|spec|adr|build|qa|verify|prod>
```

This is **routing metadata only**: it may guide the operator/adapter to use a stronger or
uncorrelated checker, but it never changes readiness and never overrides fact gates. If
`model=default`, keep the current agent model; if a phase declares a model/effort and the
host supports switching, use it for that phase.

### Discovery intake: reality can reopen the contract

Three facts are allowed to push work back into discovery/SPEC instead of being buried as narration:

```bash
python3 $QL production-finding --repo <REPO> --severity HIGH \
  --source <sentry|support|user|prod-log> --title "..." --evidence "..."
python3 $QL spec-doubt --repo <REPO> --kind spec-wrong \
  --note "why the SPEC/AC seems wrong" --evidence "demo/log/file pointer"
python3 $QL spec-change-request --repo <REPO> --source SD-001 \
  --requested-change "what must change in SPEC/ADR" --evidence "demo/log/file pointer"
```

- `production-finding` is for post-merge/prod feedback. It feeds the next discovery/SPEC cycle.
- `spec-doubt` is the builder's escape hatch when the SPEC conflicts with reality. Do **not** code around a doubtful SPEC silently; log it and ask the human.
- `spec-change-request` is the structured bridge from evidence/doubt to a human-approved SPEC/ADR amendment; an open SCR keeps the phase escalated.
- Resolve them only after the feedback was incorporated, explicitly reviewed, or the SPEC/ADR amendment decision was recorded:
  `production-finding --id PF-001 --resolve --note "fed into SPEC"` / `spec-doubt --id SD-001 --resolve --decision "SPEC amended"` / `spec-change-request --id SCR-001 --resolve --decision accepted --amended ACCEPTANCE.md`.

The config lists every repo and its type (maven|flutter|python|node|go|rust|dotnet|cpp|gradle|swift). In a multi-repo session the
other repos must be mounted via `--add-dir` or `additionalDirectories`; the `path`
fields in the config are relative to where you run `init`.

For migration/legacy (risk profile E) work, also wire the golden invariant once:
install `hooks/block-approved-writes.py` as a `PreToolUse` hook in
`settings.json`, and add `*.approved.* binary` to `.gitattributes` (ships in
`templates/.gitattributes`) so line endings can't lie in the byte-compare.

## Phase 0a — Fast-path check (ADR-003; run FIRST, before planning ceremony)

If `defaults.fast_path` exists in config, run the measured classifier before demanding a
full spec package:

```bash
python qa_ledger.py fastpath-eval --repo <name> --json          # dry-run first
```

- **ALLOW** and the operator wants the shortcut: re-run with
  `--intent "<one sentence: what and why>"` to record it, then skip Phase 0's full-package
  demand. The micro-contract replaces it: the recorded INTENT plus at least one new/modified
  asserting test (readiness stays capped until that test shows up in measured evidence).
- **DENY**: state WHICH measured signal denied it — echo the engine's breakdown verbatim.
  The skill wires; it never computes and never argues with the verdict. Proceed with the
  normal full path. The operator may always choose the full path over an ALLOW; nothing —
  operator, agent or flag — can force ALLOW over a DENY (INV-RIGOR-02).
- **Re-evaluate before the PR step** (same command, same intent): thresholds exceeded mid-run
  flip the run to `ESCALATED` — the derived phase blocks pr-ready and readiness is capped.
  Produce the ADR + ACCEPTANCE the change turned out to deserve, then `resolve-escalation`.

## Phase 0 — Plan (ADR-first)

- **Read `CONSTITUTION.md` first (if present).** It lists the project invariants no SPEC
  or ADR may violate. A change that would breach an invariant is a BLOCKER — escalate, do
  not work around it. The CONSTITUTION constrains the whole build.
- The ADR set + `ACCEPTANCE.md` are the input to this loop. They typically come from the
  `uscha-adr-refine` skill (the front-half counterpart): `/uscha-adr-refine` → ADR set → `/uscha-devloop`.
- Confirm or write the ADR + PLAN. The plan must state **acceptance criteria** (the
  `ACCEPTANCE.md` checkboxes) and the **severity gate / coverage threshold** up front.
  The loop targets the plan, not "no issues". If acceptance criteria are missing, stop
  and run `uscha-adr-refine` first (or ask the human).
- Acceptance criteria become the contract tests in Phase 1.

## Phase 1 — Coverage gate → conditional characterization (per repo)

For each repo, decide whether a safety net exists before any refactoring:

```bash
python3 $QL snapshot --repo <REPO> --phase pre
python3 $QL check-coverage --repo <REPO>
# exit 0 = OK, exit 1 = BELOW threshold (a real report), exit 2 = UNMEASURED (no report)
```

- **Coverage >= threshold:** the existing suite is the guardrail. Skip to Phase 2.
- **Coverage < threshold (a real report, exit 1):** write **characterization / contract
  tests at the boundary** (public API, endpoints, input→output behavior) — NOT internals.
  These must survive refactoring. The ADR acceptance criteria are the spec for these.
  **Have the human review these tests before trusting them as a gate** — a test that
  passes for the wrong reason poisons the whole loop. Characterization tests are for
  EXISTING code nobody has tested yet, not for code that has not been run at all.
- **UNMEASURED (exit 2, no report found):** on a greenfield repo this means the test
  command has never been run with coverage — not that the code failed a test. Produce
  the report first: run the repo's test command with coverage, then re-run
  `check-coverage`. Only if a real report then shows coverage below threshold does the
  characterization path above apply.
- **Migration/legacy (profile E): capture the golden BEFORE touching anything.** Run
  the `uscha-characterize` skill (or `uscha-reverse-discovery` for a whole-system map first): it
  executes the ORIGINAL code against a real input corpus, emits `.received` fixtures,
  and STOPS for the human to approve them as `.approved`. No approved golden = no
  migration build. This is the baseline `golden-diff` gates against in Phase 3.

## Phase 2 — Build

Implement per the PLAN. Commit per logical step with conventional commits
(`feat:`, `fix:`, `refactor:`…) so the trail is reviewable and revertible.

### ADR discipline during build

- **Consult before touching governed areas.** Before working on an area covered by an
  accepted ADR, read it and follow its Implementation Plan (affected paths, patterns,
  tests). Also re-check `CONSTITUTION.md`: if the change would breach an invariant, stop
  and escalate — a constitution breach is a BLOCKER and is never resolved silently in code.
  If the code contradicts the ADR, flag it to the human — never resolve the conflict
  silently in code.
- **Proactive ADR triggers — stop and propose an ADR** when you are about to: introduce
  a new dependency, create a new architectural pattern others must follow, choose between
  real alternatives with non-obvious trade-offs, or contradict an accepted ADR. Tell the
  human the decision, why it matters, and ask whether to capture it. If no, leave a short
  `// ADR-not-taken: <why>` comment and move on.
- **Link code ↔ ADR.** When implementing a decision, add one lightweight comment at the
  entry point: `// ADR: <slug> — see docs/adr/ADR-NNN-<slug>.md`. This makes supersede
  safe (you can find all code an ADR governs).
- **Treat `Status: Experiment` as a visible hypothesis, not a license to improvise.** It
  may proceed only when the ADR declares Hypothesis, Feedback Signal, Review By/Trigger,
  Promote Criteria and Rollback/Supersede Criteria. `dashboard --json`/Mirador expose
  malformed or expired experiment metadata as advisory; it is not a readiness score and
  not a hard PR gate by itself.
- **Never edit the SPEC/ADR to make the implementation look correct.** If reality forces
  a change, amend the SPEC (version it) and return to Ready.

## Phase 2b — Simplicity check ("Reduce") — ADVISORY by default (kit 2.1.0)

Before the QA loop, check the change isn't overbuilt. This is the CONSTITUTION's
**Simplicidad** invariant made deterministic — diff minimality, nesting depth and new
abstractions, scored over the diff (not AST cyclomatic complexity; honest proxies):

```bash
git diff --unified=0 <base> | python3 $QL simplicity-check --config uscha.config.json
# or: python3 $QL simplicity-check --from-git --base <base>
```

Reads `SIMPLICITY: NN/100 — SIMPLE | ACCEPTABLE | OVERBUILT  (advisory | declared gate)`.

**Advisory is the default and it exits 0** (ADR-043). Every budget is the KIT'S OPINION until
the project declares its own; an opinion that stops a loop is a gate nobody asked for. In
advisory mode an OVERBUILT verdict is **information for the human**: cut what is cheap to cut,
**report it in the PR body** with the score and the flags, and **never block on it** — do not
loop, do not refuse to converge, do not "fix" the diff to chase the number.

**It gates only when the project says so**: at least one declared budget in
`config.defaults.simplicity` **AND** `defaults.simplicity.gate: true` (or `--gate`). Then
OVERBUILT is exit 1 and a BLOCKER again: reduce first (guard clauses, drop speculative
types/layers, split giant hunks) and re-run. `gate: true` with **no** budget declared is a
config error, exit 2 — a gate with no budget is not a gate.

**`max_nesting` is an INDENTATION-DEPTH proxy, not AST nesting** — it counts leading
indentation on added lines. A wrapped call argument, JSX, or a multi-line Java/Kotlin literal
raises it with no control flow present at all, which is the single most common false OVERBUILT.
Discount it accordingly; the kit does not try to make it language-aware. For a 2-space codebase
pass `--indent-width 2`.

**Tests are OUTSIDE the budget** (kit 1.11.0): test files (the 9 stack conventions) are
counted and reported apart (`test_lines_added`) but never gate — writing tests must not
push a diff toward OVERBUILT (deleting them is already blocked by gate-check). A good
project can have MORE test code than production code.

**Persist the verdict** so convergence and readiness see it (facts block through the
ledger, not through your goodwill) — and persist it as what it WAS:

```bash
# advisory mode (the default): the run is recorded, and it caps nothing and blocks nothing
python3 $QL log-gate --repo <REPO> --iteration <N> --kind simplicity \
  --verdict advisory [--note "OVERBUILT 58/100 — advisory, no budget declared"]

# declared gate only (defaults.simplicity.gate: true + budgets)
python3 $QL log-gate --repo <REPO> --iteration <N> --kind simplicity \
  --verdict <pass|fail> [--note "OVERBUILT: +612 lines vs 400 budget"]
```

**Never log an advisory run as `pass`.** `pass` means a declared gate ran and came back clean;
an advisory run means there was no gate. Readiness prints them apart (`N ok · 1 advisory`) and
the mirador shows `ADVISORY` instead of `OK` — but only if you tell it the truth here.

### Phase 2c — REUSE-FIRST gate (kit 1.26.0)

Simplicity scores the diff in ISOLATION; it cannot see that the new block re-implements
something the repo already has. `waste-check` closes that gap — deterministic Type-1/2
clone detection of the diff **vs the repo** (the muda GitClear calls dominant in AI code):

```bash
python3 $QL waste-check --from-git --base <base> --repo-root . --config uscha.config.json
```

Reads `WASTE: NN/100 — LEAN | ACCEPTABLE | WASTEFUL`. **Advisory by default** (exit 0): the
flags name the existing `file:line` to reuse instead of cloning. It reports a FACT (the block
exists elsewhere) but "wasteful" is a heuristic with false positives (boilerplate, DTOs,
embedded SQL/JSON), so it does NOT block unless the human declares it — `defaults.waste.gate:
true` or `--gate`. When gated and WASTEFUL, reuse/refactor before the QA loop and persist:

```bash
python3 $QL log-gate --repo <REPO> --iteration <N> --kind waste \
  --verdict <pass|fail> [--note "clona util/money.py:40 — reusar"]
```

It runs on **prod code only** (tests excluded, like simplicity) and skips the files the diff
touches (no self-match). Honest scope: Type-1/2 over normalized lines, not semantic clones;
tune `defaults.waste` (`allow_paths` for legit boilerplate, `window_size`, budgets). In risk
profile A (trivial change) skip it. It **collapses into `readiness`** as a `gate:waste` line.

## Phase 3 — QA loop (per repo)

Run the tools in `config.defaults.qa_tools_order` (default: code-review → judgment-day
→ improve). One pass of all tools = one cycle.

Read the EFFECTIVE order, not the config file: since 2.0.0 `uscha init` generates a minimal
config, so `qa_tools_order` is often absent there and resolves from `defaults.risk_profile`
(ADR-001) or from the engine default. `qa_ledger.py doctor --json` prints it as
`effective.qa_tools_order` with its `origin`; the ledger froze the same value at `init`. A
project on **profile A runs `code-review` only** — do NOT invoke judgment-day or improve there,
and convergence must not wait for them.

After **each** tool pass:

1. Apply only fixes at/above the severity gate. Send the rest to `ISSUES-DEFERRED.md`.
2. Run the repo test command. If red and the fix isn't obvious → escalate.
3. Log the agent QA tools (code-review / judgment-day / improve) with `log-step`,
   using counts from each tool's own summary. These counts are **self-reported
   narration** (recorded for churn/retrospective); the blocking signals in this loop
   are the MEASURED records — ingest-gate, log-gate, snapshots:

```bash
python3 $QL log-step --repo <REPO> --tool <code-review|judgment-day|improve> \
  --iteration <N> \
  --reported <total findings> --gated-reported <findings at/above gate> \
  --fixed <fixed this pass> --deferred <sent to backlog> --suppressed <false positives> \
  --tests-passed <true|false> --files-changed <count> \
  --fingerprint <stable,finding,ids>   # enables oscillation detection
```

3b. **Fact gates — on every pass that changed code**, run gate-check (did the change
weaken the measuring apparatus? did it ADD a secret? — private keys, cloud tokens and
key-container files block as facts, kit 1.12.0; generic password/token literals advise;
new dependencies advise too — the change-budget rule "no new deps without approval" made
visible, kit 1.30.0). A rename is reported as a MOVE, never as a deleted test, and `--repo`
SCOPES the diff to that repo's path, so a monorepo sibling's hunks are neither this repo's
findings nor this repo's exit code (kit 2.2.0)
and, for migration work, golden-diff (does behavior still match the human-approved
baseline?). Then PERSIST each verdict with `log-gate` — a failing fact gate blocks
convergence and caps readiness ≤65 through the ledger:

**Rubric — the non-testable layer (kit 1.23.0).** If a `RUBRIC.md` exists (or
`defaults.rubric.file` declares one), on passes that changed code run the
`uscha-rubric` grade (isolated context — the grader reads ONLY diff + rubric;
evidence-or-nothing) and ingest the contract JSON:
`rubric-ingest --repo <REPO> --report reports/rubric-grade.json --iteration <N>`.
Advisory by default — it gates ONLY when the human declared
`defaults.rubric.gate: true` (then a below-threshold score blocks convergence like
any fact gate). Never declare the gate yourself.

**Find Bugs Once (kit 1.16.0).** When a pass you log carries `--fixed > 0`, run
`regression-check --repo <REPO> --from-git --base <base>`: closing findings without
adding a single NON-BLANK test-tree line is **NARRATED** closure — the failing test
goes BEFORE the fix (Tip 31), and every bug is found ONCE (Tip 94). It is a tripwire,
not a judge: any non-blank test line counts (even a comment — content across 9 stacks
is not guessed), so honor the `has_test_definition`/`has_assertion` facts and the
"evidencia DÉBIL" warning; test QUALITY is pit-check's job. NARRATED advises by
default; persist `log-gate --kind regression --verdict fail` if the team gates it.
Resolving a `flag-blocker` now REQUIRES `--escape-analysis "<which gate/test should
have caught this and what was done>"` — reflection is part of closing, not optional.

```bash
python3 $QL gate-check --from-git --base <base> --repo <REPO>   # exit 1 = BLOCKER
python3 $QL log-gate --repo <REPO> --iteration <N> --kind gate-check --verdict <pass|fail>

# CI, when the pipeline has run for this commit (kit 2.2.0). A green run is a FACT and
# belongs in the ledger; --ref is the receipt that outlives the conversation:
python3 $QL log-gate --repo <REPO> --iteration <N> --kind ci   --verdict <pass|fail|not-run> --ref <run URL or id>

python3 $QL golden-diff [--dir <fixtures-root>]   # exit 0 CLEAN · 1 DIVERGE · 2 NOT-RUN
python3 $QL log-gate --repo <REPO> --iteration <N> --kind golden-diff \
  --verdict <pass|fail|not-run>   # not-run records the absence — it is never green
```

(pit-check stays on its scheduled/incremental tier — CONSTITUTION §Tests efectivos —
but when a PIT report EXISTS and fails the gate, persist it the same way:
`log-gate --kind pit-check --verdict fail`.)

**Field truth — the greenfield evidence class (kit 2.2.0, ADR-046).** `characterize` and
`golden-diff` answer "does it still do what the OLD code did?", which greenfield has no way to
ask: there is no old code, and every test payload was invented by the agent that wrote the
subject. `corpus-run` is the evidence class for that gap — REAL inputs with their real expected
outputs, one JSON object per line (`input`, `expected`, optional `id`), each fed to the command
on stdin and compared against its trimmed stdout. Run it when the repo declares a corpus
(`repos[R].corpus`) or when the human hands you one; it is NOT part of the inner loop, since a
real corpus can be large (schedule it like pit-check). It is **advisory** until the project
declares a budget — with no `--threshold`, `repos[R].corpus_threshold` or
`defaults.corpus_threshold`, the percentage is measured and gates nothing, and it never counts as
an `ok` gate. With one declared, a run under it caps readiness ≤65 and blocks convergence like
any fact gate. A missing, empty or malformed corpus is exit 2 naming the line — never a scored
0 %. Never author a corpus yourself: an invented corpus is the invented input this instrument
exists to expose.

```bash
python3 $QL corpus-run --repo <REPO> --corpus <path.jsonl> --command "<cmd>"   [--threshold <P>] [--ac AC-FIELD-01] [--timeout 30]   # exit 1 = under the declared budget
# a corpus measured elsewhere (CI, a nightly) goes in through the parity door:
python3 $QL log-gate --repo <REPO> --iteration <N> --kind corpus   --verdict <pass|fail|advisory|not-run>
```

`--ac` stamps criterion ids on the record: a criterion whose only evidence is a corpus record
closes MEASURED iff that record PASSED, and a ticked criterion without one reports
`narrated_only` — the same rule a green JUnit testcase has always obeyed. `readiness` prints one
conditional `--- field <repo>: ...` line per repo that declares a corpus or ran one; it carries
no weight in the score (that dimension is deferred to its own ADR).

4. The **static analysis gate** (`java-qa-gate`: Checkstyle/PMD/SpotBugs/FindSecBugs)
   is NOT counted by hand. Run the gate so its XML reports are written, then ingest
   them — the ledger parses the reports, normalizes severities to the common gate
   scale, splits FindSecBugs (SECURITY) out from SpotBugs, and computes the real
   `fixed` count by diffing finding-IDs against the previous run:

```bash
# run your java-qa-gate first (it must emit:
#   target/checkstyle-result.xml, target/pmd.xml, target/spotbugsXml.xml)
python3 $QL ingest-gate --repo <REPO> --iteration <N>
# one static-gate step is logged per linter; pass --combined to merge into one.
```

Severity normalization: Checkstyle error→HIGH / warning→MEDIUM; PMD priority
1→BLOCKER … 5→LOW; SpotBugs priority 1→HIGH / 2→MEDIUM / 3→LOW; FindSecBugs
(category SECURITY) floored to HIGH. A report file that EXISTS but is empty credits the
fix; an ABSENT report is not treated as clean (means the gate didn't run).

**End-of-cycle checks (advisory — you make the final call):**

```bash
python3 $QL converged   --repo <REPO> --tools-per-cycle <count>   # exit 0 = converged
python3 $QL oscillation --repo <REPO> --tool <tool>               # exit 1 = oscillating
```

Convergence requires ALL of: the latest agent step of EVERY tool in `qa_tools_order`
clean (zero gated findings, zero files changed, tests green — padding the window with
extra clean steps does not help, and a red snapshot vetoes a narrated green), AND the
latest static-gate run of every linter clean at the gate level, AND every persisted
fact gate (`gate:*`, `blocker:*` records from log-gate/flag-blocker) clean. A clean
agent cycle alone does not converge if any measured gate still flags something.

- **Converged** → leave the loop for this repo.
- **Not converged** → next cycle, up to `config.defaults.max_iterations`.

## Phase 4 — Integration / contract pass (multi-repo)

After each repo converges individually, run the cross-repo layer with all repos mounted.
Run the integration/contract test command from the config and treat contract breakages
as gated findings. Log under `--repo integration`. This is the second layer of your
two-layer QA architecture; per-repo green does not imply the seams are green.

## Phase 5 — Verify (coverage generation, once)

Now that code is stable:

```bash
# /improve test  → write the fine-grained coverage you deferred earlier
# Then run the full suite and regenerate coverage reports.
python3 $QL snapshot --repo <REPO> --phase post   # for every repo + integration
```

Full suite must be green and coverage at/above threshold before proceeding.

**`reports/` is EVIDENCE, not build noise (kit 2.4.0).** JUnit XML, coverage reports
(JaCoCo/Cobertura/lcov/go cover), and `reports/smoke.json` are what `snapshot`,
`check-coverage` and `smoke-ingest` read to turn "we ran tests" into a measured fact.
Keep it, commit it, and never delete or `.gitignore` it to get a clean-looking commit —
a repo with no `reports/` reads UNMEASURED, not "clean". (`uscha init`'s generated
`.gitignore`, kit 2.4.0, never lists `reports/` for exactly this reason.)

## Phase 5b — Rebuild test (optional; risk profile C+/E or periodic CI)

Completeness of the SPEC, not correctness of the build: is the spec package enough to
regenerate the system from scratch? Worth running for critical/legacy work or on a
schedule, not every feature.

```bash
# 1) capture the signature of the system as it stands now
python3 $QL rebuild --mode baseline --config uscha.config.json
# 2) in a CLEAN tree / fresh session, regenerate PRODUCTION code from SPEC/ADR/
#    ACCEPTANCE only, PRESERVING the test suite, then run the tests.
# 3) score the regenerated tree against the baseline
python3 $QL rebuild --mode compare --baseline REBUILD-BASELINE.json   # exit 0 = COVERS
```

The dominant signal is the preserved suite: tests that passed originally but fail on
regenerated code are behavior the SPEC left implicit. Verdicts: COVERS (≥90), PARTIAL
(≥70), DIVERGE (<70). Feed the listed gaps back into the SPEC, then re-run — divergence
is a spec gap, not a code bug.

## Phase 6 — PR (stop at merge)

- **Gate the PR on the DERIVED state (kit 1.18.0)** — per repo:

```bash
python3 $QL phase --repo <REPO> --require pr-ready   # exit 1 = the facts say no
```

`pr-ready` is a PHASE VALUE, not a subcommand — there is no `qa_ledger.py pr-ready`.
It is always read through `phase --repo <REPO> --require pr-ready` as shown above
(`qa_ledger.py pr-ready` alone is an argparse error).

The state is COMPUTED from the ledger (converged + green tests + zero
BLOCKER/CRITICAL + no open escalation), never self-declared — if it exits 1, the
output lists exactly which facts are missing; do NOT open the PR, close the gap.
A `spike/*` branch NEVER passes this gate (kit 1.19.0): spike code is disposable
by contract — its only legitimate output is an ADR with lessons, never a merge.
- **Before declaring TERMINADO, run the seal (kit 1.92.0, INV-T1 / ADR-038):**

```bash
python3 $QL check-terminado          # 0 = sealed · 1 = broken · 2 = UNMEASURED
```

  It recomputes, from the ledger and the tree, whether the recorded evidence still belongs to
  the code on disk: the repo subtree clean, no source-relevant change since the last
  snapshot's commit (a non-source difference — docs, the ledger, the reports themselves —
  seals with a `note` naming what moved; ADR-039), every ingested report still hashing to
  what was recorded. **Exit 1** — do not declare TERMINADO:
  re-snapshot on the CURRENT state (`snapshot --repo <REPO> --phase post`) and record why the
  seal broke. **Exit 2** — the seal is UNMEASURED (no git, or no snapshot recorded): say so
  plainly; an answer nobody could measure is not a TERMINADO either.
- Ensure conventional-commit history is clean.
- **List the agent-origin items that are still unconfirmed** in the PR body, under
  **Agent-origin, unconfirmed** (principle 6). `spec-check` names them with file:line;
  copy that list. An empty list is written as "none" — silence and "none" are not the
  same fact.
- **Cite the operability line** in the PR body, under **Operability** (kit 2.2.0, ADR-048):
  copy `readiness`'s `--- operability: ...` line verbatim, whichever four verdicts it
  carries. A reviewer should not have to ask whether this change ships with a release
  path, a reset and a RUNBOOK — and a red line in the PR body is a conversation, which is
  exactly the conversation two projects never had until their last week.
- Open the PR(s). Confirm CI is green.
- **STOP.** Present the PR link(s) and wait for the human to merge.

## Phase 7 — Smoke run (measured, kit 2.2.0, ADR-047)

**Evidence is executed, not narrated.** This phase used to end in a prose checklist, and
prose is exactly where a smoke run goes to die: "the jar served /admin", "the simulator
answered 200 in 6 ms" are an agent's sentences, believed because they are written
confidently. A real one cost a field team a release — every simulator run returned an
empty list because the database had no rows, and a smoke narrated as "verified" would
have hidden it behind a paragraph. So RUN the smoke paths (real endpoints, real user
flows, real device paths for this change) with the project's own tool, have that tool
write `reports/smoke.json`, and INGEST it:

```json
{"checks": [{"name": "AC-28 the jar serves /admin", "ok": true, "status": 200,
             "latency_ms": 6, "evidence": "curl -sS localhost:8080/admin | head -1"}]}
```

```bash
python3 $QL smoke-ingest --repo <REPO> --report reports/smoke.json   # exit 1 = a check failed
```

`name` and a boolean `ok` are the whole mandatory contract; `status`, `latency_ms` and
`evidence` are optional and never invented. A missing `checks`, an EMPTY list, or a check
without a name or a boolean `ok` is exit 2 naming it — a run that verified nothing is not
a clean gate. A failed check is a BLOCKER like any other fact gate: readiness ≤65,
convergence blocked, cleared by a later clean report. `smoke` is a FACT kind and never
runs advisory — a check either answered or it did not. Name a check `AC-nn ...` (the same
tag grammar a JUnit testcase name uses) and a green check inside a passing report closes
that criterion MEASURED; a failed one vetoes it, exactly like a red test.
`log-gate --kind smoke --verdict pass|fail|not-run` is the parity door for a smoke
measured elsewhere. `uscha-kit/templates/scripts/smoke-report-example.json` is the
reference report. If the human runs the smoke by hand, they still write the report: a
checklist a human ticked is evidence, a checklist an agent narrated is not.

**The PR body cites the gate** (phase 6): paste the `smoke-ingest` verdict line —
`<repo>/gate:smoke: PASS — 8/8 checks ok` — not a prose claim that the smoke passed.

## Phase 8 — Hand off to docs + retrospective

```bash
python3 $QL summary           # human-readable
python3 $QL summary --json    # machine-readable, includes post_merge_calibration
```

**Operability is MEASURED here, not promised (kit 2.2.0, ADR-048).** Release by CI, the
reset/seed script and the RUNBOOK used to live in this phase as PROSE, and prose is what
lets "we do that at the end" survive every gate the kit has. Run it, per repo, BEFORE
readiness so the record is in the ledger when the KPI renders:

```bash
python3 $QL operability --repo <REPO>          # exit 0 always; the gate is the profile's
```

It reads four FACTS out of the tree and never runs anything: a `.github/workflows` step
that runs the repo's **configured** test command, a workflow that publishes or attaches a
release asset, a `RUNBOOK.md` (`docs/` first) naming start/config/rollback/smoke in EN or
ES, and a `seed_command` the config declares whose script is on disk. Repo path first,
CONFIG ROOT second, and it NAMES which it read. A CI system it does not read (GitLab,
Jenkins, Azure) is reported `unknown` — never green, never red.

The POSTURE is the risk profile's, not the kit's: on **A/B** the record is `advisory`
(caps nothing, blocks nothing, never counted as `ok`); on **C/D/E** — where
`defaults.operability.gate` is `true` — a missing check is a BLOCKER that caps readiness
≤65, blocks convergence, and makes `phase --require pr-ready` refuse NAMING the missing
one. Do NOT hand-wave a missing check: write the workflow, write the RUNBOOK section,
declare the seed command. If the project genuinely does not want the gate, that is a
declaration (`defaults.operability.gate: false`), not a silence.

`readiness` then prints one conditional line — `--- operability: ci ok · release missing ·
runbook ok · seed missing (advisory)` — and that is the line you cite in the PR body.

**Readiness KPI — show this after finishing ANY task, not only full runs.** It measures
the STATE of the result (not effort spent), as a weighted score 0..100 with hard caps:

```bash
python3 $QL readiness --acceptance <ACCEPTANCE.md> --tools-per-cycle <count> --record
```

Always pass `--record` here (kit 1.47.0): it persists the score to `readiness_history`
(the mirador time-lapse) and the measured summary + loop odometer — derived phase, pass
count, plateau flag per repo — to `ledger["measured"]` (what the statusline reads).
Without it the trail starves: the mirador shows "no history yet" and the statusline
falls back to counting checkboxes. Recording is append-only facts, never a gate.

**Tick measured-but-unchecked boxes before closing (kit 2.4.0).** `readiness` reports
`measured_unchecked` (`--json`) / a `· measured but unticked: AC-...` line (default view):
AC-IDs the ledger already closed with a green, name-tagged test but whose `ACCEPTANCE.md`
checkbox is still `[ ]`. For every ID it lists, flip that box to `[x]` in `ACCEPTANCE.md` —
the engine measured it; ticking is bookkeeping, never the other way round. Then re-run
`readiness` and confirm the list is empty before the close block. This is ONE-DIRECTIONAL:
never tick a box the ledger has not closed (that would be narrating, and `readiness`
already reports narrated-but-unmeasured boxes separately as `narrated_only`) — a tick
without a green test stays narrated-only, not measured.

Right after readiness, run the spec-maintenance advisory (kit 1.66.0):

```bash
python3 $QL spec-drift --repo <REPO>
```

It searches the repo's own path first and the CONFIG ROOT (where `uscha.config.json` and
the ledger live) second, and NAMES which of the two it read — in a monorepo the single
`SPEC.md` at the root is the spec of every repo, and "no spec documents" used to be
indistinguishable from "no drift" (kit 2.2.0).

Milliseconds, deterministic, exit 0 always — it never gates, so running it every pass
adds zero ceremony. What it adds is VISIBILITY: the run lands in the ledger, so the
mirador card and `/uscha-status` show drift without anyone remembering the command —
the user this advisory exists for is precisely the one who never types it. If any doc
reads `SPEC_STALE`, mention it in the close block's `Blocks:` line as advisory context
(it blocks nothing; it informs the human's next conversation).

**Single-verdict view (kit 1.25.0, anti-ceremony).** By default `readiness` is ONE
screen: the verdict line, any conditional warning that actually fired (it speaks only
when it matters), and a `--- gates:` line that COLLAPSES every persisted gate record —
the per-language linter gates (`*-qa-gate` from `ingest-gate`) plus the discrete fact
gates (`gate:*` from `log-gate`, `rubric:grade`, `blocker:*` from `flag-blocker`) — into
`N ok / M bloqueando (repo/gate...)`. This
is presentation over facts already in the ledger — it never recomputes the score, so
the KPI is identical to before the rollup existed. When the acceptance file has traceable
AC-IDs, the default view also prints the honest **`acceptance medido: X%`** line (kit
1.28.0 — criteria closed by a green name-tagged test / total; the `measured_pct` field in
`--json`): the one "% done" the kit can sign, MEASURED not narrated, informational and
never a gate. With no AC-IDs it does not print (a % over bare checkboxes would be
narrated). Pass `--verbose` to expand the dimensions table, the acceptance/coverage/churn
summary and the per-repo breakdown.
Show the human the default; reach for `--verbose` only when they ask what is inside the
number.

Dimensions and default weights: acceptance (traced, MEASURED) 30, ADR/checkbox
completion 15, coverage 15, static gate 20, convergence 10, integration 10. A
lint-capable repo whose static gate NEVER ran scores that dimension UNMEASURED (0.0) —
silence is not success. Hard caps override the weighted score: tests red → ≤35,
BLOCKER/CRITICAL open → ≤65, unresolved escalation → ≤75 (held until
`resolve-escalation` — a recorded event, not an implication). A `CONSTITUTION.md`
breach does NOT reach the engine by itself: **you MUST log it** —
`flag-blocker --repo <REPO> --kind constitution --note "<invariant breached>"` — and
once logged it caps readiness ≤65 and blocks convergence until resolved with
`flag-blocker --repo <REPO> --kind constitution --resolve --escape-analysis
"Added a regression test for the invariant."`. Bands:
<50 NOT READY, 50–79 IN PROGRESS, 80–94 RELEASE CANDIDATE, 95–100 READY.

**Acceptance traceability (the DOMINANT dimension — kit 1.10.0).** Each ACCEPTANCE
criterion carries a stable ID: `- [ ] AC-01 — when X then Y`. A criterion counts as
CLOSED only when ≥1 GREEN testcase whose name carries the tag (`test_ac1_x`,
`testAC01X`, `"AC-01: ..."` — IDs normalize by number, `AC-01 == AC_1 == ac1`) exists
in the ingested JUnit reports AND no tagged testcase is red. Since kit 1.87.0 (ADR-036)
a FAMILY prefix is read the same way: `- [ ] AC-BC-07 — ...` closes on `AC-BC-07_x`,
`test_ac_bc_7_y` or `AC_BC_7` (normalized to `AC-BC-7`; the family needs a separator on
both sides — camelCase `testACBC07` is NOT a tag, and `AC-7-x` is still the bare `AC-7`). The checkbox is the
NARRATIVE; the testcase is the FACT — a checked box without a green tagged test shows
up as `narrated_only` and does NOT close (measured beats narrated, per criterion).
A JUnit report older than the repo's source code is treated as STALE (the code changed
after the tests ran) and is DISCARDED — a criterion backed only by stale reports stays
UNMEASURED, never falsely closed or vetoed (kit 1.31.0; surfaced as `stale_reports`).
Since 1.93.0 (ADR-039) that clock rule is not the only one: a report the clock rejects is still
FRESH when its `sha256` matches what the last `snapshot` recorded for it AND git shows no
source-relevant change since that snapshot's commit — so a clone, a `git worktree add`, a merge
or a CI checkout, which re-date every file without changing a byte, no longer un-measure green
evidence. Either rule suffices; with no git, no recorded commit or no recorded hash the clock
rule decides alone, exactly as before.
So: when you write the tests for a criterion, put its AC-n in the test name; run
`spec-check --acceptance ACCEPTANCE.md` up front (zero traceable criteria / duplicate
IDs block as structural FACTS). Files without IDs fall back to the checkbox ratio
with a warning (legacy mode — adopt incrementally).

**Heed the two readiness advisories (kit 1.14.0 — they recommend, never gate).**
`stall:` = gated findings flat or RISING for 3 agent cycles: more iteration is not
approaching the solution — stop iterating, go back to the ADR / re-plan WITH the human.
`stop-signal:` = every repo converged and zero blocking facts remain: what's left is
measurable debt (coverage/acceptance), not findings — raise "candidate to cut and go
to PR" to the human instead of polishing further.

ADR completion is parsed from the **acceptance task list** (markdown `- [x]`/`- [ ]`),
read-only — set the path via `config.defaults.acceptance_file` or `--acceptance`. Count
the WHOLE file (the CLI default); only pass `--section` if you have verified the heading
text matches your template exactly — a mismatched section silently zeroes a heavy
dimension. Present the single-verdict headline (the number, the capping blocker if any,
and the collapsed `--- gates:` line); add `--verbose` only if the human wants the
dimension breakdown behind the number. Cycles/regressions are churn (process health) and
are reported separately — they never raise readiness.

Optionally (on request — reporting, not part of the verified build) invoke the `uscha-sysdoc`
skill to generate the two-view HTML deck. Finish with a retrospective drawn FROM the
ledger summary: total steps, %fixed per tool, coverage, prod LOC vs test LOC, test
count, tests/kLOC, **first-time yield** (kit 1.27.0 — the Lean % of repos that cleared
QA on the first cycle with no second pass, regression or escalation; passive, derived
from the ledger, informational only — never a gate), plus concrete methodology
improvements.

## Escalation contract — STOP and ask the human when:

- The iteration cap is hit without convergence.
- Oscillation is detected (a finding set keeps returning).
- A previously-passing test now fails and the fix is non-trivial.
- Two tools give contradictory directives on the same code.
- A fix would require an architectural decision (ADR-level change).
- A change would breach the `CONSTITUTION.md` (an inviolable invariant) — never trade it
  away; changing the constitution is a separate, explicit human decision.

Record every escalation, and record its CLOSURE — the readiness cap holds until the
human resolves it:

```bash
python3 $QL escalate --repo <REPO> --reason "<what blocked + what you need from me>"
# ... human reviews/decides ...
python3 $QL resolve-escalation --repo <REPO> --note "<how it was closed>"
```

A CONSTITUTION breach is escalated AND flagged as a first-class blocker:

```bash
python3 $QL flag-blocker --repo <REPO> --kind constitution --note "<invariant breached>"
# after the human decision:  flag-blocker --repo <REPO> --kind constitution --resolve
```

Never auto-merge, never silently exceed the iteration cap, never fix below the gate to
make the number look better.
