@andresmassello/uscha 2.2.0 → 2.4.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 +8 -2
  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 +30 -8
  7. package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +17 -2
  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 +30 -8
  22. package/uscha-kit/skills/uscha-discovery/SKILL.md +17 -2
  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.2.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
48
+ **Kit v2.4.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
 
@@ -218,6 +219,11 @@ that same-model reruns differ structurally about as much as different models do
218
219
  `NOISY`) — so one earlier variance narrative was **retracted**. Every claim above is a subcommand
219
220
  you can run; every unmeasured part is labeled. That honesty is the method applied to itself.
220
221
 
222
+ Every verdict above is tied to a named criterion, and every one a human judged is tied to the
223
+ person who signed it — the ledger itself, `bench-curate --human` for the compiled artifacts, and
224
+ the `origin: agent` markers that record which specification items the agent proposed versus the
225
+ human decided.
226
+
221
227
  → The full thesis, with before/after diagrams and the REAL vs VISION vs REJECTED table:
222
228
  **[uscha.dev/diamond](https://uscha.dev/diamond)** · the mechanism, in three diagrams:
223
229
  **[uscha.dev/how](https://uscha.dev/how)**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresmassello/uscha",
3
- "version": "2.2.0",
3
+ "version": "2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
30
+ <!-- uscha kit: 2.4.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
 
@@ -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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.4.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>]`
@@ -112,6 +120,12 @@ Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`,
112
120
  4. **Grill, don't agree.** Surface contradictions, fuzzy/overloaded terms, missing
113
121
  failure modes and unstated constraints. A discovery where you agreed with everything
114
122
  failed.
123
+ Two habits, both advisory (unmeasured, kept because they are cheap): **every magnitude
124
+ carries a number or a range** -- "many tasks", "fast", "large files" are not scope;
125
+ "20 to 100 tasks", "under 200 ms", "up to 50 MB" are -- and **rules are written as
126
+ constraints, not wishes**: "no partial implementations, no TODO left in the diff" is
127
+ checkable; "remember to finish things" is not. A magnitude without a number and a rule
128
+ without a boundary are questions you still owe the human.
115
129
  5. **Write files lazily and inline.** Create a file only when you have something real to
116
130
  write, and update it the moment a decision crystallizes — don't batch to the end.
117
131
  6. **Mark what YOU decided: `origin: agent`.** Any acceptance criterion, ADR decision item
@@ -238,7 +252,8 @@ answer.
238
252
  you proposed and the human approved). Distinct from the glossary: this is the model, not
239
253
  the vocabulary.
240
254
  - **`SPEC.md`** — objective/value, risk, scope/out-of-scope, behavior,
241
- inputs/outputs/errors, acceptance, test plan, operation, rollback.
255
+ inputs/outputs/errors, acceptance, test plan, operation, rollback. Magnitudes with numbers
256
+ or ranges; rules as constraints with a boundary, never as reminders.
242
257
  - **`docs/adr/ADR-NNN-<slug>.md`** — one per durable decision. Format: Status
243
258
  (proposed/accepted/**experiment**/deprecated/superseded) · Context · Alternatives · Decision ·
244
259
  Consequences · **Implementation Plan** (affected paths, patterns to follow, tests to
@@ -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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
21
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
27
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
25
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
61
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
25
+ <!-- uscha kit: 2.4.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.2.0",
4
+ "version": "2.4.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.2.0",
3
+ "version": "2.4.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.2.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v2.4.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.2.0
1
+ uscha-kit 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
30
+ <!-- uscha kit: 2.4.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
 
@@ -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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
22
+ <!-- uscha kit: 2.4.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>]`
@@ -112,6 +120,12 @@ Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`,
112
120
  4. **Grill, don't agree.** Surface contradictions, fuzzy/overloaded terms, missing
113
121
  failure modes and unstated constraints. A discovery where you agreed with everything
114
122
  failed.
123
+ Two habits, both advisory (unmeasured, kept because they are cheap): **every magnitude
124
+ carries a number or a range** -- "many tasks", "fast", "large files" are not scope;
125
+ "20 to 100 tasks", "under 200 ms", "up to 50 MB" are -- and **rules are written as
126
+ constraints, not wishes**: "no partial implementations, no TODO left in the diff" is
127
+ checkable; "remember to finish things" is not. A magnitude without a number and a rule
128
+ without a boundary are questions you still owe the human.
115
129
  5. **Write files lazily and inline.** Create a file only when you have something real to
116
130
  write, and update it the moment a decision crystallizes — don't batch to the end.
117
131
  6. **Mark what YOU decided: `origin: agent`.** Any acceptance criterion, ADR decision item
@@ -238,7 +252,8 @@ answer.
238
252
  you proposed and the human approved). Distinct from the glossary: this is the model, not
239
253
  the vocabulary.
240
254
  - **`SPEC.md`** — objective/value, risk, scope/out-of-scope, behavior,
241
- inputs/outputs/errors, acceptance, test plan, operation, rollback.
255
+ inputs/outputs/errors, acceptance, test plan, operation, rollback. Magnitudes with numbers
256
+ or ranges; rules as constraints with a boundary, never as reminders.
242
257
  - **`docs/adr/ADR-NNN-<slug>.md`** — one per durable decision. Format: Status
243
258
  (proposed/accepted/**experiment**/deprecated/superseded) · Context · Alternatives · Decision ·
244
259
  Consequences · **Implementation Plan** (affected paths, patterns to follow, tests to
@@ -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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
21
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
27
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
25
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
61
+ <!-- uscha kit: 2.4.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.2.0 -- generated region: edit tools/skill-blocks/, then run `python tools/gen-skill-blocks.py` (never this block by hand) -->
25
+ <!-- uscha kit: 2.4.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.2.0",
3
+ "version": "2.4.0",
4
4
  "project": null,
5
5
  "defaults": {
6
6
  "coverage_threshold": 60,