@andresmassello/uscha 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +5 -4
  2. package/package.json +1 -1
  3. package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +9 -1
  4. package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +9 -1
  5. package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +41 -5
  6. package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +33 -8
  7. package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +9 -1
  8. package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +5 -1
  9. package/uscha-kit/.claude/skills/uscha-reverse-discovery/SKILL.md +9 -1
  10. package/uscha-kit/.claude/skills/uscha-rubric/SKILL.md +9 -1
  11. package/uscha-kit/.claude/skills/uscha-status/SKILL.md +5 -1
  12. package/uscha-kit/.claude/skills/uscha-sysdoc/SKILL.md +9 -1
  13. package/uscha-kit/.claude-plugin/plugin.json +1 -1
  14. package/uscha-kit/.codex-plugin/plugin.json +1 -1
  15. package/uscha-kit/README.md +6 -1
  16. package/uscha-kit/VERSION +1 -1
  17. package/uscha-kit/install-uscha.py +52 -0
  18. package/uscha-kit/skills/uscha-adr-refine/SKILL.md +9 -1
  19. package/uscha-kit/skills/uscha-characterize/SKILL.md +9 -1
  20. package/uscha-kit/skills/uscha-devloop/SKILL.md +41 -5
  21. package/uscha-kit/skills/uscha-devloop/qa_ledger.py +33 -8
  22. package/uscha-kit/skills/uscha-discovery/SKILL.md +9 -1
  23. package/uscha-kit/skills/uscha-mirador/SKILL.md +5 -1
  24. package/uscha-kit/skills/uscha-reverse-discovery/SKILL.md +9 -1
  25. package/uscha-kit/skills/uscha-rubric/SKILL.md +9 -1
  26. package/uscha-kit/skills/uscha-status/SKILL.md +5 -1
  27. package/uscha-kit/skills/uscha-sysdoc/SKILL.md +9 -1
  28. package/uscha-kit/uscha.config.json +1 -1
package/README.md CHANGED
@@ -42,9 +42,10 @@ npx --yes @andresmassello/uscha@latest init
42
42
 
43
43
  Requires **Python 3.8+** on the machine (the engine is Python stdlib — no pip installs, no
44
44
  runtime dependencies). The npm package is a thin router; the canonical installer is
45
- `uscha-kit/install-uscha.py`.
45
+ `uscha-kit/install-uscha.py`. `init` also writes a minimal, per-repo-type `.gitignore` when the
46
+ project has none (kit 2.4.0) — it never lists `reports/`, the ledger's own evidence.
46
47
 
47
- **Kit v2.3.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
48
+ **Kit v2.5.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
48
49
  [changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG.md)
49
50
  (the per-release changelogs live in the repo, not in the npm tarball)
50
51
 
@@ -190,12 +191,12 @@ automatic tool can perform: a human verdict.
190
191
  |---|---|---|
191
192
  | Asset → typed graph | `ir-extract`, `ir-render` | the whole package becomes one canonical IR (M2, ADR-015) — deterministic, `UNTYPED` is a measurement not an error |
192
193
  | Forward, the compiler | `compile-validate`, `compile-ingest` | any model produces code; the engine validates the output contract and never compiles (M3, ADR-016) |
193
- | Forward, is it the *same* system? | `bootstrap-oracle`, `bootstrap-variance`, `bench` | a withheld oracle judges blind compilations — **12 archetypes, 8 PASS · 4 PARTIAL**, four blind compilers across two vendors (Haiku · Sonnet · Opus · OpenAI Codex `gpt-5.5`), JS included (M4/M5, ADR-017/018/028/029/042) |
194
+ | Forward, is it the *same* system? | `bootstrap-oracle`, `bootstrap-variance`, `bench` | a withheld oracle judges blind compilations — **12 archetypes, 8 PASS · 4 PARTIAL (measured September 2026)**, four blind compilers across two vendors (Haiku · Sonnet · Opus · OpenAI Codex `gpt-5.5`), JS included (M4/M5, ADR-017/018/028/029/042) |
194
195
  | Reverse, facts | `discover`, `golden-diff` (+ the `/uscha-characterize` skill) | system map + mechanically captured golden; typed candidate observations with evidence class (M1, ADR-013) |
195
196
  | Reverse, the human gate | `curate`, `promote`, `curation-check`, `bench-curate` | one verdict per candidate, append-only ledger verified against git; unjudged → `pr-ready` blocked naming it (ADR-009/010, INV-CURATION-01) |
196
197
  | Fidelity, honestly | `fidelity`, `roundtrip`, `bench-roundtrip`, `bench-r2` | per-compiler fidelity vector, id-level round trip, recoverability **0.815**, and the **noise floor** under every variance claim (ADR-014/022/027/030) |
197
198
 
198
- **Read the numbers the way the repo does.** 8 of 12 archetypes regenerate to the same system
199
+ **Read the numbers the way the repo does.** 8 of 12 archetypes (measured September 2026; four blind compilers — Haiku, Sonnet, Opus and OpenAI Codex gpt-5.5) regenerate to the same system
199
200
  under an oracle the compilers never saw — that is the closed loop working. It was 9 of 12 until
200
201
  1.99.0, when a fourth compiler from a second vendor read one genuinely ambiguous sentence in
201
202
  `transformer` the other way and lost a case the three Claude-family models had agreed on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresmassello/uscha",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Spec-driven development for LLM coding agents: 9 skills + a stdlib evidence engine. Facts block, guesses advise; the human approves.",
