@andresmassello/uscha 2.0.0 → 2.2.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 (32) hide show
  1. package/README.md +21 -4
  2. package/package.json +1 -1
  3. package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +2 -0
  4. package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +2 -0
  5. package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +171 -15
  6. package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +2089 -97
  7. package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +58 -4
  8. package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +2 -0
  9. package/uscha-kit/.claude/skills/uscha-reverse-discovery/SKILL.md +2 -0
  10. package/uscha-kit/.claude/skills/uscha-rubric/SKILL.md +2 -0
  11. package/uscha-kit/.claude/skills/uscha-status/SKILL.md +40 -0
  12. package/uscha-kit/.claude/skills/uscha-sysdoc/SKILL.md +2 -0
  13. package/uscha-kit/.claude-plugin/plugin.json +2 -2
  14. package/uscha-kit/.codex-plugin/plugin.json +1 -1
  15. package/uscha-kit/README.md +249 -11
  16. package/uscha-kit/VERSION +1 -1
  17. package/uscha-kit/install-uscha.py +18 -2
  18. package/uscha-kit/skills/uscha-adr-refine/SKILL.md +2 -0
  19. package/uscha-kit/skills/uscha-characterize/SKILL.md +2 -0
  20. package/uscha-kit/skills/uscha-devloop/SKILL.md +171 -15
  21. package/uscha-kit/skills/uscha-devloop/qa_ledger.py +2089 -97
  22. package/uscha-kit/skills/uscha-discovery/SKILL.md +58 -4
  23. package/uscha-kit/skills/uscha-mirador/SKILL.md +2 -0
  24. package/uscha-kit/skills/uscha-reverse-discovery/SKILL.md +2 -0
  25. package/uscha-kit/skills/uscha-rubric/SKILL.md +2 -0
  26. package/uscha-kit/skills/uscha-status/SKILL.md +40 -0
  27. package/uscha-kit/skills/uscha-sysdoc/SKILL.md +2 -0
  28. package/uscha-kit/templates/CLAUDE.md +16 -0
  29. package/uscha-kit/templates/CONSTITUTION.md +55 -3
  30. package/uscha-kit/templates/docs/adr/README.md +15 -0
  31. package/uscha-kit/templates/scripts/smoke-report-example.json +24 -0
  32. package/uscha-kit/uscha.config.json +8 -2
@@ -19,6 +19,8 @@ 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) -->
23
+
22
24
  ## First contact (show ONCE, then never again)
23
25
 
24
26
  **Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
