# uscha-kit

**Kit version:** v2.5.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**

Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
**Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
`uscha-characterize`, `uscha-rubric`, `uscha-mirador`, `uscha-status`) and a measurement engine (`qa_ledger.py`).

**Who it's for:** a solo operator carrying ONE non-trivial or risky change, kept
honest by a deterministic ledger and a human gate at the merge. It is NOT for trivial
changes (a one-liner runs build+test and that's it).

## What's inside

```
uscha-kit/
?? INSTALL.md                   # install guide: npx, Codex, Claude Code, alternatives
?? install-uscha.py             # canonical installer used by npm/npx
├─ uscha.config.json            # config: repos, thresholds, commands
├─ hooks/
│  └─ block-approved-writes.py     # PreToolUse: best-effort block on writing a golden (Claude only)
├─ templates/
│  ├─ CLAUDE.md                    # permanent repo protocol
│  ├─ CONSTITUTION.md              # inviolable invariants (fill in the domain)
│  ├─ .gitattributes               # *.approved.* binary — so line endings don't lie
│  └─ docs/adr/                    # ADR scaffold
?? .codex-plugin/plugin.json      # Codex plugin manifest
?? skills/                         # Codex plugin mirror of .claude/skills (smoke checks sync)
└─ .claude/skills/
   ├─ uscha-discovery/                   # vague idea → 1x1 grilling → CONSTITUTION/SPEC/ADR/ACCEPTANCE…
   ├─ uscha-adr-refine/                  # known feature: precision interview → ADR + ACCEPTANCE
   ├─ uscha-reverse-discovery/           # brownfield: FACTS map of the existing system (does not propose shape)
   ├─ uscha-characterize/                # captures the golden by running the ORIGINAL code (stops at human approval)
   ├─ uscha-devloop/
   │  ├─ SKILL.md                  # orchestrator: plan → build → QA loop → PR
   │  └─ qa_ledger.py              # measurement + ledger + gates (ingest/log-gate/golden-diff/gate-check/pit/simplicity/rebuild)
   ├─ uscha-sysdoc/                     # (optional) two-view HTML deck from the ledger
   └─ uscha-rubric/                      # (optional) rubric grading — thin adapter; the core is agnostic
```

## Fast-path (ADR-003) — measured entry for trivial changes

A one-line fix should not pay the ceremony of a schema migration. `fastpath-eval` grants the
shortcut from **measured signals**, never from anyone's opinion of "small":

```bash
python qa_ledger.py fastpath-eval --repo <name> --json            # dry-run: verdict only
python qa_ledger.py fastpath-eval --repo <name> --intent "fix: x" # records it in the ledger
```

Config (`defaults.fast_path`; absent block = feature off, behavior identical to earlier
releases): `max_files_changed` (3) and `max_loc_delta` (80) measured from
`git diff --numstat` against the merge-base with `origin/main` (fallback `main`) **plus
untracked files** — a new file is still a change; `protected_paths` globs deny regardless of
size; `require_asserting_test` caps readiness while a fast-path run has no measured test.

**Fail-closed:** no git, no resolvable base, no config → `DENY` with the reason named.
"Could not measure" never grants the shortcut. **Escalation:** re-running `fastpath-eval`
mid-run (the devloop does, before the PR step) with thresholds now exceeded flips the run to
`ESCALATED` through the standard escalation machinery — the derived phase blocks `pr-ready`
and readiness is capped until a human runs `resolve-escalation`, after producing the ADR +
ACCEPTANCE the change turned out to deserve. **The override is asymmetric (INV-RIGOR-02):**
you can always force the full path; nothing can force `ALLOW` over a measured `DENY`.

## Golden coverage (ADR-006) — the veto knows what a golden actually covers

A golden freezes behavior. Changing code a golden exercises is never a trivial change — but
until the engine knew WHICH source files a golden covers, that veto could not be measured, so
ADR-004 deferred it rather than ship a gate that pretended. `golden-coverage` builds the
mapping **by measurement**:

```bash
python qa_ledger.py golden-coverage --harness tests/golden/harness-x.py --golden tests/golden/x.approved.json
```

It runs the harness under `coverage.py` and records the source files that actually executed
into `golden.coverage.json` at the repo root (same convention as `golden.scrub.json`). The
harnesses drive their subject through a **subprocess**, so instrumentation is injected into
every python they spawn — measuring only the parent would record nothing and produce an empty
map, which would read as "this golden covers nothing".

Then, **opt-in**, the fast-path grows a `golden_touched` signal:

```json
"fast_path": { "forbid_when_golden_touched": true }
```

Absent or `false` → the veto does not exist and behavior is identical to earlier releases.
Declared → **fail-closed**: touching a mapped file denies (naming the golden and the file), and
so does a missing manifest, because "could not measure" never grants the shortcut. A malformed
manifest exits 2 rather than degrading into a silent "no mapping". Every verdict carries the
capture commit and tool version in the signal's `source` — provenance, not a freshness gate:
an aged map's real risk is a false negative, which cannot be detected without re-capturing.

Two honest limits (ADR-006): `coverage.py` is Python-only, so a harness in another language
cannot produce a map — declaring the veto there yields a permanent `DENY`, and the remedy is
not to declare it. And **file** granularity over-fires on monolithic files: in a repo whose
engine is one large module, the veto fires on nearly every change to it. When it errs, it errs
toward more ceremony, never less.

## Spec-drift (ADR-005) — the spec maintenance tax, made visible

Specs rot silently: the code moves and `SPEC.md` stays where it was. `spec-drift` detects
that lag **mechanically** — and reports it as **advisory, never a gate**, because whether an
older spec still covers newer code is a relevance judgment, and a guess advises.

```bash
python qa_ledger.py spec-drift --repo <name> --json   # advisory report; exit 0 always
```

It searches `repos[<name>].path` first and the **config root** (where `uscha.config.json` and the
ledger live) second, and the report NAMES which of the two it read (`spec_source`). A monorepo
keeps ONE `SPEC.md` at the root and it governs every repo — before 2.2.0 that case answered "no
spec documents", which reads exactly like "no drift".

Map specs to code with a `governs:` glob list in the frontmatter of `SPEC.md` and each
`docs/adr/*.md`:

```markdown
---
governs:
  - src/payments/**
  - db/**
---
```

Per document: **`SPEC_STALE`** when governed code outran the spec by more than
`defaults.spec_drift.max_lag_days` (default 30), listing the newer files; **`CLEAN`** when it
did not; **`UNMAPPED`** when there is no `governs:` frontmatter *or its globs match nothing*
— absence of a mapping is absence of measurement, not "no drift"; **`UNTRACKED`** when the
spec has no commit date to compare; **`NO-CODE`** when it declares `governs: []`, i.e. a
decision that governs no source (negative ADRs) — a declaration, not an omission. The latest run lands in the ledger (`spec_drift`) so the
mirador can surface it. No readiness impact, no exit-code gate: a stale spec is a prompt for
a human conversation, not a blocked pipeline.

## Operability (ADR-048) — release, reset and the RUNBOOK are part of done

A field finding, twice: release by CI, the reset/seed script and the RUNBOOK arrived in the
**last week** of two projects. The devloop named all four in phase 8 prose, and a narrated
dimension is not a weak gate — it is an absent one. `operability` reads them as FACTS in the tree:

```bash
python qa_ledger.py operability --repo <name> [--json]   # exit 0 always
```

| check | `ok` when |
|---|---|
| `ci` | a `.github/workflows/*.yml` step runs the repo's **configured** test command (`repos[R].test_command`, else `defaults.test_command_<type>`) — verbatim, or its first token beside a test-ish subcommand |
| `release` | a workflow publishes or attaches an asset: `softprops/action-gh-release`, `actions/upload-release-asset`, `gh release create`, `gh release upload`, `gh release`, `npm publish`, `twine upload` |
| `runbook` | `docs/RUNBOOK.md` (or `RUNBOOK.md`, or `defaults.operability.runbook`) exists AND names start/boot, config, rollback and smoke — EN or ES, case-insensitive; the absent ones are NAMED |
| `seed` | `repos[R].operability.seed_command` or `defaults.operability.seed_command` is declared, and its script is on disk when the command names a path |

Repo path first, **config root** second (`realpath` on both sides), and the output NAMES which it
read. It never executes anything. A CI system it does not read — GitLab, Jenkins, Azure — is
reported `unknown`, never green and never red.

The POSTURE is your risk profile's, not the kit's. `defaults.operability.gate` is `false` by the
engine's default and `true` on presets **C, D and E**:

- **advisory** (A, B, or no profile): the record caps nothing, blocks nothing, and never joins the
  `N ok` count in `readiness`;
- **gate** (C, D, E): a `missing` check is a BLOCKER — readiness capped ≤ 65, convergence blocked,
  and `phase --require pr-ready` refuses **naming** the missing check.

`readiness` prints one conditional line and stays silent with no record:

```
--- operability: ci ok · release missing · runbook ok · seed missing (advisory)
```

Nothing is graded. The engine can see that a `## Rollback` heading exists; whether the procedure
under it is correct is yours.

## Evidence origin (ADR-007) - green, but green at *what*?

Freshness compares file mtimes, so the ledger could say "tests green" without being able to
say which commit that was true of. Every snapshot now stamps where it came from:

```json
"origin": { "commit": "5d17cf4...", "dirty": false }
```

Measured with `git rev-parse HEAD` and `git status --porcelain` in the repo path. **Untracked
files count as dirty** - an untracked file the suite depends on is exactly the contamination
worth recording. **No git, no repo -> both `null`**, and `dirty: null` never reads as clean:
a tree state nobody could measure is not a clean one.

Advisory throughout: `snapshot` prints `origin=<sha8>/<clean|dirty|unknown>`,
`dashboard --json` carries `evidence_origin` when a snapshot has one, `/uscha-status` says one
line when the latest evidence was dirty. Readiness, phase and convergence are untouched -
knowing a tree was dirty does not tell you the evidence is wrong, only that it was not
produced from a commit alone. The git-worktree clean-room that would answer the stronger
question is deliberately deferred; ADR-007 records why.

## Clean-room (ADR-008) - verify the commit, not your tree

A suite can pass in your working tree *because of uncommitted state* and fail against the
commit alone. The ledger records that as measured, with provenance true of the TREE and not of
the COMMIT that will merge.

```bash
python qa_ledger.py cleanroom --repo <name> --run "<your test command>" [--setup "npm ci"]
```

It creates a detached `git worktree` of the commit, verifies it is clean by construction, runs
what you gave it, records `ref` / `worktree_sha` / `status` / wall-clock, and removes the
worktree unconditionally (a zombie worktree is a defect). Status is specific:
`GREEN`, `RED`, `SETUP_FAILED` (a bootstrap failure is not a failing suite),
`WORKTREE_FAILED`, `WORKTREE_DIRTY`.

**The engine never decides what to run.** The command arrives via `--run`, the same contract
`golden-coverage` uses for `--harness`: the engine owns isolation, the SHA binding and
cleanup, and never turns a config file into a code-execution surface. That is also why it
works for any stack - it never had to know the stack.

**Opt-in gate:** `defaults.clean_room.mode` absent or `"off"` and nothing changes. `"final"`
makes `pr-ready` require a GREEN run pinned to the current HEAD; a new commit makes the
previous run stale for the gate.

Honest limit: this is **not** a substitute for CI. A local worktree runs on your machine, your
OS, your shell - environment variance is invisible to it. Different failure class, different
instrument.

## Curation (ADR-009/010) - candidates in quarantine, verdicts on the record

Reverse discovery's brownfield entry: the agent may author CANDIDATE specs of a legacy
system - in `discovery/`, with mandatory `evidence` (`test|code|inference`) and
`confidence` frontmatter, refs the engine resolves against real files - and **nothing is
promoted without a human verdict**:

```bash
python qa_ledger.py curation-check --repo <name> [--json]
```

Verdicts live in `BEHAVIOR-LEDGER.md` (`preserve` / `fix` / `undefined`, one ADR each) -
a human-readable table with machine-enforced rules: strict shape (`exit 2` on malformed,
because under this gate a silent "no verdicts" would UNBLOCK what it guards), append-only
verified against git (revert = new row + new ADR, never an edit; latest row wins). While
any candidate lacks a verdict, `pr-ready` is blocked naming it (INV-CURATION-01) - the
quarantine is measured, not promised. No `discovery/` directory -> the feature does not
exist and nothing changes.

## Oracle divergences + roundtrip (slice 2)

A `fix` verdict means the new system must NOT match the legacy golden - and that divergence
is **declared**, never tolerated implicitly:

```json
// golden.divergences.json
{ "divergences": { "invoice-totals.approved.json": {
    "adr": "ADR-RD-003", "reason": "IVA now rounds; legacy truncated" } } }
```

`golden-diff` then reads the pair as `expected_divergence` (named, with its ADR) instead of
a blocker - and a declared pair that comes back **identical** goes red: the fix the
declaration describes is not in the output. Malformed declarations exit 2.

`roundtrip --repo <name>` is the advisory closing of the loop, v1: which promoted
candidates are traceable in the code via an embedded `uscha-spec: <candidate>` marker -
coverage by id, deliberately not semantic matching, exit 0 always.

## End-to-end flow

`uscha-discovery` is the front for something new (you only have the idea); `uscha-adr-refine` is the front
for a known feature; `uscha-devloop` builds and verifies. They meet at the `ACCEPTANCE.md`.

```
/uscha-discovery     # idea only + reference material → 1x1 grilling (proposes, you decide)
   ↓           #   writes CONTEXT.md, CONSTITUTION.md, SPEC.md, docs/adr/*.md, ACCEPTANCE.md, RISKS.md, HANDOFF.md
/uscha-devloop      # plan → build → QA loop (fact gates to the ledger) → PR (stops at the merge)
   ↓
/uscha-sysdoc       # (optional, on request) documents the system from the ledger
```

(For an already known feature, instead of `/uscha-discovery` you use `/uscha-adr-refine`.)

**Migration/legacy on-ramp (profile E):** the golden is the field truth and is captured
BEFORE touching anything.

```
/uscha-reverse-discovery   # FACTS map of the old system (endpoints, contracts, dependencies)
   ↓
/uscha-characterize        # runs the ORIGINAL code with a real corpus → .received → STOPS:
   ↓                 #   a HUMAN approves the golden (a best-effort hook discourages the agent; golden-diff measures)
/uscha-devloop            # migrates; golden-diff byte-compares against the .approved on each pass
```

## Requirements

- **Python 3.8+** (stdlib only — no `pip install`). `cloc` is NOT needed; the LOC is
  counted in Python.
- For `ingest-gate` and coverage to work, your Maven build must emit the
  reports (your `java-qa-gate` already has the plugins; these are the paths the ledger
  expects):

  | data        | plugin / goal                                   | file |
  |-------------|--------------------------------------------------|---------|
  | coverage    | `jacoco-maven-plugin` (`report`)                 | `target/site/jacoco/jacoco.xml` (or `jacoco-aggregate/`) |
  | test count  | `maven-surefire-plugin` / `failsafe`             | `target/surefire-reports/TEST-*.xml` |
  | checkstyle  | `maven-checkstyle-plugin` (`checkstyle`)         | `target/checkstyle-result.xml` |
  | pmd         | `maven-pmd-plugin` (`pmd`)                        | `target/pmd.xml` |
  | spotbugs+fsb| `spotbugs-maven-plugin` (+ findsecbugs)          | `target/spotbugsXml.xml` |

  Flutter: coverage from `coverage/lcov.info` (`flutter test --coverage`);
  the test count is approximate (it counts `test(`/`testWidgets(`).

  Python (`type: python`, kit 1.4.0):

  | data        | command                                           | file |
  |-------------|---------------------------------------------------|---------|
  | coverage    | `pytest --cov --cov-report=xml:reports/coverage.xml` | `coverage.xml` or `reports/coverage.xml` (Cobertura) |
  | test count  | `pytest --junitxml=reports/junit.xml`             | `reports/junit.xml` (wrapped root `<testsuites>` supported) |
  | ruff        | `ruff check --output-format=json > reports/ruff.json` | `reports/ruff.json` (S*/E9*/F82*→HIGH · B*→MEDIUM · rest→LOW) |
  | mypy        | `mypy src > reports/mypy.txt`                     | `reports/mypy.txt` (error→HIGH · warning→MEDIUM · note→INFO) |

  `ingest-gate` finds them automatically by the repo's type, or with explicit `--ruff/--mypy`.
  Contract identical to Java: a missing report = the gate didn't run (it never credits fixes).

  TypeScript/JS (`type: node`, kit 1.5.0):

  | data        | command                                           | file |
  |-------------|---------------------------------------------------|---------|
  | coverage    | `jest --coverage` (or vitest with lcov reporter)  | `coverage/lcov.info` (same parser as Flutter) |
  | test count  | `jest-junit` / `vitest --reporter=junit`          | `reports/junit.xml` or `junit.xml` (wrapped root supported) |
  | eslint      | `eslint . --format json > reports/eslint.json`    | `reports/eslint.json` (error→HIGH · warn→MEDIUM · `security/*` floor HIGH · ruleId null→HIGH) |
  | tsc         | `tsc --noEmit > reports/tsc.txt`                  | `reports/tsc.txt` (error TS → HIGH) |

  `ingest-gate` finds them by the repo's type, or with explicit `--eslint/--tsc`.
  Same absence contract.

  Go (`type: go`, kit 1.6.0):

  | data        | command                                           | file |
  |-------------|---------------------------------------------------|---------|
  | coverage    | `go test -coverprofile=coverage.out ./...`        | `coverage.out` (native cover profile — % by STATEMENTS, the Go convention) |
  | test count  | `gotestsum --junitfile reports/junit.xml -- ./...`| `reports/junit.xml` (wrapped JUnit supported) |
  | golangci    | `golangci-lint run --output.checkstyle.path=reports/golangci.xml` (v2; in v1: `--out-format checkstyle > ...`) | `reports/golangci.xml` (checkstyle format: error→HIGH · warning→MEDIUM; includes gosec if enabled) |

  `ingest-gate` finds it by the repo's type, or with explicit `--golangci`. The
  `_test.go` tests live alongside the code (Go convention) — the LOC classifies them by suffix.
  `vendor/` and `testdata/` are excluded from the LOC. Same absence contract.
  **Watch out for severities**: golangci-lint emits `severity=error` for EVERYTHING unless you
  configure `severity:` rules — without that, even style nits gate as HIGH;
  configure severities (or a lean linter set) so that MEDIUM really exists.

  Rust (`type: rust`, kit 1.7.0):

  | data        | command                                           | file |
  |-------------|---------------------------------------------------|---------|
  | coverage    | `cargo llvm-cov --cobertura --output-path reports/coverage.xml` | `reports/coverage.xml` (Cobertura — same parser as Python) |
  | test count  | `cargo nextest run` + copy (see note)              | `reports/junit.xml` (wrapped JUnit supported) |
  | clippy      | `cargo clippy --message-format=json > reports/clippy.json` | `reports/clippy.json` (JSONL: error→HIGH · warning→MEDIUM · `code:null` compile-error→HIGH; summaries without span ignored) |

  `ingest-gate` with explicit `--clippy` or by type. Its JSONL evidence is fail-closed:
  each nonblank line must be a UTF-8 Cargo JSON object with a string `reason`; malformed
  records or malformed compiler diagnostics are rejected before ledger mutation. An empty
  file plus valid Cargo summaries and span-less diagnostic summaries remain clean/noise.
  The inline `#[cfg(test)]` tests count as prod LOC (documented limitation); `tests/` = integration.
  **Watch out for junit**: nextest does NOT emit JUnit by default — you have to enable it in
  `.config/nextest.toml` (`[profile.default.junit] path = "junit.xml"`) and the file
  lands in `target/nextest/default/junit.xml`; copy it to `reports/junit.xml`
  (`cp target/nextest/default/junit.xml reports/junit.xml`, already included in the
  example `test_command_rust`).

  C#/.NET (`type: dotnet`, kit 1.7.0):

  | data        | command                                           | file |
  |-------------|---------------------------------------------------|---------|
  | coverage    | coverlet.msbuild: `/p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$PWD/reports/coverage.xml` | `reports/coverage.xml` (Cobertura) |
  | test count  | `dotnet test --logger "junit;LogFilePath=$PWD/reports/junit.xml"` (JUnitXml.TestLogger package) | `reports/junit.xml` |
  | roslyn      | `dotnet build /p:ErrorLog="reports/analysis.sarif,version=2"` | `reports/analysis.sarif` (SARIF: error→HIGH · warning→MEDIUM · note→INFO; suppressed ignored) |

  `ingest-gate` with explicit `--sarif` or by type. SARIF is the universal static-analysis
  format — the parser works for any tool that emits it.
  **Watch out for paths**: relative `LogFilePath` and `CoverletOutput` resolve against the
  TEST PROJECT directory, not the repo root — hence the `$PWD`
  (absolute anchoring). With multiple test projects, merge (`/p:MergeWith`) or use one
  report per project. `ErrorLog` without `,version=2` emits SARIF **v1**; the parser
  has a v1 fallback, but ask for v2 (the comma requires the quotes).

  C++ (`type: cpp`, kit 1.8.0):

  | data        | command                                           | file |
  |-------------|---------------------------------------------------|---------|
  | coverage    | `gcovr --cobertura reports/coverage.xml` (over gcov) | `reports/coverage.xml` (Cobertura — same parser) |
  | test count  | `ctest --test-dir build --output-junit ../reports/junit.xml` (CMake ≥3.21) or `--gtest_output=xml:...` | `reports/junit.xml` (flat AND wrapped root supported) |
  | clang-tidy  | `clang-tidy <files> > reports/clang-tidy.txt`     | `reports/clang-tidy.txt` (error→HIGH · warning→MEDIUM · `cert-*`/security floor HIGH) |

  `ingest-gate` with explicit `--clang-tidy` or by type. The build system (CMake/
  Bazel/make) belongs to the per-repo adapter — the kit only requires that the reports exist.
  `cmake-build-*` (any CLion profile) and `_deps` are excluded from the LOC.

  **Text-report boundary:** mypy, tsc, and clang-tidy ingest only their recognized diagnostic
  lines. Their legitimate clean/noise forms include status/success text, tool summaries, and
  (for clang-tidy) passed-through compiler output, so a nonempty unmatched line is not safely
  distinguishable from a clean report. The kit deliberately does **not** invent fail-closed
  heuristics for those text formats; use their structured output where a fail-closed schema is
  required.

  Kotlin/JVM with Gradle (`type: gradle`, kit 1.9.0) — **Kotlin over Maven already
  works with `type: maven`** (`.kt` has always counted; JaCoCo/Surefire don't
  distinguish JVM language). This type is for the common Gradle case:

  | data        | command                                           | file |
  |-------------|---------------------------------------------------|---------|
  | coverage    | `./gradlew test jacocoTestReport`                 | `build/reports/jacoco/test/jacocoTestReport.xml` (JaCoCo — same parser as maven) |
  | test count  | (same `./gradlew test`)                           | `build/test-results/**/TEST-*.xml` (per-class, like surefire) |
  | detekt      | `./gradlew detekt` **separately** (see note)      | `build/reports/detekt/detekt.xml` (checkstyle format: error→HIGH · warning→MEDIUM; absolute paths relativized) |

  `ingest-gate` with explicit `--detekt` or by type. It works the same for Java-over-
  Gradle. LOC: `src/main` = prod; `src/test` AND custom source sets (`src/
  integrationTest`, `src/functionalTest` — any `src/*Test`) = test
  (`.kt`/`.kts`/`.java`). The JaCoCo requirement: `jacoco` plugin +
  `jacocoTestReport { reports { xml.required = true } }`.
  **Watch out for detekt**: do NOT chain it to the test command — its default is `maxIssues: 0`,
  meaning ONE finding breaks the build and the test run would read red even though
  the tests pass. Run `./gradlew detekt` separately, before the `ingest-gate`
  (the lint gate has its own channel).

  Swift (`type: swift`, kit 1.9.0):

  | data        | command                                           | file |
  |-------------|---------------------------------------------------|---------|
  | coverage    | `swift test --enable-code-coverage` + `llvm-cov export -format=lcov ... > coverage/lcov.info` | `coverage/lcov.info` (lcov — same parser as Flutter/node) |
  | test count  | `swift test --xunit-output reports/junit.xml`     | `reports/junit.xml` **+ `reports/junit-swift-testing.xml`** (they are SUMMED — see note) |
  | swiftlint   | `swiftlint lint --reporter checkstyle > reports/swiftlint.xml` | `reports/swiftlint.xml` (checkstyle format: error→HIGH · warning→MEDIUM; absolute paths relativized) |

  `ingest-gate` with explicit `--swiftlint` or by type. LOC: SwiftPM convention
  (`Sources/` = prod, `Tests/` + `*Tests.swift` = test). The lcov export needs
  the `llvm-cov export` step against the tests binary (the `.profdata` alone is
  not enough) — leave it in your repo's `test_command_swift`; on macOS it is
  `xcrun llvm-cov`, on Linux plain `llvm-cov` (and the binary lives in
  `.build/debug/<Pkg>PackageTests.xctest`).
  **Watch out for Swift Testing**: `--xunit-output` writes the XCTest results to
  `junit.xml` and the **Swift Testing** ones (the default in Swift 6) to a SECOND
  file `junit-swift-testing.xml` — the engine sums BOTH; if it only read the
  first, a Swift 6 package would measure `tests=0` and a real failure would be
  invisible (fail-open).

## Installation

**Recommended path: npm/npx.** Use the same installer for Codex, Claude Code,
or both. Full guide: [`INSTALL.md`](INSTALL.md).

```bash
npx --yes @andresmassello/uscha@latest version
npx --yes @andresmassello/uscha@latest install --target codex --dry-run
npx --yes @andresmassello/uscha@latest install --target codex
npx --yes @andresmassello/uscha@latest doctor --target codex
```

For a machine used by both Codex and Claude Code:

```bash
npx --yes @andresmassello/uscha@latest install --target both --dry-run
npx --yes @andresmassello/uscha@latest install --target both
npx --yes @andresmassello/uscha@latest doctor --target both
```

Use a Git checkout only when developing Uscha itself or testing unreleased
changes:

```bash
python uscha-kit/install-uscha.py install --target both --mode link --dry-run
python uscha-kit/install-uscha.py install --target both --mode link
```

Claude Code's native plugin flow remains available for Claude-only users, but
`npx` is the universal path and also covers Codex.



**Verify the installation with `doctor`** (kit 1.22.0, in the spirit of flutter doctor —
Windows and Linux, ASCII output, exit 1 only on errors):

```bash
python uscha-kit/install-uscha.py doctor --target both
# or engine-only: python3 ~/.claude/skills/uscha-devloop/qa_ledger.py doctor
```

It checks: Python >=3.8 · git · the 9 skills alongside the engine (frontmatter
verified) · **whether the INSTALLED skills are older than this kit** (2.2.0, ADR-045) · the
INV-GOLDEN-01 hook (present + registered in settings.json +
powershell/pwsh interpreter) · and if there is a `uscha.config.json` in the cwd:
config parseable, ACCEPTANCE with AC-IDs, ledger integrity, the QA skills
from `qa_tools_order` (the loop orchestrates them without bringing them in) and the
primary toolchain of each repo by type (its absence is a WARNING — it may live only in CI).

**Are the installed skills older than the kit?** (2.2.0, ADR-045.) Every generated `SKILL.md`
orientation block now opens with `<!-- uscha kit: X.Y.Z -->`, rendered from `uscha-kit/VERSION`
and re-rendered by the release script at every bump, so an installed skill can be dated. `doctor`
reads that marker out of each install root the installer writes to — or the ones you name — and
reports both versions:

```bash
python3 ~/.claude/skills/uscha-devloop/qa_ledger.py doctor --json
# -> "kit_version" and "skills_installed": [{target, root, status, installed, kit}]
python3 .../qa_ledger.py doctor --installed /some/other/skills   # repeatable
```

`status` is `current`, `outdated` (with **both** versions; `installed: null` when the blocks
predate 2.2.0 and carry no marker at all) or `not installed` — which is not a fault, since the kit
installs one agent at a time. This is **advisory**: an outdated install is a warning, never an
error, and `doctor`'s exit code is unchanged. It exists because a discovery once ran on skills
from 1.54.0 while the kit was at 1.97.0 and nothing said a word. `/uscha-status` prints the same
finding as one line above its readout.

The comparison needs BOTH halves, so the installer ships the second one: `install` copies
`uscha-kit/VERSION` beside the installed skills as **`.uscha-kit-VERSION`**
(`~/.claude/skills/.uscha-kit-VERSION`, and the equivalent root for `pi`, `cursor`, `copilot`,
`gemini` and `cline`; the Codex plugin carries the same file at its plugin root), and `uninstall`
removes it. The name is namespaced on purpose: `~/.claude/skills/` is a directory the kit does not
own, and a copy called `VERSION` would overwrite whatever else lived under that name and delete it
on uninstall. **A foreign `VERSION` is never written and never removed** (`AC-SK-08`).

Without that copy an INSTALLED engine has no kit version to compare against and `doctor` says so —
`installed-skill freshness is UNMEASURED`, naming the directories it read, never a silent
`current`. Two other sources answer first where they can: the kit checkout itself, reached through
the engine's REALPATH so a `--mode link` install resolves the kit its skills point into
(`AC-SK-09`), and `uscha-install.json`'s `version` where the copy was removed by hand. Found by
running the first-use walkthrough end to end; pinned by `AC-SK-07`, which installs for real.

## Configure

`uscha init` **generates** a minimal `uscha.config.json` for the project: its name, the repo it
detected (path, type, test command), `acceptance_file`, `id_granularity`, `max_iterations`, and
the `fast_path` block and the `integration` switch (no contract command: that names a build
system and is yours to add). Those last two are there because the engine defaults
both to OFF and the kit means them ON — without them `fastpath-eval` would answer
`DENY configured: false` on a fresh project and readiness would measure five dimensions instead
of six. Every other knob is absent on purpose and resolves to the engine default. The kit's own
`uscha-kit/uscha.config.json` is the **comprehensive reference** — every knob at the kit's
value, to read and copy from — and is no longer copied into projects.

`uscha init` also writes a minimal `.gitignore` (kit 2.4.0), scoped to the detected repo type
(`__pycache__/`, `*.pyc`, `.pytest_cache/`, `.coverage` for python; `node_modules/` for node; and
so on) — never `reports/`, which is the ledger's own evidence (JUnit, coverage, smoke). An
existing `.gitignore` is always left untouched, even with `--force`.

That matters because of one rule: **a knob you declare > the preset named by
`defaults.risk_profile` > the engine default**. A copied default is an explicit declaration, so
a project holding the whole reference leaves its risk profile nothing to decide.

- `defaults.risk_profile`: `A`..`E` (ADR-001) — a named preset that expands into
  `qa_tools_order`, `coverage_threshold` and `golden_required`. `A` (trivial) runs
  `[code-review]` only; `E` (migration/legacy) requires the three tools, coverage 80 and an
  approved golden. Absent = the kit defaults.
- Check what is actually in force, and where each value came from:

```bash
python3 ~/.claude/skills/uscha-devloop/qa_ledger.py doctor --json
# -> "risk_profile" and "effective": {knob: {value, origin}}
```

  `origin` is `override` (you declared it), `profile <X>` (the preset supplied it), or
  `default` (the engine's own value). An override that supersedes a profile is reported as
  information, not an error — declaring a knob by hand is how a preset is bent. Nothing is
  written back into your config: a knob nobody declared stays absent, which is what lets the
  engine keep telling a requirement apart from a default. With no profile and no
  `qa_tools_order`, that knob reads `not declared` and convergence uses a window of
  `--tools-per-cycle` agent steps.

**Migrating a project initialised before 2.0.0.** Its config is the full copy, so
`coverage_threshold` and `qa_tools_order` read `origin: override` and no profile can move them.
(`golden_required` is the one the reference never declared, which is why a profile could still
supply it — that is the whole shape of the defect: the preset reached only the keys the copy
happened to omit.) Delete the ones you never meant to declare, then add the profile:

```diff
 {
   "defaults": {
-    "coverage_threshold": 60,
-    "qa_tools_order": ["code-review", "judgment-day", "improve"],
+    "risk_profile": "A",
     "acceptance_file": "ACCEPTANCE.md"
   }
 }
```

Nothing is deleted for you: a value equal to a former default cannot be told apart from a value
you chose, so existing configs keep behaving exactly as they did.

The full set of knobs, all optional:

- `repos[]`: name, `path` (relative to the primary repo), `type` (`maven`|`flutter`|`python`|`node`|`go`|`rust`|`dotnet`|`cpp`|`gradle`|`swift`).
- `defaults.coverage_threshold`: triggers the characterization phase if below it.
- `defaults.severity_gate`: which severities block (default BLOCKER/CRITICAL/HIGH).
- `defaults.id_granularity`: `line` or `file` (default `file`: more stable if you refactor a lot).
- `defaults.acceptance_file`: path of the **acceptance task list** (markdown checkboxes) that feeds the ADR completion of readiness. Default `ACCEPTANCE.md`.
- `defaults.constitution_file`: path of the **CONSTITUTION** (inviolable invariants). Default `CONSTITUTION.md`.
- `defaults.rebuild.coverage_tolerance`: coverage points the rebuild can drop without penalty (default 5).
- `defaults.readiness_weights` / `readiness_caps` / `static_gate_zero_at`: weights and caps of the KPI.
- `defaults.execution_policy`: phase-level routing metadata (`method`, `tier`, `model`, `effort`) shown by `execution-policy` and Mirador. It guides the operator; it does **not** affect readiness.
- Discovery intake commands (`production-finding`, `spec-doubt`, `spec-change-request`) persist post-merge production facts, SPEC doubts, and human contract-change bridges so the next cycle reopens discovery/SPEC instead of hiding reality in narration.
- ADRs may use `Status: Experiment` when a decision is an explicit, measured hypothesis. `dashboard --json`/Mirador expose `adr_status`, feedback/review metadata, malformed/expired counts; this is advisory visibility, not readiness scoring.
- `defaults.max_iterations`, `tools_per_cycle`, test commands.

## Multi-repo: mounting the other repos

The skill runs from the primary repo; the others are mounted in the session:

```bash
cd <repo-primario>
claude --add-dir ../backend-api --add-dir ../mobile-app
```

(or `additionalDirectories` in `.claude/settings.json`).

## Use

Inside Claude Code, invoke the orchestrator (with the ADR/PLAN ready or ask it for one):

```
/uscha-devloop
```

The skill handles the phases on its own: plan → coverage gate → (characterization if needed)
→ build → per-repo QA loop → integration → verify → PR (stops at the merge, you approve
it) → smoke list. It logs every step in `QA-LEDGER.json`. At the end:

```
/uscha-sysdoc        # generates docs/system-deck.html (CEO + technical, navigable)
```

## Quick check (engine dry run, without the skill)

To confirm the engine parses your reports correctly BEFORE trusting it with the loop:

```bash
cd <repo-primario>
QL=".claude/skills/uscha-devloop/qa_ledger.py"

python3 $QL --help                                  # see subcommands
python3 $QL init --config uscha.config.json      # creates QA-LEDGER.json
# later, a new service joins an existing loop — this APPENDS and re-seals the checksum,
# it never rebuilds the ledger (2.2.0):
python3 $QL init --add-repo web-app --path web-app --type node --test-command "npm test"

# run your build with the reports, then:
python3 $QL snapshot      --repo backend-api --phase pre
python3 $QL check-coverage --repo backend-api       # exit 0 = OK, 1 = below threshold
python3 $QL ingest-gate   --repo backend-api --iteration 1
python3 $QL summary                                 # human summary
python3 $QL summary --json                          # includes post_merge_calibration
```

If `snapshot`/`ingest-gate` say "no report found", it's because the build hasn't generated
the XML yet — run `mvn test` with the plugins active first.

## Readiness KPI (when finishing any task)

Shows the "ready for release" status as a 0..100 score, **based on the state of the
result, not on effort spent**:

```bash
python3 $QL readiness --acceptance ACCEPTANCE.md
python3 $QL readiness --json            # consumed by uscha-sysdoc (traffic-light widget)
python3 $QL execution-policy --phase qa  # one-line methodology/model/effort routing
python3 $QL production-finding --repo backend-api --severity HIGH --title "..." --evidence "..."
python3 $QL spec-doubt --repo backend-api --kind spec-wrong --note "..." --evidence "..."
python3 $QL spec-change-request --repo backend-api --source SD-001 --requested-change "..." --evidence "..."
```

- Dimensions/weights: **acceptance traced (MEASURED) 30**, ADR/checkboxes 15, coverage 15,
  static gate 20, convergence 10, integration 10. The **ADR completion** comes from your
  acceptance task list (checkboxes `- [x]`/`- [ ]`, read-only) — count the whole file
  (CLI default); `--section` only if you verified that the heading matches
  exactly (a mismatch silently zeroes the dimension).
- **Traceability (kit 1.10.0, the dominant dimension)**: each criterion carries a stable ID
  — `- [ ] AC-01 — when X then Y`. A criterion closes MEASURED only when there exists
  ≥1 GREEN testcase with its tag in the name (`test_ac1_x` / `testAC01X` / `"AC-01: ..."`
  — normalized by number: `AC-01 == AC_1 == ac1`) in the already-ingested JUnit reports,
  and no tagged testcase in red. The checkbox is NARRATIVE; the testcase is FACT: an
  `[x]` without a green test appears as `narrated_only` and does NOT close. Anti-Goodhart: the agent
  can no longer raise the KPI by polishing coverage — only by closing criteria with named
  tests. `spec-check --acceptance ACCEPTANCE.md` validates the structure as a FACT (zero
  traceable criteria or duplicate IDs = BLOCKED). Without IDs: it falls back to the checkbox ratio
  with a warning (legacy, incremental adoption). Flutter doesn't emit JUnit: its criteria don't
  close measured (documented limitation).
- A lintable repo whose static gate **never ran** scores that dimension UNMEASURED (0.0)
  — silence is not success.
- **Agent-origin items (kit 2.2.0, ADR-044)**: when `ACCEPTANCE.md`, `docs/adr/*.md` or
  `HANDOFF.md` carry unconfirmed `(origin: agent)` markers, readiness prints one line of its own
  — `--- origin: N agent-origin item(s) unconfirmed` — outside the gates rollup. Advisory: it
  caps nothing and blocks nothing. See below.
- **Hard caps** (they override the ceiling): tests in red → ≤35, open BLOCKER/CRITICAL → ≤65,
  unresolved escalation → ≤75 (holds until `resolve-escalation`, a recorded event).
- Bands: `<50 NOT READY` · `50–79 IN PROGRESS` · `80–94 RELEASE CANDIDATE` · `95–100 READY`.
- Multi-repo: per-repo and aggregate (min() for blockers, LOC-weighted for quality).
- Cycles/regressions are **churn** (process health), reported separately and never
  raise readiness.

## Rebuild test (SPEC completeness)

A different question for the ledger: not "did this build pass?" (correctness) but "is the SPEC
enough to regenerate the system?" (completeness). For profiles C+/E or periodic in CI.

```bash
# 1) in the ORIGINAL tree: capture the signature the rebuild must match
python3 $QL rebuild --mode baseline --config uscha.config.json   # → REBUILD-BASELINE.json
# 2) in a CLEAN tree / new session: regenerate ONLY the production code from
#    SPEC/ADR/ACCEPTANCE, PRESERVING the tests, and run the suite.
# 3) score the regenerated tree against the baseline
python3 $QL rebuild --mode compare --baseline REBUILD-BASELINE.json   # exit 0 = COVERS
python3 $QL rebuild --mode compare --baseline REBUILD-BASELINE.json --json   # consumed by uscha-sysdoc
```

- Dimensions/weights: tests 60, acceptance 20, coverage 15, surface 5. The dominant signal
  is the **preserved suite**: a test that passed and fails in the regenerated code = behavior
  the SPEC left implicit.
- Verdicts: `COVERS ≥90` · `PARTIAL ≥70` · `DIVERGE <70`. The score lists the concrete
  **gaps** — feed them back into the SPEC and re-run. Divergence is a spec hole, not a code bug.

## Simplicity check — "Reduce" (minimality of the change) — ADVISORY by default

The **Simplicity** invariant of the CONSTITUTION made deterministic: it scores the *diff*
(not CC by AST — they are measurable proxies: minimality, nesting, new abstractions).

```bash
git diff --unified=0 <base> | python3 $QL simplicity-check --config uscha.config.json
python3 $QL simplicity-check --from-git --base main            # uses git for you
python3 $QL simplicity-check --diff changes.diff --json        # consumed by uscha-sysdoc / CI
```

- Dimensions/weights: diff_size 35, nesting 30, net_growth 20, fan_out 8, blob 7
  (abstraction does NOT weigh in the score — it's a guessy proxy, kept as a metric + advisory flag).
- Verdicts: `SIMPLE ≥85` · `ACCEPTABLE ≥65` · `OVERBUILT <65`.
  A gross excess (2× budget, or very deep nesting) caps the score at 60 no matter what.
- **Advisory by default, exit 0** (kit 2.1.0, ADR-043): every budget above is the KIT's opinion
  until you declare your own, and an opinion that exits 1 is a gate nobody asked for. It
  **gates** — OVERBUILT = exit 1 = BLOCKER — only with at least one budget declared in
  `defaults.simplicity` **AND** `defaults.simplicity.gate: true` (or `--gate`). `gate: true`
  with no budget declared is a config error, exit 2: a gate with no budget is not a gate.
  `log-gate --kind simplicity --verdict advisory` persists an advisory run as an advisory —
  readiness prints `N ok · 1 advisory` and the mirador reads `ADVISORY`, never `OK`.
- **`max_nesting` is an INDENTATION-DEPTH proxy**, not AST nesting: it reads leading
  indentation on added lines, so a wrapped argument, JSX, or a multi-line Java literal inflates
  it with no control flow present. It is named as a proxy in the report rather than made
  language-aware. 2-space codebase → `--indent-width 2`.
- **Tests OUT of the budget** (kit 1.11.0): the test files (conventions of the
  9 stacks) are counted and reported separately (`test_lines_added`) but do not gate — writing
  tests never pushes the diff to OVERBUILT (deleting them is already blocked by gate-check).
- The flags tell you what to trim (guard clauses, speculative types/layers, giant hunks).
- Budgets in `defaults.simplicity`. No risk profile owns them: the gate is a human declaration
  under every profile A–E.

## Agent-origin markers — ask for DECISIONS, never for INFORMATION (ADR-044)

Two field findings, one rule. **Information the tree holds is READ, never asked**: the current
version of a tracked `.md`, what a config says, whether a test exists. And **a decision the human
never made never enters scope silently** — an agent turned a global default into a tree-wide
rename that was approved by rebound inside a twenty-item summary, and had to be cancelled after
112 files had moved.

So anything the AGENT introduced — an acceptance criterion, an ADR decision item, a `HANDOFF.md`
rule — carries one trailing 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)
```

- **No marker = human origin.** That is the default, so nothing already written is retro-tagged.
- **Confirmation is PER ITEM**, recorded as `confirmed: YYYY-MM-DD` on the same line. A
  package-level "ok" confirms none of them, which is the whole point.
- **An unconfirmed item is not in scope**: it is not implemented, not gated on, not quoted as
  agreed.
- A `confirmed:` that is not a real `YYYY-MM-DD` date counts as **unconfirmed** and is named — a
  typo must never read as a human's approval.
- A marker inside a fenced block or an inline code span (like the two above) is documentation and
  is skipped, so a file that DEFINES the grammar is not read as a finding.

What the engine does with it, on the files it already reads plus `ACCEPTANCE.md`, `docs/adr/*.md`
and `HANDOFF.md`:

```bash
python3 $QL spec-check --spec SPEC.md --acceptance ACCEPTANCE.md
#   ~ origin: 2 agent-origin item(s) unconfirmed -- AC-07 (ACCEPTANCE.md:41), D-03 (docs/adr/ADR-002-x.md:57)
python3 $QL readiness
#   --- origin: 2 agent-origin item(s) unconfirmed   (spec-check names them)
```

**ADVISORY, both of them.** `spec-check` keeps the exit code it had; `readiness` prints its line
outside the gates rollup and caps nothing, blocks nothing, and changes no score. A gate here would
need a budget the project adopted, and nobody has declared one (the 2.1.0 posture, ADR-043).
`spec-check --json` carries `agent_origin: {unconfirmed: [...], confirmed: n}`; `readiness --json`
carries the same object when there is anything to report. A tree with no marker anywhere prints
exactly what it printed before.

## Field truth for greenfield — `corpus-run` (ADR-046)

From a live greenfield build: *the parser passed every test the agent wrote, and it was wrong.*
Running the REAL corpus is what exposed it — 96.96 % before the fix, 99.645 % after. In greenfield
every test payload is invented by the agent that writes the code, so a green suite can be silent
about the inputs the world actually produces; and `characterize`/`golden-diff` have no answer
there, because their doctrine is *the old code is the truth* and there is no old code.

The corpus is **JSONL** — one object per line, `input` and `expected` required, `id` optional:

```jsonl
{"id": "inv-2019-03", "input": "...raw payload...", "expected": {"total": 1042, "currency": "EUR"}}
```

Each case runs the command once with its input on **stdin** (verbatim when it is a string,
JSON-encoded otherwise); the trimmed **stdout** is compared to `expected` — string compare, then
JSON-equal when both sides parse as JSON. A non-zero exit is a miss named with its code; a case
past `--timeout` (default 30 s) is a miss named `timeout`. File order is run order, so two runs
over one corpus report the same misses in the same places.

```bash
python3 $QL corpus-run --repo <REPO> --corpus corpus/real.jsonl --command "python -m myparser"   --threshold 99 --ac AC-FIELD-01
#   [qa_ledger] backend-api/gate:corpus: FAIL 96.96 % (3195/3295) — threshold 99 % from --threshold
python3 $QL readiness
#   --- field backend-api: corpus 96.96 % (3195/3295) < 99 % FAIL
```

- **The threshold is yours**: `--threshold`, else `repos[R].corpus_threshold`, else
  `defaults.corpus_threshold`. With **none declared the run is ADVISORY** — measured, persisted,
  gating nothing, and never counted as an `ok` gate (the 2.1.0 posture, ADR-043). With one
  declared, a run under it caps readiness ≤65 and blocks convergence like any fact gate.
- **A corpus that cannot be read is exit 2, naming the line** — missing, empty, malformed, or
  missing an `input`/`expected` key. An unreadable corpus scored as 0 % would be an unmeasurable
  input reported as a measured catastrophe.
- **`--ac AC-nn` closes a criterion MEASURED on a green run**, exactly as a green testcase does; a
  ticked criterion whose only corpus evidence is red or advisory reports `narrated_only`.
- **`log-gate --kind corpus`** records a run measured elsewhere (CI, a nightly) and is the third
  and last kind that accepts `--verdict advisory`, because the check's own default mode is
  advisory. Every other FACT kind still refuses it.
- **No weight.** `readiness` prints one conditional `field` line per repo that declares a corpus
  or ran one, and nothing else moves; the `field` *dimension* is deferred to its own ADR.
- **Never author the corpus yourself.** A corpus the agent invented is the invented input this
  instrument exists to expose.

## The smoke run as measured evidence — `smoke-ingest` (ADR-047)

From the field: *every simulator run returned an empty list, because the database had no rows —
and the smoke was reported as verified.* An empty list is a 200. Phase 7 used to end in a
paragraph — "the jar served /admin", "the simulator answered 200 in 6 ms" — and a paragraph
cannot tell "the endpoint answered correctly" from "the endpoint answered". **Evidence is
executed, not narrated.**

The contract is the smallest thing a shell script can emit — `name` and a **boolean** `ok` are the
whole mandatory surface:

```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
#   [qa_ledger] backend-api/gate:smoke: FAIL — 7/8 checks ok
#     FAIL healthz (status 503)
#     caps readiness <=65 and blocks convergence until a clean smoke
python3 $QL readiness
#   --- smoke backend-api: 7/8 checks ok, 1 failed (healthz) FAIL
```

- **A failed check is a BLOCKER**, through the same record `gate-check` writes: readiness ≤65,
  convergence blocked, cleared by a later clean report.
- **A report the engine cannot read is exit 2, naming the check or the field** — missing,
  malformed, no `checks` key, a check with no `name` or no boolean `ok`. An **EMPTY** `checks`
  list is refused too: a run that verified nothing is not a clean gate.
- **`smoke` is a FACT kind and never runs advisory.** `corpus` may (a percentage needs an adopted
  budget); `ok` is binary, so an advisory smoke would be a mandatory gate cleared by goodwill.
  `log-gate --kind smoke --verdict pass|fail|not-run` is the parity door.
- **A check named `AC-nn ...` closes that criterion MEASURED** when it is `ok` and its report
  passed — the same tag grammar a JUnit testcase name uses. A **failed** tagged check VETOES,
  exactly like a red test, and `readiness --json` reports both halves
  (`acceptance.smoke_closed`, `acceptance.smoke_vetoed`).
- **No weight.** One conditional `smoke` line per repo that ingested a report, and nothing else
  moves.
- **`templates/scripts/smoke-report-example.json`** is the reference report (three checks, one
  tagged `AC-01`). Never narrate the report: a checklist a human ticked is evidence, a checklist
  an agent wrote is not.

## Ledger subcommands

`bench - bench-curate - bench-r2 - bench-roundtrip - bootstrap-oracle - bootstrap-variance - check-coverage - check-terminado - cleanroom - compile-ingest - compile-validate - converged - corpus-run - curate - curation-check - dashboard - discover - doctor - escalate - execution-policy - facts - fastpath-eval - fidelity - flag-blocker - gate-check - golden-coverage - golden-diff - ingest-gate - init - ir-extract - ir-render - lang-compare - log-gate - log-step - operability - oscillation - phase - pit-check - production-finding - promote - readiness - rebuild - regression-check - resolve-escalation - roundtrip - rubric-ingest - simplicity-check - smoke-ingest - snapshot - spec-change-request - spec-check - spec-doubt - spec-drift - summary - top - waste-check` - the exact current `qa_ledger.py` parser surface (56 subcommands, derived from `SYSTEM-FACTS.json`, itself introspected from `build_parser()`); each supports `--help`.

`gate-check --repo R` SCOPES the diff to `repos[R].path` (2.2.0): in a monorepo one `git diff`
carries every repo's hunks, and a sibling's findings are neither this repo's report nor this
repo's exit code. A rename is reported under `moved` — informational, never a deletion — both
from git's own rename headers (`--from-git` forces `-M`, so the caller's `diff.renames` config
cannot hide one) and from an exact delete/add pair in a diff whose producer detected none. A
real deletion still blocks.

The **fact gates** (golden-diff, gate-check, pit-check, simplicity, **ci**) are PERSISTED with
`log-gate`: a fail blocks convergence and caps readiness ≤65 via the ledger; `--verdict
advisory` records a run that is measured but not gating (it caps nothing, blocks nothing, and
never counts as an `ok` gate) and is accepted ONLY for `simplicity|waste` — every FACT gate
refuses it. `--kind ci` (2.2.0) records what the pipeline did, with `--ref <run URL or id>`
stored on the record as the receipt; a `ci` fail caps and blocks exactly like `gate-check`. A CONSTITUTION violation is recorded with `flag-blocker` (same
effect, until `--resolve`).

## Notes

- **It doesn't merge on its own.** It creates the PR and stops; the merge is yours.
- **ADR experiments are visible hypotheses.** `Status: Experiment` requires Hypothesis, Feedback Signal, Review By/Trigger, Promote Criteria and Rollback/Supersede Criteria. Missing or expired metadata is shown by `dashboard --json`/Mirador as advisory, not as a hard PR gate.
- **Tracked `.md` protocol.** Before touching CLAUDE.md / plan/delta docs / docs/adr,
  the skill asks for the current version of the file (it doesn't regenerate and overwrite real progress).
- `ingest-gate` credits a fix only if the report EXISTS and came back clean; a missing
  report = the gate didn't run (it doesn't invent zeros).

## Setting up the workbench (generic setup)

Before using the skills you need the base toolchain (Claude Code + Python + git/gh +
the skills installed). It's all in **`WORKBENCH.md`**: installation, verification and
update, without the specifics of each stack (Java/MSSQL/linters = per-repo adapter).

- What I have installed:  `bash workbench-doctor.sh`
- Kit version:      `python uscha-kit/install-uscha.py version` or `cat VERSION`

## Templates for the repo (so the repo becomes "methodology-ready")

The kit installs the skills; these templates leave the **repo** ready. Copy them to the root
of the repo where you're going to work:

```
cp uscha-kit/templates/CLAUDE.md        <repo>/CLAUDE.md        # permanent repo protocol
cp uscha-kit/templates/CONSTITUTION.md  <repo>/CONSTITUTION.md  # inviolable invariants (fill in the domain)
cp -r uscha-kit/templates/docs    <repo>/docs           # docs/adr scaffold
# if you use other agents besides Claude Code:  cp <repo>/CLAUDE.md <repo>/AGENTS.md
```

Then, complete the "Project adapter" block of the `CLAUDE.md` with the build/test/gate
commands of that stack (it's the only project-specific thing).

## Verify the agent reads the kit

Inside Claude Code, ask it:

```
List the active rules from CLAUDE.md and the available skills.
```

The protocol rules and the commands `/uscha-discovery`, `/uscha-adr-refine`,
`/uscha-devloop`, and `/uscha-sysdoc` should appear. (For the machine's toolchain: `bash uscha-kit/workbench-doctor.sh`.)