5
5
  "author": {
6
6
  "name": "Andres Massello",
@@ -19,7 +19,7 @@ phases. **You are NOT a generator. You are an interrogator that distills.** The
19
19
  is in the questions, not in agreeing.
20
20
 
21
21
  <!-- uscha:orientation-block:begin -->
22
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
23
23
 
24
24
  ## First contact (show ONCE, then never again)
25
25
 
@@ -54,6 +54,14 @@ block onward, derived state wins.
54
54
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
55
55
  They are navigation, not ceremony: one line per turn, one block at the end.
56
56
 
57
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
58
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
59
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
60
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
61
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
62
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
63
+ below.
64
+
57
65
  **Open every turn with a breadcrumb**, then the content:
58
66
 
59
67
  `[uscha · adr-refine · <step> → <target>]`
@@ -19,7 +19,7 @@ what the code DOES, mechanically, by running it — never what it should do.** Y
19
19
  the capture harness; you may NOT create, rename, or edit any `.approved` file.
20
20
 
21
21
  <!-- uscha:orientation-block:begin -->
22
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
23
23
 
24
24
  ## First contact (show ONCE, then never again)
25
25
 
@@ -54,6 +54,14 @@ block onward, derived state wins.
54
54
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
55
55
  They are navigation, not ceremony: one line per turn, one block at the end.
56
56
 
57
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
58
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
59
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
60
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
61
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
62
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
63
+ below.
64
+
57
65
  **Open every turn with a breadcrumb**, then the content:
58
66
 
59
67
  `[uscha · characterize · <step> → <target>]`
@@ -27,7 +27,7 @@ artifacts; these can block) and **self-reported** agent counts (log-step — nar
27
27
  recorded for the retrospective; a measured red always overrides a narrated green).
28
28
 
29
29
  <!-- uscha:orientation-block:begin -->
30
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
30
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
31
31
 
32
32
  ## First contact (show ONCE, then never again)
33
33
 
@@ -62,6 +62,14 @@ block onward, derived state wins.
62
62
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
63
63
  They are navigation, not ceremony: one line per turn, one block at the end.
64
64
 
65
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
66
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
67
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
68
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
69
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
70
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
71
+ below.
72
+
65
73
  **Open every turn with a breadcrumb**, then the content:
66
74
 
67
75
  `[uscha · devloop · <step> → <target>]`
@@ -239,15 +247,22 @@ For each repo, decide whether a safety net exists before any refactoring:
239
247
 
240
248
  ```bash
241
249
  python3 $QL snapshot --repo <REPO> --phase pre
242
- python3 $QL check-coverage --repo <REPO> # exit 0 = OK, exit 1 = BELOW threshold
250
+ python3 $QL check-coverage --repo <REPO>
251
+ # exit 0 = OK, exit 1 = BELOW threshold (a real report), exit 2 = UNMEASURED (no report)
243
252
  ```
244
253
 
245
254
  - **Coverage >= threshold:** the existing suite is the guardrail. Skip to Phase 2.
246
- - **Coverage < threshold (or no report):** write **characterization / contract tests
247
- at the boundary** (public API, endpoints, input→output behavior) — NOT internals.
255
+ - **Coverage < threshold (a real report, exit 1):** write **characterization / contract
256
+ tests at the boundary** (public API, endpoints, input→output behavior) — NOT internals.
248
257
  These must survive refactoring. The ADR acceptance criteria are the spec for these.
249
258
  **Have the human review these tests before trusting them as a gate** — a test that
250
- passes for the wrong reason poisons the whole loop.
259
+ passes for the wrong reason poisons the whole loop. Characterization tests are for
260
+ EXISTING code nobody has tested yet, not for code that has not been run at all.
261
+ - **UNMEASURED (exit 2, no report found):** on a greenfield repo this means the test
262
+ command has never been run with coverage — not that the code failed a test. Produce
263
+ the report first: run the repo's test command with coverage, then re-run
264
+ `check-coverage`. Only if a real report then shows coverage below threshold does the
265
+ characterization path above apply.
251
266
  - **Migration/legacy (profile E): capture the golden BEFORE touching anything.** Run
252
267
  the `uscha-characterize` skill (or `uscha-reverse-discovery` for a whole-system map first): it
253
268
  executes the ORIGINAL code against a real input corpus, emits `.received` fixtures,
@@ -521,6 +536,13 @@ python3 $QL snapshot --repo <REPO> --phase post # for every repo + integration
521
536
 
522
537
  Full suite must be green and coverage at/above threshold before proceeding.
523
538
 
539
+ **`reports/` is EVIDENCE, not build noise (kit 2.4.0).** JUnit XML, coverage reports
540
+ (JaCoCo/Cobertura/lcov/go cover), and `reports/smoke.json` are what `snapshot`,
541
+ `check-coverage` and `smoke-ingest` read to turn "we ran tests" into a measured fact.
542
+ Keep it, commit it, and never delete or `.gitignore` it to get a clean-looking commit —
543
+ a repo with no `reports/` reads UNMEASURED, not "clean". (`uscha init`'s generated
544
+ `.gitignore`, kit 2.4.0, never lists `reports/` for exactly this reason.)
545
+
524
546
  ## Phase 5b — Rebuild test (optional; risk profile C+/E or periodic CI)
525
547
 
526
548
  Completeness of the SPEC, not correctness of the build: is the spec package enough to
@@ -549,6 +571,10 @@ is a spec gap, not a code bug.
549
571
  python3 $QL phase --repo <REPO> --require pr-ready # exit 1 = the facts say no