@@ -94,9 +96,16 @@ Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`,
94
96
  that makes discovery work: you propose (entities, endpoints, architecture, a default
95
97
  decision), the human confirms or corrects. Never dump a list of 20 questions, and
96
98
  never ask the human to supply structure you can propose yourself.
97
- 2. **Explore instead of asking.** If a reference doc/URL/PDF, the existing codebase, or
98
- an existing `CONTEXT.md`/`docs/adr/` can answer a question, read it first. Only ask
99
- the human what genuinely requires their judgment.
99
+ **Close every question round item by item.** Before moving on, list what YOU decided in
100
+ that round and ask the human to confirm each one SEPARATELY — an "ok" over a twenty-item
101
+ summary is a rebound, not a decision, and it is exactly how a global default became a
102
+ tree-wide rename nobody had asked for. Write the answer onto the item as it arrives
103
+ (principle 6).
104
+ 2. **Explore instead of asking — ask for DECISIONS, never for INFORMATION** (ADR-044). If a
105
+ reference doc/URL/PDF, the existing codebase, or an existing `CONTEXT.md`/`docs/adr/`
106
+ can answer a question, read it first. The current version of a tracked file, what a
107
+ config says, whether a test exists: all information — READ it. Only ask the human what
108
+ genuinely requires their judgment.
100
109
  3. **Propose the shape.** From the idea + references, propose the core entities, the
101
110
  operation/API surface, and 2–3 architecture options with trade-offs. Walk the design
102
111
  tree branch by branch, resolving dependencies between decisions one at a time.
@@ -105,6 +114,14 @@ Keep the CONTENT in the conversation's language, but keep the labels (`CLOSED`,
105
114
  failed.
106
115
  5. **Write files lazily and inline.** Create a file only when you have something real to
107
116
  write, and update it the moment a decision crystallizes — don't batch to the end.
117
+ 6. **Mark what YOU decided: `origin: agent`.** Any acceptance criterion, ADR decision item
118
+ or HANDOFF rule that came from your proposal rather than from a human answer is written
119
+ with the marker on its own line — `(origin: agent)` while it is unconfirmed, and
120
+ `(origin: agent, confirmed: YYYY-MM-DD)` the moment the human confirms THAT item. Items
121
+ with no marker are human-origin by default; nothing already written is retro-tagged. An
122
+ unconfirmed item is NOT in scope: it is written down so it is visible, never so it is
123
+ agreed. `spec-check` names the unconfirmed ones with file:line (advisory — it changes no
124
+ exit code), and your close block lists them.
108
125
 
109
126
  ## Inputs
110
127
 
@@ -137,6 +154,17 @@ answer.
137
154
  1. **Purpose / value / why now.** What job does this remove? Cost of not doing it?
138
155
  2. **Domain model.** Propose the core entities and their relationships. ("From the domain
139
156
  I deduce these core entities: … do they work for you, or is one missing?")
157
+ Then ask, MANDATORY, before the round closes (ADR-046): **"what input comes from the REAL
158
+ world, and where is the corpus?"** In greenfield every test payload is invented by the agent
159
+ that writes the code, so a green suite can be silent about the inputs the world actually
160
+ produces — one field build shipped a parser that passed every test its author wrote and was
161
+ 96.96 % right on the real corpus. Name the real-input surfaces (files, payloads, messages,
162
+ documents), ask WHERE a sample with its expected outputs can be obtained, and record the path
163
+ as `repos[R].corpus` in `uscha.config.json` so `qa_ledger.py corpus-run` can score it.
164
+ **No corpus on day 1 is not a blocker — it is a HIGH risk in `RISKS.md` with an OWNER and a
165
+ date.** Do not invent one: a corpus the agent authored is the same invented input this
166
+ question exists to expose. The percentage gates nothing until the project declares a budget
167
+ (`corpus_threshold`), which is round 10's decision, not the kit's.
140
168
  3. **Operation / API surface.** Propose the endpoints/operations and their
141
169
  contracts (idempotency, status codes).
142
170
  4. **Stack and lifecycle (MANDATORY — before any stack/architecture decision is
@@ -187,6 +215,18 @@ answer.
187
215
  `spike/*` branch and its ONLY legitimate output is an **ADR with lessons**
188
216
  (facts that feed the SPEC) — never mergeable code. The contract is executable:
189
217
  `phase --require pr-ready` refuses any `spike/*` branch, INV-GOLDEN-01 style.
218
+ 12. **Operability — who owns the RUNBOOK and the seed? (kit 2.2.0, ADR-048).** Ask it on
219
+ DAY 1, with your recommended answer, because the cheapest moment to decide it is before
220
+ anything is built and the most expensive is the week before go-live: who writes and owns
221
+ the **RUNBOOK** (start/boot, config, rollback, smoke), who owns the **seed/reset**
222
+ command that puts a fresh environment into a known state, and who owns the **release
223
+ workflow** that publishes the artifact. Record the answers as they crystallize:
224
+ `defaults.operability.seed_command` (or `repos[R].operability.seed_command`) in
225
+ `uscha.config.json`, and the RUNBOOK at `docs/RUNBOOK.md`. This is MEASURED, not
226
+ narrated — `qa_ledger.py operability --repo <REPO>` reads the four facts out of the tree,
227
+ and on risk profiles C/D/E a missing one is a BLOCKER, so a project that defers them
228
+ cannot reach `pr-ready`. Two field projects discovered all four in their last week; the
229
+ question costs one minute here.
190
230
 
191
231
  ## Files to write (lazily, inline)
192
232
 
@@ -210,7 +250,9 @@ answer.
210
250
  `Rollback / Supersede Criteria`.
211
251
  - **`ACCEPTANCE.md`** — Definition of Done as `- [ ]` checkboxes + success metrics. This
212
252
  is the file the readiness KPI measures downstream. Give EVERY criterion a stable
213
- traceable ID: `- [ ] AC-01 — when X then Y` (sequential, never reused). Downstream,
253
+ traceable ID: `- [ ] AC-01 — when X then Y` (sequential, never reused); a criterion YOU
254
+ proposed also carries the `origin: agent` marker of principle 6 until the human confirms
255
+ it item by item. Downstream,
214
256
  a criterion only closes MEASURED when a green testcase carries its tag in the name
215
257
  (`test_ac1_x` / `testAC01X` / `"AC-01: ..."`) — write criteria so each one is
216
258
  coverable by at least one named test.
@@ -231,6 +273,18 @@ as explicit assumptions), every failure mode has defined behavior, out-of-scope
231
273
  explicit, and the DoD is checkable. State plainly that it converged, then write/finalize
232
274
  the package and the handoff.
233
275
 
276
+ **The close block names what is still unconfirmed.** Before you emit it, run
277
+
278
+ ```bash
279
+ python3 <path-to>/qa_ledger.py spec-check --spec SPEC.md --acceptance ACCEPTANCE.md
280
+ ```
281
+
282
+ and copy the `~ origin: N agent-origin item(s) unconfirmed` list into `Blocks:` — one item
283
+ per line, with its file and line. Those are the decisions the human still owes; converging
284
+ with them silently in the package is the failure this marker exists to prevent. `Produced:`
285
+ still lists the files. If the list is empty, `Blocks:` says `nothing` as it always did:
286
+ silence and an explicit `nothing` are not the same fact.
287
+
234
288
  ## Handoff
235
289
 
236
290
  End with the implementation handoff (works for a human, an agent, or CI):
@@ -18,6 +18,8 @@ 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) -->
22
+
21
23
  ## Orientation markers (non-negotiable)
22
24
 
23
25
  The operator must never have to ask "where am I?" or "what happens now?".
@@ -24,6 +24,8 @@ 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) -->
28
+
27
29
  ## First contact (show ONCE, then never again)
28
30
 
29
31
  **Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
@@ -22,6 +22,8 @@ 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) -->
26
+
25
27
  ## First contact (show ONCE, then never again)
26
28
 
27
29
  **Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
@@ -19,7 +19,47 @@ skill (**pull** — one screen when the human asks), and the **mirador** (bird's
19
19
  HTML). This skill exists because some surfaces never show a statusline; the answer
20
20
  is the same data, printed in chat when requested.
21
21
 
22
+ ## Before the readout: are the installed skills older than the kit? (2.2.0)
23
+
24
+ A field report found `~/.claude/skills/uscha-*` still on 1.54.0 while the kit in the repo was
25
+ 1.97.0. An entire discovery ran on prose three months stale and NOTHING said so, because a
26
+ SKILL.md carried no version to compare. Since 2.2.0 every generated orientation block opens with
27
+ `<!-- uscha kit: X.Y.Z ... -->`, so the comparison is mechanical. Do it FIRST, before the
28
+ breadcrumb:
29
+
30
+ 1. Run the engine's own check — it reads the marker out of every installed `SKILL.md` and
31
+ compares it against the kit's `VERSION`:
32
+
33
+ ```
34
+ python <kit>/.claude/skills/uscha-devloop/qa_ledger.py doctor --json
35
+ ```
36
+
37
+ Read `skills_installed`: one row per install root the installer writes to (Claude
38
+ `~/.claude/skills/`, Codex `~/plugins/uscha/skills/`, pi `~/.agents/skills/`, and the other
39
+ Agent-Skills roots), each with `status`, `installed` and `kit`. `doctor` exits 0 whether or
40
+ not anything is outdated — this reports, it never gates.
41
+ 2. If any row reads `outdated`, print exactly ONE line ABOVE the breadcrumb, then continue
42
+ normally:
43
+
44
+ ```
45
+ SKILLS OUTDATED: installed 1.54.0 < kit 2.2.0 -- run `python install-uscha.py install --target claude`
46
+ ```
47
+
48
+ Where the installed block predates 2.2.0 and carries no marker at all, `installed` is null:
49
+ say `installed <no kit: marker, pre-2.2.0>` rather than inventing a version.
50
+ 3. `not installed` is NOT a finding — the kit installs one agent at a time, and six absent roots
51
+ are the normal shape of a healthy machine. Say nothing about them.
52
+
53
+ Advisory, always: the line never blocks the readout, never changes a number, and the block below
54
+ prints exactly as it would have. What it removes is the silence.
55
+
56
+ Honest limit: an installed skill that predates 2.2.0 does not carry these instructions, so on
57
+ that surface the warning cannot come from the skill itself. The `doctor` seam is the instrument
58
+ that still works there — it runs from any kit checkout and reads the installs from outside.
59
+
22
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) -->
62
+
23
63
  ## Orientation markers (non-negotiable)
24
64
 
25
65
  The operator must never have to ask "where am I?" or "what happens now?".
@@ -22,6 +22,8 @@ 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) -->
26
+
25
27
  ## First contact (show ONCE, then never again)
26
28
 
27
29
  **Only when this project has no uscha artifacts yet** -- no `QA-LEDGER.json`, no `SPEC.md` or
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "uscha",
4
- "version": "2.0.0",
4
+ "version": "2.2.0",
5
5
  "displayName": "Uscha",
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, 53 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
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": {
8
8
  "name": "Andres Massello",
9
9
  "url": "https://github.com/andresmassello"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "2.0.0",
3
+ "version": "2.2.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.0.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v2.2.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`,
@@ -108,6 +108,11 @@ older spec still covers newer code is a relevance judgment, and a guess advises.
108
108
  python qa_ledger.py spec-drift --repo <name> --json # advisory report; exit 0 always
109
109
  ```
110
110
 
111
+ It searches `repos[<name>].path` first and the **config root** (where `uscha.config.json` and the
112
+ ledger live) second, and the report NAMES which of the two it read (`spec_source`). A monorepo
113
+ keeps ONE `SPEC.md` at the root and it governs every repo — before 2.2.0 that case answered "no
114
+ spec documents", which reads exactly like "no drift".
115
+
111
116
  Map specs to code with a `governs:` glob list in the frontmatter of `SPEC.md` and each
112
117
  `docs/adr/*.md`:
113
118
 
@@ -128,6 +133,44 @@ decision that governs no source (negative ADRs) — a declaration, not an omissi
128
133
  mirador can surface it. No readiness impact, no exit-code gate: a stale spec is a prompt for
129
134
  a human conversation, not a blocked pipeline.
130
135
 
136
+ ## Operability (ADR-048) — release, reset and the RUNBOOK are part of done
137
+
138
+ A field finding, twice: release by CI, the reset/seed script and the RUNBOOK arrived in the
139
+ **last week** of two projects. The devloop named all four in phase 8 prose, and a narrated
140
+ dimension is not a weak gate — it is an absent one. `operability` reads them as FACTS in the tree:
141
+
142
+ ```bash
143
+ python qa_ledger.py operability --repo <name> [--json] # exit 0 always
144
+ ```
145
+
146
+ | check | `ok` when |
147
+ |---|---|
148
+ | `ci` | a `.github/workflows/*.yml` step runs the repo's **configured** test command (`repos[R].test_command`, else `defaults.test_command_<type>`) — verbatim, or its first token beside a test-ish subcommand |
149
+ | `release` | a workflow publishes or attaches an asset: `softprops/action-gh-release`, `actions/upload-release-asset`, `gh release create`, `gh release upload`, `gh release`, `npm publish`, `twine upload` |
150
+ | `runbook` | `docs/RUNBOOK.md` (or `RUNBOOK.md`, or `defaults.operability.runbook`) exists AND names start/boot, config, rollback and smoke — EN or ES, case-insensitive; the absent ones are NAMED |
151
+ | `seed` | `repos[R].operability.seed_command` or `defaults.operability.seed_command` is declared, and its script is on disk when the command names a path |
152
+
153
+ Repo path first, **config root** second (`realpath` on both sides), and the output NAMES which it
154
+ read. It never executes anything. A CI system it does not read — GitLab, Jenkins, Azure — is
155
+ reported `unknown`, never green and never red.
156
+
157
+ The POSTURE is your risk profile's, not the kit's. `defaults.operability.gate` is `false` by the
158
+ engine's default and `true` on presets **C, D and E**:
159
+
160
+ - **advisory** (A, B, or no profile): the record caps nothing, blocks nothing, and never joins the
161
+ `N ok` count in `readiness`;
162
+ - **gate** (C, D, E): a `missing` check is a BLOCKER — readiness capped ≤ 65, convergence blocked,
163
+ and `phase --require pr-ready` refuses **naming** the missing check.
164
+
165
+ `readiness` prints one conditional line and stays silent with no record:
166
+
167
+ ```
168
+ --- operability: ci ok · release missing · runbook ok · seed missing (advisory)
169
+ ```
170
+
171
+ Nothing is graded. The engine can see that a `## Rollback` heading exists; whether the procedure
172
+ under it is correct is yours.
173
+
131
174
  ## Evidence origin (ADR-007) - green, but green at *what*?
132
175
 
133
176
  Freshness compares file mtimes, so the ledger could say "tests green" without being able to
@@ -436,12 +479,47 @@ python uscha-kit/install-uscha.py doctor --target both
436
479
  ```
437
480
 
438
481
  It checks: Python >=3.8 · git · the 9 skills alongside the engine (frontmatter
439
- verified) · the INV-GOLDEN-01 hook (present + registered in settings.json +
482
+ verified) · **whether the INSTALLED skills are older than this kit** (2.2.0, ADR-045) · the
483
+ INV-GOLDEN-01 hook (present + registered in settings.json +
440
484
  powershell/pwsh interpreter) · and if there is a `uscha.config.json` in the cwd:
441
485
  config parseable, ACCEPTANCE with AC-IDs, ledger integrity, the QA skills
442
486
  from `qa_tools_order` (the loop orchestrates them without bringing them in) and the
443
487
  primary toolchain of each repo by type (its absence is a WARNING — it may live only in CI).
444
488
 
489
+ **Are the installed skills older than the kit?** (2.2.0, ADR-045.) Every generated `SKILL.md`
490
+ orientation block now opens with `<!-- uscha kit: X.Y.Z -->`, rendered from `uscha-kit/VERSION`
491
+ and re-rendered by the release script at every bump, so an installed skill can be dated. `doctor`
492
+ reads that marker out of each install root the installer writes to — or the ones you name — and
493
+ reports both versions:
494
+
495
+ ```bash
496
+ python3 ~/.claude/skills/uscha-devloop/qa_ledger.py doctor --json
497
+ # -> "kit_version" and "skills_installed": [{target, root, status, installed, kit}]
498
+ python3 .../qa_ledger.py doctor --installed /some/other/skills # repeatable
499
+ ```
500
+
501
+ `status` is `current`, `outdated` (with **both** versions; `installed: null` when the blocks
502
+ predate 2.2.0 and carry no marker at all) or `not installed` — which is not a fault, since the kit
503
+ installs one agent at a time. This is **advisory**: an outdated install is a warning, never an
504
+ error, and `doctor`'s exit code is unchanged. It exists because a discovery once ran on skills
505
+ from 1.54.0 while the kit was at 1.97.0 and nothing said a word. `/uscha-status` prints the same
506
+ finding as one line above its readout.
507
+
508
+ The comparison needs BOTH halves, so the installer ships the second one: `install` copies
509
+ `uscha-kit/VERSION` beside the installed skills as **`.uscha-kit-VERSION`**
510
+ (`~/.claude/skills/.uscha-kit-VERSION`, and the equivalent root for `pi`, `cursor`, `copilot`,
511
+ `gemini` and `cline`; the Codex plugin carries the same file at its plugin root), and `uninstall`
512
+ removes it. The name is namespaced on purpose: `~/.claude/skills/` is a directory the kit does not
513
+ own, and a copy called `VERSION` would overwrite whatever else lived under that name and delete it
514
+ on uninstall. **A foreign `VERSION` is never written and never removed** (`AC-SK-08`).
515
+
516
+ Without that copy an INSTALLED engine has no kit version to compare against and `doctor` says so —
517
+ `installed-skill freshness is UNMEASURED`, naming the directories it read, never a silent
518
+ `current`. Two other sources answer first where they can: the kit checkout itself, reached through
519
+ the engine's REALPATH so a `--mode link` install resolves the kit its skills point into
520
+ (`AC-SK-09`), and `uscha-install.json`'s `version` where the copy was removed by hand. Found by
521
+ running the first-use walkthrough end to end; pinned by `AC-SK-07`, which installs for real.
522
+
445
523
  ## Configure
446
524
 
447
525
  `uscha init` **generates** a minimal `uscha.config.json` for the project: its name, the repo it
@@ -549,6 +627,9 @@ QL=".claude/skills/uscha-devloop/qa_ledger.py"
549
627
 
550
628
  python3 $QL --help # see subcommands
551
629
  python3 $QL init --config uscha.config.json # creates QA-LEDGER.json
630
+ # later, a new service joins an existing loop — this APPENDS and re-seals the checksum,
631
+ # it never rebuilds the ledger (2.2.0):
632
+ python3 $QL init --add-repo web-app --path web-app --type node --test-command "npm test"
552
633
 
553
634
  # run your build with the reports, then:
554
635
  python3 $QL snapshot --repo backend-api --phase pre
@@ -593,6 +674,10 @@ python3 $QL spec-change-request --repo backend-api --source SD-001 --requested-c
593
674
  close measured (documented limitation).
594
675
  - A lintable repo whose static gate **never ran** scores that dimension UNMEASURED (0.0)
595
676
  — silence is not success.
677
+ - **Agent-origin items (kit 2.2.0, ADR-044)**: when `ACCEPTANCE.md`, `docs/adr/*.md` or
678
+ `HANDOFF.md` carry unconfirmed `(origin: agent)` markers, readiness prints one line of its own
679
+ — `--- origin: N agent-origin item(s) unconfirmed` — outside the gates rollup. Advisory: it
680
+ caps nothing and blocks nothing. See below.
596
681
  - **Hard caps** (they override the ceiling): tests in red → ≤35, open BLOCKER/CRITICAL → ≤65,
597
682
  unresolved escalation → ≤75 (holds until `resolve-escalation`, a recorded event).
598
683
  - Bands: `<50 NOT READY` · `50–79 IN PROGRESS` · `80–94 RELEASE CANDIDATE` · `95–100 READY`.
@@ -621,9 +706,9 @@ python3 $QL rebuild --mode compare --baseline REBUILD-BASELINE.json --json # c
621
706
  - Verdicts: `COVERS ≥90` · `PARTIAL ≥70` · `DIVERGE <70`. The score lists the concrete
622
707
  **gaps** — feed them back into the SPEC and re-run. Divergence is a spec hole, not a code bug.
623
708
 
624
- ## Simplicity gate — "Reduce" (minimality of the change)
709
+ ## Simplicity check — "Reduce" (minimality of the change) — ADVISORY by default
625
710
 
626
- The **Simplicity** invariant of the CONSTITUTION made a deterministic gate: it scores the *diff*
711
+ The **Simplicity** invariant of the CONSTITUTION made deterministic: it scores the *diff*
627
712
  (not CC by AST — they are measurable proxies: minimality, nesting, new abstractions).
628
713
 
629
714
  ```bash
@@ -634,21 +719,174 @@ python3 $QL simplicity-check --diff changes.diff --json # consumed by usc
634
719
 
635
720
  - Dimensions/weights: diff_size 35, nesting 30, net_growth 20, fan_out 8, blob 7
636
721
  (abstraction does NOT weigh in the score — it's a guessy proxy, kept as a metric + advisory flag).
637
- - Verdicts: `SIMPLE ≥85` · `ACCEPTABLE ≥65` · `OVERBUILT <65` (exit 1 = BLOCKER: trim and re-run).
722
+ - Verdicts: `SIMPLE ≥85` · `ACCEPTABLE ≥65` · `OVERBUILT <65`.
638
723
  A gross excess (2× budget, or very deep nesting) caps the score at 60 no matter what.
724
+ - **Advisory by default, exit 0** (kit 2.1.0, ADR-043): every budget above is the KIT's opinion
725
+ until you declare your own, and an opinion that exits 1 is a gate nobody asked for. It
726
+ **gates** — OVERBUILT = exit 1 = BLOCKER — only with at least one budget declared in
727
+ `defaults.simplicity` **AND** `defaults.simplicity.gate: true` (or `--gate`). `gate: true`
728
+ with no budget declared is a config error, exit 2: a gate with no budget is not a gate.
729
+ `log-gate --kind simplicity --verdict advisory` persists an advisory run as an advisory —
730
+ readiness prints `N ok · 1 advisory` and the mirador reads `ADVISORY`, never `OK`.
731
+ - **`max_nesting` is an INDENTATION-DEPTH proxy**, not AST nesting: it reads leading
732
+ indentation on added lines, so a wrapped argument, JSX, or a multi-line Java literal inflates
733
+ it with no control flow present. It is named as a proxy in the report rather than made
734
+ language-aware. 2-space codebase → `--indent-width 2`.
639
735
  - **Tests OUT of the budget** (kit 1.11.0): the test files (conventions of the
640
736
  9 stacks) are counted and reported separately (`test_lines_added`) but do not gate — writing
641
737
  tests never pushes the diff to OVERBUILT (deleting them is already blocked by gate-check).
642
738
  - The flags tell you what to trim (guard clauses, speculative types/layers, giant hunks).
643
- - Budgets in `defaults.simplicity`; adjustable per risk profile. 2-space `--indent-width 2`.
739
+ - Budgets in `defaults.simplicity`. No risk profile owns them: the gate is a human declaration
740
+ under every profile A–E.
644
741
 
645
- ## Ledger subcommands
742
+ ## Agent-origin markers — ask for DECISIONS, never for INFORMATION (ADR-044)
743
+
744
+ Two field findings, one rule. **Information the tree holds is READ, never asked**: the current
745
+ version of a tracked `.md`, what a config says, whether a test exists. And **a decision the human
746
+ never made never enters scope silently** — an agent turned a global default into a tree-wide
747
+ rename that was approved by rebound inside a twenty-item summary, and had to be cancelled after
748
+ 112 files had moved.
646
749
 
647
- `bench - bench-curate - bench-r2 - bench-roundtrip - bootstrap-oracle - bootstrap-variance - check-coverage - check-terminado - cleanroom - compile-ingest - compile-validate - converged - curate - curation-check - dashboard - discover - doctor - escalate - execution-policy - facts - fastpath-eval - fidelity - flag-blocker - gate-check - golden-coverage - golden-diff - ingest-gate - init - ir-extract - ir-render - lang-compare - log-gate - log-step - oscillation - phase - pit-check - production-finding - promote - readiness - rebuild - regression-check - resolve-escalation - roundtrip - rubric-ingest - simplicity-check - snapshot - spec-change-request - spec-check - spec-doubt - spec-drift - summary - top - waste-check` - the exact current `qa_ledger.py` parser surface (53 subcommands, derived from `SYSTEM-FACTS.json`, itself introspected from `build_parser()`); each supports `--help`.
750
+ So anything the AGENT introduced an acceptance criterion, an ADR decision item, a `HANDOFF.md`
751
+ rule — carries one trailing marker on its own line:
752
+
753
+ ```
754
+ - [ ] AC-12 — when X then Y. (origin: agent)
755
+ - [ ] AC-12 — when X then Y. (origin: agent, confirmed: 2026-09-07)
756
+ ```
757
+
758
+ - **No marker = human origin.** That is the default, so nothing already written is retro-tagged.
759
+ - **Confirmation is PER ITEM**, recorded as `confirmed: YYYY-MM-DD` on the same line. A
760
+ package-level "ok" confirms none of them, which is the whole point.
761
+ - **An unconfirmed item is not in scope**: it is not implemented, not gated on, not quoted as
762
+ agreed.
763
+ - A `confirmed:` that is not a real `YYYY-MM-DD` date counts as **unconfirmed** and is named — a
764
+ typo must never read as a human's approval.
765
+ - A marker inside a fenced block or an inline code span (like the two above) is documentation and
766
+ is skipped, so a file that DEFINES the grammar is not read as a finding.
767
+
768
+ What the engine does with it, on the files it already reads plus `ACCEPTANCE.md`, `docs/adr/*.md`
769
+ and `HANDOFF.md`:
770
+
771
+ ```bash
772
+ python3 $QL spec-check --spec SPEC.md --acceptance ACCEPTANCE.md
773
+ # ~ origin: 2 agent-origin item(s) unconfirmed -- AC-07 (ACCEPTANCE.md:41), D-03 (docs/adr/ADR-002-x.md:57)
774
+ python3 $QL readiness
775
+ # --- origin: 2 agent-origin item(s) unconfirmed (spec-check names them)
776
+ ```
777
+
778
+ **ADVISORY, both of them.** `spec-check` keeps the exit code it had; `readiness` prints its line
779
+ outside the gates rollup and caps nothing, blocks nothing, and changes no score. A gate here would
780
+ need a budget the project adopted, and nobody has declared one (the 2.1.0 posture, ADR-043).
781
+ `spec-check --json` carries `agent_origin: {unconfirmed: [...], confirmed: n}`; `readiness --json`
782
+ carries the same object when there is anything to report. A tree with no marker anywhere prints
783
+ exactly what it printed before.
784
+
785
+ ## Field truth for greenfield — `corpus-run` (ADR-046)
786
+
787
+ From a live greenfield build: *the parser passed every test the agent wrote, and it was wrong.*
788
+ Running the REAL corpus is what exposed it — 96.96 % before the fix, 99.645 % after. In greenfield
789
+ every test payload is invented by the agent that writes the code, so a green suite can be silent
790
+ about the inputs the world actually produces; and `characterize`/`golden-diff` have no answer
791
+ there, because their doctrine is *the old code is the truth* and there is no old code.
792
+
793
+ The corpus is **JSONL** — one object per line, `input` and `expected` required, `id` optional:
794
+
795
+ ```jsonl
796
+ {"id": "inv-2019-03", "input": "...raw payload...", "expected": {"total": 1042, "currency": "EUR"}}
797
+ ```
798
+
799
+ Each case runs the command once with its input on **stdin** (verbatim when it is a string,
800
+ JSON-encoded otherwise); the trimmed **stdout** is compared to `expected` — string compare, then
801
+ JSON-equal when both sides parse as JSON. A non-zero exit is a miss named with its code; a case
802
+ past `--timeout` (default 30 s) is a miss named `timeout`. File order is run order, so two runs
803
+ over one corpus report the same misses in the same places.
804
+
805
+ ```bash
806
+ python3 $QL corpus-run --repo <REPO> --corpus corpus/real.jsonl --command "python -m myparser" --threshold 99 --ac AC-FIELD-01
807
+ # [qa_ledger] backend-api/gate:corpus: FAIL 96.96 % (3195/3295) — threshold 99 % from --threshold
808
+ python3 $QL readiness
809
+ # --- field backend-api: corpus 96.96 % (3195/3295) < 99 % FAIL
810
+ ```
811
+
812
+ - **The threshold is yours**: `--threshold`, else `repos[R].corpus_threshold`, else
813
+ `defaults.corpus_threshold`. With **none declared the run is ADVISORY** — measured, persisted,
814
+ gating nothing, and never counted as an `ok` gate (the 2.1.0 posture, ADR-043). With one
815
+ declared, a run under it caps readiness ≤65 and blocks convergence like any fact gate.
816
+ - **A corpus that cannot be read is exit 2, naming the line** — missing, empty, malformed, or
817
+ missing an `input`/`expected` key. An unreadable corpus scored as 0 % would be an unmeasurable
818
+ input reported as a measured catastrophe.
819
+ - **`--ac AC-nn` closes a criterion MEASURED on a green run**, exactly as a green testcase does; a
820
+ ticked criterion whose only corpus evidence is red or advisory reports `narrated_only`.
821
+ - **`log-gate --kind corpus`** records a run measured elsewhere (CI, a nightly) and is the third
822
+ and last kind that accepts `--verdict advisory`, because the check's own default mode is
823
+ advisory. Every other FACT kind still refuses it.
824
+ - **No weight.** `readiness` prints one conditional `field` line per repo that declares a corpus
825
+ or ran one, and nothing else moves; the `field` *dimension* is deferred to its own ADR.
826
+ - **Never author the corpus yourself.** A corpus the agent invented is the invented input this
827
+ instrument exists to expose.
828
+
829
+ ## The smoke run as measured evidence — `smoke-ingest` (ADR-047)
830
+
831
+ From the field: *every simulator run returned an empty list, because the database had no rows —
832
+ and the smoke was reported as verified.* An empty list is a 200. Phase 7 used to end in a
833
+ paragraph — "the jar served /admin", "the simulator answered 200 in 6 ms" — and a paragraph
834
+ cannot tell "the endpoint answered correctly" from "the endpoint answered". **Evidence is
835
+ executed, not narrated.**
836
+
837
+ The contract is the smallest thing a shell script can emit — `name` and a **boolean** `ok` are the
838
+ whole mandatory surface:
839
+
840
+ ```json
841
+ {"checks": [{"name": "AC-28 the jar serves /admin", "ok": true, "status": 200,
842
+ "latency_ms": 6, "evidence": "curl -sS localhost:8080/admin | head -1"}]}
843
+ ```
844
+
845
+ ```bash
846
+ python3 $QL smoke-ingest --repo <REPO> --report reports/smoke.json
847
+ # [qa_ledger] backend-api/gate:smoke: FAIL — 7/8 checks ok
848
+ # FAIL healthz (status 503)
849
+ # caps readiness <=65 and blocks convergence until a clean smoke
850
+ python3 $QL readiness
851
+ # --- smoke backend-api: 7/8 checks ok, 1 failed (healthz) FAIL
852
+ ```
853
+
854
+ - **A failed check is a BLOCKER**, through the same record `gate-check` writes: readiness ≤65,
855
+ convergence blocked, cleared by a later clean report.
856
+ - **A report the engine cannot read is exit 2, naming the check or the field** — missing,
857
+ malformed, no `checks` key, a check with no `name` or no boolean `ok`. An **EMPTY** `checks`
858
+ list is refused too: a run that verified nothing is not a clean gate.
859
+ - **`smoke` is a FACT kind and never runs advisory.** `corpus` may (a percentage needs an adopted
860
+ budget); `ok` is binary, so an advisory smoke would be a mandatory gate cleared by goodwill.
861
+ `log-gate --kind smoke --verdict pass|fail|not-run` is the parity door.
862
+ - **A check named `AC-nn ...` closes that criterion MEASURED** when it is `ok` and its report
863
+ passed — the same tag grammar a JUnit testcase name uses. A **failed** tagged check VETOES,
864
+ exactly like a red test, and `readiness --json` reports both halves
865
+ (`acceptance.smoke_closed`, `acceptance.smoke_vetoed`).
866
+ - **No weight.** One conditional `smoke` line per repo that ingested a report, and nothing else
867
+ moves.
868
+ - **`templates/scripts/smoke-report-example.json`** is the reference report (three checks, one
869
+ tagged `AC-01`). Never narrate the report: a checklist a human ticked is evidence, a checklist
870
+ an agent wrote is not.
871
+
872
+ ## Ledger subcommands
648
873
 
649
- The **fact gates** (golden-diff, gate-check, pit-check, simplicity) are PERSISTED with
650
- `log-gate`: a fail blocks convergence and caps readiness ≤65 via the ledger. A CONSTITUTION
651
- violation is recorded with `flag-blocker` (same effect, until `--resolve`).
874
+ `bench - bench-curate - bench-r2 - bench-roundtrip - bootstrap-oracle - bootstrap-variance - check-coverage - check-terminado - cleanroom - compile-ingest - compile-validate - converged - corpus-run - curate - curation-check - dashboard - discover - doctor - escalate - execution-policy - facts - fastpath-eval - fidelity - flag-blocker - gate-check - golden-coverage - golden-diff - ingest-gate - init - ir-extract - ir-render - lang-compare - log-gate - log-step - operability - oscillation - phase - pit-check - production-finding - promote - readiness - rebuild - regression-check - resolve-escalation - roundtrip - rubric-ingest - simplicity-check - smoke-ingest - snapshot - spec-change-request - spec-check - spec-doubt - spec-drift - summary - top - waste-check` - the exact current `qa_ledger.py` parser surface (56 subcommands, derived from `SYSTEM-FACTS.json`, itself introspected from `build_parser()`); each supports `--help`.
875
+
876
+ `gate-check --repo R` SCOPES the diff to `repos[R].path` (2.2.0): in a monorepo one `git diff`
877
+ carries every repo's hunks, and a sibling's findings are neither this repo's report nor this
878
+ repo's exit code. A rename is reported under `moved` — informational, never a deletion — both
879
+ from git's own rename headers (`--from-git` forces `-M`, so the caller's `diff.renames` config
880
+ cannot hide one) and from an exact delete/add pair in a diff whose producer detected none. A
881
+ real deletion still blocks.
882
+
883
+ The **fact gates** (golden-diff, gate-check, pit-check, simplicity, **ci**) are PERSISTED with
884
+ `log-gate`: a fail blocks convergence and caps readiness ≤65 via the ledger; `--verdict
885
+ advisory` records a run that is measured but not gating (it caps nothing, blocks nothing, and
886
+ never counts as an `ok` gate) and is accepted ONLY for `simplicity|waste` — every FACT gate
887
+ refuses it. `--kind ci` (2.2.0) records what the pipeline did, with `--ref <run URL or id>`
888
+ stored on the record as the receipt; a `ci` fail caps and blocks exactly like `gate-check`. A CONSTITUTION violation is recorded with `flag-blocker` (same
889
+ effect, until `--resolve`).
652
890
 
653
891
  ## Notes
654
892
 
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 2.0.0
1
+ uscha-kit 2.2.0
@@ -33,6 +33,13 @@ SKILL_ROOTS = {
33
33
  }
34
34
  TARGETS = ("codex", "claude") + tuple(SKILL_ROOTS)
35
35
  HOOK_NAME = "block-approved-writes.py"
36
+ # The kit's version, copied beside the installed skills so an INSTALLED engine can date them
37
+ # (ADR-045). The name is NAMESPACED (2.2.0, fresh review): until this release the copy was
38
+ # called `VERSION`, a bare shared name written into directories the kit does not own --
39
+ # `~/.claude/skills/`, `~/.cursor/skills/` -- where an install would overwrite whatever else
40
+ # lived under it and an uninstall would delete it. An installer owns the files it creates and
41
+ # nothing else, and the only way to guarantee that is to claim a name nobody else would pick.
42
+ KIT_VERSION_COPY = ".uscha-kit-VERSION"
36
43
 
37
44
 
38
45
  class InstallError(Exception):
@@ -274,7 +281,7 @@ def stage_plugin(plugin_root, mode):
274
281
  (stage / "skills").mkdir()
275
282
  for skill in SKILLS:
276
283
  copy_skill(source / skill, stage / "skills" / skill, mode)
277
- shutil.copy2(KIT_ROOT / "VERSION", stage / "VERSION")
284
+ shutil.copy2(KIT_ROOT / "VERSION", stage / KIT_VERSION_COPY)
278
285
  shutil.copy2(KIT_ROOT / "uscha.config.json", stage / "uscha.config.json")
279
286
  return stage
280
287
  except Exception:
@@ -366,7 +373,7 @@ def install_claude(home, mode, dry_run, operations):
366
373
  data = prepared_settings(settings, hook_command(hook)) # parse and merge before writes, including dry-run
367
374
  marker_data = marker("claude", root, mode)
368
375
  operations.extend({"action": "install-skill", "path": str(root / "skills" / skill)} for skill in SKILLS)
369
- operations.extend([{"action": "copy-hook", "path": str(hook)}, {"action": "atomic-write-json", "path": str(settings)}, {"action": "write-marker-last", "path": str(install_marker)}])
376
+ operations.extend([{"action": "copy-version", "path": str(root / "skills" / KIT_VERSION_COPY)}, {"action": "copy-hook", "path": str(hook)}, {"action": "atomic-write-json", "path": str(settings)}, {"action": "write-marker-last", "path": str(install_marker)}])
370
377
  if dry_run:
371
378
  return root
372
379
 
@@ -383,6 +390,7 @@ def install_claude(home, mode, dry_run, operations):
383
390
  copy_skill(source / skill, staged / "skills" / skill, mode)
384
391
  (staged / "hooks").mkdir()
385
392
  shutil.copy2(KIT_ROOT / "hooks" / HOOK_NAME, staged / "hooks" / HOOK_NAME)
393
+ shutil.copy2(KIT_ROOT / "VERSION", staged / "skills" / KIT_VERSION_COPY)
386
394
  atomic_json(staged / "settings.json", data)
387
395
  atomic_json(staged / "uscha-install.json", marker_data)
388
396
 
@@ -390,6 +398,8 @@ def install_claude(home, mode, dry_run, operations):
390
398
  entries = [(skills_root / skill, staged / "skills" / skill, backups / "skills" / skill)
391
399
  for skill in SKILLS]
392
400
  entries.extend([
401
+ (skills_root / KIT_VERSION_COPY, staged / "skills" / KIT_VERSION_COPY,
402
+ backups / "skills" / KIT_VERSION_COPY),
393
403
  (hook, staged / "hooks" / HOOK_NAME, backups / "hooks" / HOOK_NAME),
394
404
  (settings, staged / "settings.json", backups / "settings.json"),
395
405
  (install_marker, staged / "uscha-install.json", backups / "uscha-install.json"),
@@ -456,6 +466,7 @@ def install_skills_only(target, home, mode, dry_run, operations):
456
466
  install_marker = root / "uscha-install.json"
457
467
  marker_data = marker(target, root, mode)
458
468
  operations.extend({"action": "install-skill", "path": str(root / skill)} for skill in SKILLS)
469
+ operations.append({"action": "copy-version", "path": str(root / KIT_VERSION_COPY)})
459
470
  operations.append({"action": "write-marker-last", "path": str(install_marker)})
460
471
  if dry_run:
461
472
  return root
@@ -471,9 +482,12 @@ def install_skills_only(target, home, mode, dry_run, operations):
471
482
  backups.mkdir()
472
483
  for skill in SKILLS:
473
484
  copy_skill(source / skill, staged / skill, mode)
485
+ shutil.copy2(KIT_ROOT / "VERSION", staged / KIT_VERSION_COPY)
474
486
  atomic_json(staged / "uscha-install.json", marker_data)
475
487
 
476
488
  entries = [(root / skill, staged / skill, backups / skill) for skill in SKILLS]
489
+ entries.append((root / KIT_VERSION_COPY, staged / KIT_VERSION_COPY,
490
+ backups / KIT_VERSION_COPY))
477
491
  entries.append((install_marker, staged / "uscha-install.json", backups / "uscha-install.json"))
478
492
  # The loops below bind PATHS. They must NOT be named `target`: a Python for-loop has no
479
493
  # scope of its own, so that would permanently rebind this function's `target` argument
@@ -1056,11 +1070,13 @@ def uninstall_target(target, home, dry_run, operations, force):
1056
1070
  elif target in SKILL_ROOTS:
1057
1071
  for skill in SKILLS:
1058
1072
  drop(root / skill, "uscha skill")
1073
+ drop(root / KIT_VERSION_COPY, "kit version beside the installed skills")
1059
1074
  drop(marker_path, "install marker")
1060
1075
  else:
1061
1076
  skills_root = root / "skills"
1062
1077
  for skill in SKILLS:
1063
1078
  drop(skills_root / skill, "uscha skill")
1079
+ drop(skills_root / KIT_VERSION_COPY, "kit version beside the installed skills")
1064
1080
  drop(root / "hooks" / HOOK_NAME, "INV-GOLDEN-01 hook")
1065
1081
  drop(marker_path, "install marker")
1066
1082
  settings_path = root / "settings.json"