550
572
  ```
551
573
 
574
+ `pr-ready` is a PHASE VALUE, not a subcommand — there is no `qa_ledger.py pr-ready`.
575
+ It is always read through `phase --repo <REPO> --require pr-ready` as shown above
576
+ (`qa_ledger.py pr-ready` alone is an argparse error).
577
+
552
578
  The state is COMPUTED from the ledger (converged + green tests + zero
553
579
  BLOCKER/CRITICAL + no open escalation), never self-declared — if it exits 1, the
554
580
  output lists exactly which facts are missing; do NOT open the PR, close the gap.
@@ -664,6 +690,16 @@ count, plateau flag per repo — to `ledger["measured"]` (what the statusline re
664
690
  Without it the trail starves: the mirador shows "no history yet" and the statusline
665
691
  falls back to counting checkboxes. Recording is append-only facts, never a gate.
666
692
 
693
+ **Tick measured-but-unchecked boxes before closing (kit 2.4.0).** `readiness` reports
694
+ `measured_unchecked` (`--json`) / a `· measured but unticked: AC-...` line (default view):
695
+ AC-IDs the ledger already closed with a green, name-tagged test but whose `ACCEPTANCE.md`
696
+ checkbox is still `[ ]`. For every ID it lists, flip that box to `[x]` in `ACCEPTANCE.md` —
697
+ the engine measured it; ticking is bookkeeping, never the other way round. Then re-run
698
+ `readiness` and confirm the list is empty before the close block. This is ONE-DIRECTIONAL:
699
+ never tick a box the ledger has not closed (that would be narrating, and `readiness`
700
+ already reports narrated-but-unmeasured boxes separately as `narrated_only`) — a tick
701
+ without a green test stays narrated-only, not measured.
702
+
667
703
  Right after readiness, run the spec-maintenance advisory (kit 1.66.0):
668
704
 
669
705
  ```bash
@@ -19,7 +19,8 @@ Stdlib only. Python 3.8+.
19
19
  Usage (see `--help` on each subcommand):
20
20
  qa_ledger.py init --config uscha.config.json [--out QA-LEDGER.json]
21
21
  qa_ledger.py snapshot --repo backend-api [--phase pre|post]
22
- qa_ledger.py check-coverage --repo backend-api [--threshold 60] # exit 0 >=, 1 <
22
+ qa_ledger.py check-coverage --repo backend-api [--threshold 60]
23
+ # exit 0 >= threshold, 1 < threshold, 2 UNMEASURED (no report)
23
24
  qa_ledger.py log-step --repo backend-api --tool code-review --iteration 1 \
24
25
  --reported 12 --gated-reported 4 --fixed 9 \
25
26
  --deferred 2 --suppressed 1 --tests-passed true \
@@ -399,6 +400,24 @@ def coverage(repo_path, repo_type):
399
400
  return flutter_coverage(repo_path)
400
401
 
401
402
 
403
+ _COVERAGE_REPORT_HINT = {
404
+ "ant": "target/site/jacoco/jacoco.xml (or -aggregate)",
405
+ "maven": "target/site/jacoco/jacoco.xml (or -aggregate)",
406
+ "gradle": "target/site/jacoco/jacoco.xml (or -aggregate)",
407
+ "python": "coverage.xml or reports/coverage.xml (Cobertura)",
408
+ "rust": "coverage.xml or reports/coverage.xml (Cobertura)",
409
+ "dotnet": "coverage.xml or reports/coverage.xml (Cobertura)",
410
+ "cpp": "coverage.xml or reports/coverage.xml (Cobertura)",
411
+ "go": "coverage.out, cover.out or reports/coverage.out",
412
+ }
413
+
414
+
415
+ def _coverage_report_hint(repo_type):
416
+ """Human-readable hint of where `coverage()` looked, for an UNMEASURED message --
417
+ never invents a single path, since several report types glob multiple locations."""
418
+ return _COVERAGE_REPORT_HINT.get(repo_type, "coverage/lcov.info")
419
+
420
+
402
421
  # --------------------------------------------------------------------------- #
403
422
  # measurement: test counts
404
423
  # --------------------------------------------------------------------------- #
@@ -2404,15 +2423,18 @@ def cmd_check_coverage(args):
2404
2423
  threshold = ledger["config"].get("defaults", {}).get("coverage_threshold", 60)
2405
2424
  cov = coverage(cfg["path"], cfg["type"])
2406
2425
  pct = cov["pct"]
2426
+ if not cov["report_found"]:
2427
+ # No coverage report at all is UNMEASURED, not a below-threshold verdict: on a
2428
+ # greenfield repo pct 0.0 + report_found False means no report was ever produced,
2429
+ # never that the code was tested and failed. Fail closed (exit 2) -- distinct from
2430
+ # exit 1 (a real report that reads below threshold) -- so callers do not confuse
2431
+ # "never measured" with "measured and failing".
2432
+ print(f"[qa_ledger] {args.repo}: coverage UNMEASURED -- no coverage report found "
2433
+ f"(looked for {_coverage_report_hint(cfg['type'])}). "
2434
+ f"Run the test command with coverage first to produce a report.")
2435
+ sys.exit(2)
2407
2436
  below = pct < threshold
2408
2437
  state = "BELOW" if below else "OK"
2409
- if not cov["report_found"]:
2410
- # No coverage report at all == treat as below threshold (needs char tests),
2411
- # but flag loudly so the skill knows tests simply haven't been run yet.
2412
- print(f"[qa_ledger] {args.repo}: NO coverage report found "
2413
- f"(threshold {threshold}%) -> treat as BELOW. "
2414
- f"Run the test command first if a report was expected.")
2415
- sys.exit(1)
2416
2438
  print(f"[qa_ledger] {args.repo}: coverage {pct}% vs threshold {threshold}% -> {state}")
2417
2439
  sys.exit(1 if below else 0)
2418
2440
 
@@ -6565,6 +6587,9 @@ COMPILE_REQUIRED = ("schema_version", "canonical_ir", "target_stack", "source",
6565
6587
  # The seal covers the load-bearing contract, NOT compilation_report: model, versions and
6566
6588
  # timestamps legitimately vary and never change WHAT was compiled. A hand edit of the
6567
6589
  # substance (source/tests/manifest/unresolved_intent) after production must trip the seal.
6590
+ # A future compiler script SHOULD populate compilation_report.model_version with the exact
6591
+ # resolved model id (as bench-compile-codex.py does with "gpt-5.5 via codex-cli ..."), never
6592
+ # a bare alias -- provenance the seal deliberately does not enforce (ADR-042 item 8).
6568
6593
  COMPILE_SEALED = ("schema_version", "canonical_ir", "target_stack",
6569
6594
  "implementation_constraints", "source", "tests",
6570
6595
  "trace_manifest", "unresolved_intent")
@@ -19,7 +19,7 @@ shape.** Your job is to interrogate until there is a shared system shape, and to
19
19
  the documents as you go — not to ask the human to design the system for you.
20
20
 
21
21
  <!-- uscha:orientation-block:begin -->
22
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
23
23
 
24
24
  ## First contact (show ONCE, then never again)
25
25
 
@@ -54,6 +54,14 @@ block onward, derived state wins.
54
54
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
55
55
  They are navigation, not ceremony: one line per turn, one block at the end.
56
56
 
57
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
58
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
59
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
60
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
61
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
62
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
63
+ below.
64
+
57
65
  **Open every turn with a breadcrumb**, then the content:
58
66
 
59
67
  `[uscha · discovery · <step> → <target>]`
@@ -18,7 +18,7 @@ Paints the REAL state of the project at a glance. It does not narrate or estimat
18
18
  wires the JSON the engine emits into the template. Read-only.
19
19
 
20
20
  <!-- uscha:orientation-block:begin -->
21
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
21
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
22
 
23
23
  ## Orientation markers (non-negotiable)
24
24
 
@@ -28,6 +28,10 @@ This skill is a **one-shot read-only readout**: its block IS the answer. It ther
28
28
  take the conversational close block — that would be exactly the padding this skill forbids.
29
29
  It carries the two minimal markers instead.
30
30
 
31
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
32
+ statusline — Codex, pi, a plain terminal — this readout IS the operator's only signal, so it
33
+ MUST appear in the visible reply text, never only inside a collapsed tool-call log.
34
+
31
35
  **Open with a breadcrumb:**
32
36
 
33
37
  `[uscha · mirador · step <n> → <target>]`
@@ -24,7 +24,7 @@ evidence-classed, content-addressed, and promoted to the contract only by a per-
24
24
  human verdict (ADR-013).**
25
25
 
26
26
  <!-- uscha:orientation-block:begin -->
27
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
27
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
28
28
 
29
29
  ## First contact (show ONCE, then never again)
30
30
 
@@ -61,6 +61,14 @@ block onward, derived state wins.
61
61
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
62
62
  They are navigation, not ceremony: one line per turn, one block at the end.
63
63
 
64
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
65
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
66
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
67
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
68
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
69
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
70
+ below.
71
+
64
72
  **Open every turn with a breadcrumb**, then the content:
65
73
 
66
74
  `[uscha · reverse-discovery · <step> → <target>]`
@@ -22,7 +22,7 @@ the grader — this skill just wraps the neutral prompt so Claude Code users get
22
22
  in one command. Never add Claude-specific behavior to the contract.
23
23
 
24
24
  <!-- uscha:orientation-block:begin -->
25
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
25
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
26
26
 
27
27
  ## First contact (show ONCE, then never again)
28
28
 
@@ -57,6 +57,14 @@ block onward, derived state wins.
57
57
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
58
58
  They are navigation, not ceremony: one line per turn, one block at the end.
59
59
 
60
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
61
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
62
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
63
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
64
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
65
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
66
+ below.
67
+
60
68
  **Open every turn with a breadcrumb**, then the content:
61
69
 
62
70
  `[uscha · rubric · <step> → <target>]`
@@ -58,7 +58,7 @@ that surface the warning cannot come from the skill itself. The `doctor` seam is
58
58
  that still works there — it runs from any kit checkout and reads the installs from outside.
59
59
 
60
60
  <!-- uscha:orientation-block:begin -->
61
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
61
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
62
62
 
63
63
  ## Orientation markers (non-negotiable)
64
64
 
@@ -68,6 +68,10 @@ This skill is a **one-shot read-only readout**: its block IS the answer. It ther
68
68
  take the conversational close block — that would be exactly the padding this skill forbids.
69
69
  It carries the two minimal markers instead.
70
70
 
71
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
72
+ statusline — Codex, pi, a plain terminal — this readout IS the operator's only signal, so it
73
+ MUST appear in the visible reply text, never only inside a collapsed tool-call log.
74
+
71
75
  **Open with a breadcrumb:**
72
76
 
73
77
  `[uscha · status · step <n> → <target>]`
@@ -22,7 +22,7 @@ switch between at any time:
22
22
  coverage, known deferred issues.
23
23
 
24
24
  <!-- uscha:orientation-block:begin -->
25
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
25
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
26
26
 
27
27
  ## First contact (show ONCE, then never again)
28
28
 
@@ -57,6 +57,14 @@ block onward, derived state wins.
57
57
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
58
58
  They are navigation, not ceremony: one line per turn, one block at the end.
59
59
 
60
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
61
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
62
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
63
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
64
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
65
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
66
+ below.
67
+
60
68
  **Open every turn with a breadcrumb**, then the content:
61
69
 
62
70
  `[uscha · sysdoc · <step> → <target>]`
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "uscha",
4
- "version": "2.3.0",
4
+ "version": "2.5.0",
5
5
  "displayName": "Uscha",
6
6
  "description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 56 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
7
7
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Uscha spec-driven development methodology for coding agents. Includes npm/npx router.",
5
5
  "author": {
6
6
  "name": "Andres Massello",
@@ -1,6 +1,6 @@
1
1
  # uscha-kit
2
2
 
3
- **Kit version:** v2.3.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v2.5.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
4
4
 
5
5
  Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
6
6
  **Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
@@ -532,6 +532,11 @@ of six. Every other knob is absent on purpose and resolves to the engine default
532
532
  `uscha-kit/uscha.config.json` is the **comprehensive reference** — every knob at the kit's
533
533
  value, to read and copy from — and is no longer copied into projects.
534
534
 
535
+ `uscha init` also writes a minimal `.gitignore` (kit 2.4.0), scoped to the detected repo type
536
+ (`__pycache__/`, `*.pyc`, `.pytest_cache/`, `.coverage` for python; `node_modules/` for node; and
537
+ so on) — never `reports/`, which is the ledger's own evidence (JUnit, coverage, smoke). An
538
+ existing `.gitignore` is always left untouched, even with `--force`.
539
+
535
540
  That matters because of one rule: **a knob you declare > the preset named by
536
541
  `defaults.risk_profile` > the engine default**. A copied default is an explicit declaration, so
537
542
  a project holding the whole reference leaves its risk profile nothing to decide.
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 2.3.0
1
+ uscha-kit 2.5.0
@@ -805,6 +805,37 @@ def project_config_bytes(repo):
805
805
  return (json.dumps(config, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
806
806
 
807
807
 
808
+ # Minimal, per repo type (kit 2.4.0). Deliberately tiny: build/cache artifacts a fresh
809
+ # clone regenerates, never source. `reports/` (JUnit, coverage, smoke) is EVIDENCE the
810
+ # ledger reads -- it must NEVER appear here, in this list or any other repo's.
811
+ GITIGNORE_PATTERNS = {
812
+ "python": ("__pycache__/", "*.pyc", ".pytest_cache/", ".coverage"),
813
+ "node": ("node_modules/",),
814
+ "flutter": (".dart_tool/", "build/"),
815
+ "maven": ("target/",),
816
+ "gradle": ("build/", ".gradle/"),
817
+ "rust": ("target/",),
818
+ "swift": (".build/",),
819
+ "cpp": ("build/",),
820
+ "dotnet": ("bin/", "obj/"),
821
+ }
822
+
823
+
824
+ def gitignore_bytes(repo_type):
825
+ """The project's .gitignore, generated for a RECOGNISED repo type only -- like
826
+ `detect_repo_type`, this never guesses. `None` means `init` writes nothing (the
827
+ human declares one by hand). LF explicitly, like every other artifact `init` writes,
828
+ so the same bytes are produced on every run for the same repo type."""
829
+ patterns = GITIGNORE_PATTERNS.get(repo_type)
830
+ if not patterns:
831
+ return None
832
+ lines = ["# Minimal .gitignore, generated by `uscha init` for a %s repo." % repo_type,
833
+ "# reports/ is evidence the ledger reads (JUnit, coverage, smoke) -- never "
834
+ "ignored here."]
835
+ lines.extend(patterns)
836
+ return ("\n".join(lines) + "\n").encode("utf-8")
837
+
838
+
808
839
  def cmd_init(args):
809
840
  repo, operations, conflicts = Path(args.repo).expanduser().resolve(), [], []
810
841
  # (source path or None, payload bytes or None, target): the config is generated, the rest
@@ -845,6 +876,25 @@ def cmd_init(args):
845
876
  target.write_bytes(want)
846
877
  else:
847
878
  shutil.copy2(source, target)
879
+ # .gitignore (kit 2.4.0): written ONLY when the repo has none yet, for a recognised
880
+ # repo type. Unlike the templates above, an EXISTING .gitignore is never a conflict and
881
+ # is never touched even with --force -- it is the human's file, `init` only fills the
882
+ # gap on a repo that has none. That also makes a second `init` report "unchanged".
883
+ gi_target = repo / ".gitignore"
884
+ gi_bytes = gitignore_bytes(detect_repo_type(repo))
885
+ gitignore_wrote = False
886
+ if gi_bytes is not None:
887
+ if gi_target.exists():
888
+ operations.append({"action": "unchanged", "path": str(gi_target),
889
+ "note": "existing .gitignore left untouched"})
890
+ elif args.dry_run:
891
+ operations.append({"action": "copy-file", "path": str(gi_target),
892
+ "source": "<generated>"})
893
+ else:
894
+ gi_target.write_bytes(gi_bytes)
895
+ operations.append({"action": "copy-file", "path": str(gi_target),
896
+ "source": "<generated>"})
897
+ gitignore_wrote = True
848
898
  # wire the statusline (kit 1.46.0): merge statusLine + Stop hook into the project's
849
899
  # settings.json so the user never edits it by hand -- never clobbering existing keys.
850
900
  sl_ops, sl_wrote, sl_conflicts = _wire_statusline_settings(repo, args.force, args.dry_run)
@@ -856,6 +906,8 @@ def cmd_init(args):
856
906
  else:
857
907
  status = "planned" if args.dry_run else "initialized"
858
908
  wrote = [str(t) for _, _, t in copies] if not args.dry_run else []
909
+ if gitignore_wrote:
910
+ wrote.append(str(gi_target))
859
911
  if sl_wrote:
860
912
  wrote.append(str(repo / ".claude" / "settings.json"))
861
913
  emit({"status": status, "dry_run": args.dry_run, "repo": str(repo),
@@ -19,7 +19,7 @@ phases. **You are NOT a generator. You are an interrogator that distills.** The
19
19
  is in the questions, not in agreeing.
20
20
 
21
21
  <!-- uscha:orientation-block:begin -->
22
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
23
23
 
24
24
  ## First contact (show ONCE, then never again)
25
25
 
@@ -54,6 +54,14 @@ block onward, derived state wins.
54
54
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
55
55
  They are navigation, not ceremony: one line per turn, one block at the end.
56
56
 
57
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
58
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
59
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
60
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
61
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
62
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
63
+ below.
64
+
57
65
  **Open every turn with a breadcrumb**, then the content:
58
66
 
59
67
  `[uscha · adr-refine · <step> → <target>]`
@@ -19,7 +19,7 @@ what the code DOES, mechanically, by running it — never what it should do.** Y
19
19
  the capture harness; you may NOT create, rename, or edit any `.approved` file.
20
20
 
21
21
  <!-- uscha:orientation-block:begin -->
22
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
23
23
 
24
24
  ## First contact (show ONCE, then never again)
25
25
 
@@ -54,6 +54,14 @@ block onward, derived state wins.
54
54
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
55
55
  They are navigation, not ceremony: one line per turn, one block at the end.
56
56
 
57
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
58
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
59
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
60
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
61
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
62
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
63
+ below.
64
+
57
65
  **Open every turn with a breadcrumb**, then the content:
58
66
 
59
67
  `[uscha · characterize · <step> → <target>]`
@@ -27,7 +27,7 @@ artifacts; these can block) and **self-reported** agent counts (log-step — nar
27
27
  recorded for the retrospective; a measured red always overrides a narrated green).
28
28
 
29
29
  <!-- uscha:orientation-block:begin -->
30
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
30
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
31
31
 
32
32
  ## First contact (show ONCE, then never again)
33
33
 
@@ -62,6 +62,14 @@ block onward, derived state wins.
62
62
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
63
63
  They are navigation, not ceremony: one line per turn, one block at the end.
64
64
 
65
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
66
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
67
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
68
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
69
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
70
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
71
+ below.
72
+
65
73
  **Open every turn with a breadcrumb**, then the content:
66
74
 
67
75
  `[uscha · devloop · <step> → <target>]`
@@ -239,15 +247,22 @@ For each repo, decide whether a safety net exists before any refactoring:
239
247
 
240
248
  ```bash
241
249
  python3 $QL snapshot --repo <REPO> --phase pre
242
- python3 $QL check-coverage --repo <REPO> # exit 0 = OK, exit 1 = BELOW threshold
250
+ python3 $QL check-coverage --repo <REPO>
251
+ # exit 0 = OK, exit 1 = BELOW threshold (a real report), exit 2 = UNMEASURED (no report)
243
252
  ```
244
253
 
245
254
  - **Coverage >= threshold:** the existing suite is the guardrail. Skip to Phase 2.
246
- - **Coverage < threshold (or no report):** write **characterization / contract tests
247
- at the boundary** (public API, endpoints, input→output behavior) — NOT internals.
255
+ - **Coverage < threshold (a real report, exit 1):** write **characterization / contract
256
+ tests at the boundary** (public API, endpoints, input→output behavior) — NOT internals.
248
257
  These must survive refactoring. The ADR acceptance criteria are the spec for these.
249
258
  **Have the human review these tests before trusting them as a gate** — a test that
250
- passes for the wrong reason poisons the whole loop.
259
+ passes for the wrong reason poisons the whole loop. Characterization tests are for
260
+ EXISTING code nobody has tested yet, not for code that has not been run at all.
261
+ - **UNMEASURED (exit 2, no report found):** on a greenfield repo this means the test
262
+ command has never been run with coverage — not that the code failed a test. Produce
263
+ the report first: run the repo's test command with coverage, then re-run
264
+ `check-coverage`. Only if a real report then shows coverage below threshold does the
265
+ characterization path above apply.
251
266
  - **Migration/legacy (profile E): capture the golden BEFORE touching anything.** Run
252
267
  the `uscha-characterize` skill (or `uscha-reverse-discovery` for a whole-system map first): it
253
268
  executes the ORIGINAL code against a real input corpus, emits `.received` fixtures,
@@ -521,6 +536,13 @@ python3 $QL snapshot --repo <REPO> --phase post # for every repo + integration
521
536
 
522
537
  Full suite must be green and coverage at/above threshold before proceeding.
523
538
 
539
+ **`reports/` is EVIDENCE, not build noise (kit 2.4.0).** JUnit XML, coverage reports
540
+ (JaCoCo/Cobertura/lcov/go cover), and `reports/smoke.json` are what `snapshot`,
541
+ `check-coverage` and `smoke-ingest` read to turn "we ran tests" into a measured fact.
542
+ Keep it, commit it, and never delete or `.gitignore` it to get a clean-looking commit —
543
+ a repo with no `reports/` reads UNMEASURED, not "clean". (`uscha init`'s generated
544
+ `.gitignore`, kit 2.4.0, never lists `reports/` for exactly this reason.)
545
+
524
546
  ## Phase 5b — Rebuild test (optional; risk profile C+/E or periodic CI)
525
547
 
526
548
  Completeness of the SPEC, not correctness of the build: is the spec package enough to
@@ -549,6 +571,10 @@ is a spec gap, not a code bug.
549
571
  python3 $QL phase --repo <REPO> --require pr-ready # exit 1 = the facts say no
550
572
  ```
551
573
 
574
+ `pr-ready` is a PHASE VALUE, not a subcommand — there is no `qa_ledger.py pr-ready`.
575
+ It is always read through `phase --repo <REPO> --require pr-ready` as shown above
576
+ (`qa_ledger.py pr-ready` alone is an argparse error).
577
+
552
578
  The state is COMPUTED from the ledger (converged + green tests + zero
553
579
  BLOCKER/CRITICAL + no open escalation), never self-declared — if it exits 1, the
554
580
  output lists exactly which facts are missing; do NOT open the PR, close the gap.
@@ -664,6 +690,16 @@ count, plateau flag per repo — to `ledger["measured"]` (what the statusline re
664
690
  Without it the trail starves: the mirador shows "no history yet" and the statusline
665
691
  falls back to counting checkboxes. Recording is append-only facts, never a gate.
666
692
 
693
+ **Tick measured-but-unchecked boxes before closing (kit 2.4.0).** `readiness` reports
694
+ `measured_unchecked` (`--json`) / a `· measured but unticked: AC-...` line (default view):
695
+ AC-IDs the ledger already closed with a green, name-tagged test but whose `ACCEPTANCE.md`
696
+ checkbox is still `[ ]`. For every ID it lists, flip that box to `[x]` in `ACCEPTANCE.md` —
697
+ the engine measured it; ticking is bookkeeping, never the other way round. Then re-run
698
+ `readiness` and confirm the list is empty before the close block. This is ONE-DIRECTIONAL:
699
+ never tick a box the ledger has not closed (that would be narrating, and `readiness`
700
+ already reports narrated-but-unmeasured boxes separately as `narrated_only`) — a tick
701
+ without a green test stays narrated-only, not measured.
702
+
667
703
  Right after readiness, run the spec-maintenance advisory (kit 1.66.0):
668
704
 
669
705
  ```bash
@@ -19,7 +19,8 @@ Stdlib only. Python 3.8+.
19
19
  Usage (see `--help` on each subcommand):
20
20
  qa_ledger.py init --config uscha.config.json [--out QA-LEDGER.json]
21
21
  qa_ledger.py snapshot --repo backend-api [--phase pre|post]
22
- qa_ledger.py check-coverage --repo backend-api [--threshold 60] # exit 0 >=, 1 <
22
+ qa_ledger.py check-coverage --repo backend-api [--threshold 60]
23
+ # exit 0 >= threshold, 1 < threshold, 2 UNMEASURED (no report)
23
24
  qa_ledger.py log-step --repo backend-api --tool code-review --iteration 1 \
24
25
  --reported 12 --gated-reported 4 --fixed 9 \
25
26
  --deferred 2 --suppressed 1 --tests-passed true \
@@ -399,6 +400,24 @@ def coverage(repo_path, repo_type):
399
400
  return flutter_coverage(repo_path)
400
401
 
401
402
 
403
+ _COVERAGE_REPORT_HINT = {
404
+ "ant": "target/site/jacoco/jacoco.xml (or -aggregate)",
405
+ "maven": "target/site/jacoco/jacoco.xml (or -aggregate)",
406
+ "gradle": "target/site/jacoco/jacoco.xml (or -aggregate)",
407
+ "python": "coverage.xml or reports/coverage.xml (Cobertura)",
408
+ "rust": "coverage.xml or reports/coverage.xml (Cobertura)",
409
+ "dotnet": "coverage.xml or reports/coverage.xml (Cobertura)",
410
+ "cpp": "coverage.xml or reports/coverage.xml (Cobertura)",
411
+ "go": "coverage.out, cover.out or reports/coverage.out",
412
+ }
413
+
414
+
415
+ def _coverage_report_hint(repo_type):
416
+ """Human-readable hint of where `coverage()` looked, for an UNMEASURED message --
417
+ never invents a single path, since several report types glob multiple locations."""
418
+ return _COVERAGE_REPORT_HINT.get(repo_type, "coverage/lcov.info")
419
+
420
+
402
421
  # --------------------------------------------------------------------------- #
403
422
  # measurement: test counts
404
423
  # --------------------------------------------------------------------------- #
@@ -2404,15 +2423,18 @@ def cmd_check_coverage(args):
2404
2423
  threshold = ledger["config"].get("defaults", {}).get("coverage_threshold", 60)
2405
2424
  cov = coverage(cfg["path"], cfg["type"])
2406
2425
  pct = cov["pct"]
2426
+ if not cov["report_found"]:
2427
+ # No coverage report at all is UNMEASURED, not a below-threshold verdict: on a
2428
+ # greenfield repo pct 0.0 + report_found False means no report was ever produced,
2429
+ # never that the code was tested and failed. Fail closed (exit 2) -- distinct from
2430
+ # exit 1 (a real report that reads below threshold) -- so callers do not confuse
2431
+ # "never measured" with "measured and failing".
2432
+ print(f"[qa_ledger] {args.repo}: coverage UNMEASURED -- no coverage report found "
2433
+ f"(looked for {_coverage_report_hint(cfg['type'])}). "
2434
+ f"Run the test command with coverage first to produce a report.")
2435
+ sys.exit(2)
2407
2436
  below = pct < threshold
2408
2437
  state = "BELOW" if below else "OK"
2409
- if not cov["report_found"]:
2410
- # No coverage report at all == treat as below threshold (needs char tests),
2411
- # but flag loudly so the skill knows tests simply haven't been run yet.
2412
- print(f"[qa_ledger] {args.repo}: NO coverage report found "
2413
- f"(threshold {threshold}%) -> treat as BELOW. "
2414
- f"Run the test command first if a report was expected.")
2415
- sys.exit(1)
2416
2438
  print(f"[qa_ledger] {args.repo}: coverage {pct}% vs threshold {threshold}% -> {state}")
2417
2439
  sys.exit(1 if below else 0)
2418
2440
 
@@ -6565,6 +6587,9 @@ COMPILE_REQUIRED = ("schema_version", "canonical_ir", "target_stack", "source",
6565
6587
  # The seal covers the load-bearing contract, NOT compilation_report: model, versions and
6566
6588
  # timestamps legitimately vary and never change WHAT was compiled. A hand edit of the
6567
6589
  # substance (source/tests/manifest/unresolved_intent) after production must trip the seal.
6590
+ # A future compiler script SHOULD populate compilation_report.model_version with the exact
6591
+ # resolved model id (as bench-compile-codex.py does with "gpt-5.5 via codex-cli ..."), never
6592
+ # a bare alias -- provenance the seal deliberately does not enforce (ADR-042 item 8).
6568
6593
  COMPILE_SEALED = ("schema_version", "canonical_ir", "target_stack",
6569
6594
  "implementation_constraints", "source", "tests",
6570
6595
  "trace_manifest", "unresolved_intent")
@@ -19,7 +19,7 @@ shape.** Your job is to interrogate until there is a shared system shape, and to
19
19
  the documents as you go — not to ask the human to design the system for you.
20
20
 
21
21
  <!-- uscha:orientation-block:begin -->
22
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
23
23
 
24
24
  ## First contact (show ONCE, then never again)
25
25
 
@@ -54,6 +54,14 @@ block onward, derived state wins.
54
54
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
55
55
  They are navigation, not ceremony: one line per turn, one block at the end.
56
56
 
57
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
58
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
59
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
60
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
61
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
62
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
63
+ below.
64
+
57
65
  **Open every turn with a breadcrumb**, then the content:
58
66
 
59
67
  `[uscha · discovery · <step> → <target>]`
@@ -18,7 +18,7 @@ Paints the REAL state of the project at a glance. It does not narrate or estimat
18
18
  wires the JSON the engine emits into the template. Read-only.
19
19
 
20
20
  <!-- uscha:orientation-block:begin -->
21
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
21
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
22
 
23
23
  ## Orientation markers (non-negotiable)
24
24
 
@@ -28,6 +28,10 @@ This skill is a **one-shot read-only readout**: its block IS the answer. It ther
28
28
  take the conversational close block — that would be exactly the padding this skill forbids.
29
29
  It carries the two minimal markers instead.
30
30
 
31
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
32
+ statusline — Codex, pi, a plain terminal — this readout IS the operator's only signal, so it
33
+ MUST appear in the visible reply text, never only inside a collapsed tool-call log.
34
+
31
35
  **Open with a breadcrumb:**
32
36
 
33
37
  `[uscha · mirador · step <n> → <target>]`
@@ -24,7 +24,7 @@ evidence-classed, content-addressed, and promoted to the contract only by a per-
24
24
  human verdict (ADR-013).**
25
25
 
26
26
  <!-- uscha:orientation-block:begin -->
27
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
27
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
28
28
 
29
29
  ## First contact (show ONCE, then never again)
30
30
 
@@ -61,6 +61,14 @@ block onward, derived state wins.
61
61
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
62
62
  They are navigation, not ceremony: one line per turn, one block at the end.
63
63
 
64
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
65
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
66
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
67
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
68
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
69
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
70
+ below.
71
+
64
72
  **Open every turn with a breadcrumb**, then the content:
65
73
 
66
74
  `[uscha · reverse-discovery · <step> → <target>]`
@@ -22,7 +22,7 @@ the grader — this skill just wraps the neutral prompt so Claude Code users get
22
22
  in one command. Never add Claude-specific behavior to the contract.
23
23
 
24
24
  <!-- uscha:orientation-block:begin -->
25
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
25
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
26
26
 
27
27
  ## First contact (show ONCE, then never again)
28
28
 
@@ -57,6 +57,14 @@ block onward, derived state wins.
57
57
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
58
58
  They are navigation, not ceremony: one line per turn, one block at the end.
59
59
 
60
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
61
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
62
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
63
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
64
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
65
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
66
+ below.
67
+
60
68
  **Open every turn with a breadcrumb**, then the content:
61
69
 
62
70
  `[uscha · rubric · <step> → <target>]`
@@ -58,7 +58,7 @@ that surface the warning cannot come from the skill itself. The `doctor` seam is
58
58
  that still works there — it runs from any kit checkout and reads the installs from outside.
59
59
 
60
60
  <!-- uscha:orientation-block:begin -->
61
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
61
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
62
62
 
63
63
  ## Orientation markers (non-negotiable)
64
64
 
@@ -68,6 +68,10 @@ This skill is a **one-shot read-only readout**: its block IS the answer. It ther
68
68
  take the conversational close block — that would be exactly the padding this skill forbids.
69
69
  It carries the two minimal markers instead.
70
70
 
71
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
72
+ statusline — Codex, pi, a plain terminal — this readout IS the operator's only signal, so it
73
+ MUST appear in the visible reply text, never only inside a collapsed tool-call log.
74
+
71
75
  **Open with a breadcrumb:**
72
76
 
73
77
  `[uscha · status · step <n> → <target>]`
@@ -22,7 +22,7 @@ switch between at any time:
22
22
  coverage, known deferred issues.
23
23
 
24
24
  <!-- uscha:orientation-block:begin -->
25
- <!-- uscha kit: 2.3.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
25
+ <!-- uscha kit: 2.5.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
26
26
 
27
27
  ## First contact (show ONCE, then never again)
28
28
 
@@ -57,6 +57,14 @@ block onward, derived state wins.
57
57
  The operator must never have to ask "where am I?" or "what happens now?". Two markers, always.
58
58
  They are navigation, not ceremony: one line per turn, one block at the end.
59
59
 
60
+ **No statusline (kit 2.4.0): the visible reply IS the statusline.** On a surface with no live
61
+ statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
62
+ so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
63
+ operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
64
+ compact `uscha-status` block (derived phase · loop · measured acceptance · next criterion, read
65
+ from the ledger — see the `uscha-status` skill), immediately before the breadcrumb/close marker
66
+ below.
67
+
60
68
  **Open every turn with a breadcrumb**, then the content:
61
69
 
62
70
  `[uscha · sysdoc · <step> → <target>]`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "COMPREHENSIVE REFERENCE, not a project config. Every knob the engine and the skills understand, at the kit's own value, so a human can read what can be declared. `uscha init` does NOT copy this file: it GENERATES a minimal project config, because a copied default is an explicit declaration and an explicit declaration outranks the preset named by defaults.risk_profile (ADR-001, as amended). Copy a block from here into your project only when you mean to override the engine default or the preset. NOTE: no version string may be written into this comment -- the release script requires exactly one occurrence of the version in this file (I3), and a second one refuses the next release.",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "project": null,
5
5
  "defaults": {
6
6
  "coverage_threshold": 60,