@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.
- package/README.md +21 -4
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +171 -15
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +2089 -97
- package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +58 -4
- package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-reverse-discovery/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-rubric/SKILL.md +2 -0
- package/uscha-kit/.claude/skills/uscha-status/SKILL.md +40 -0
- package/uscha-kit/.claude/skills/uscha-sysdoc/SKILL.md +2 -0
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +249 -11
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/install-uscha.py +18 -2
- package/uscha-kit/skills/uscha-adr-refine/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-characterize/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-devloop/SKILL.md +171 -15
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +2089 -97
- package/uscha-kit/skills/uscha-discovery/SKILL.md +58 -4
- package/uscha-kit/skills/uscha-mirador/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-reverse-discovery/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-rubric/SKILL.md +2 -0
- package/uscha-kit/skills/uscha-status/SKILL.md +40 -0
- package/uscha-kit/skills/uscha-sysdoc/SKILL.md +2 -0
- package/uscha-kit/templates/CLAUDE.md +16 -0
- package/uscha-kit/templates/CONSTITUTION.md +55 -3
- package/uscha-kit/templates/docs/adr/README.md +15 -0
- package/uscha-kit/templates/scripts/smoke-report-example.json +24 -0
- 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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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)
|
|
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.
|
|
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,
|
|
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"
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v2.
|
|
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
|
|
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
|
|
709
|
+
## Simplicity check — "Reduce" (minimality of the change) — ADVISORY by default
|
|
625
710
|
|
|
626
|
-
The **Simplicity** invariant of the CONSTITUTION made
|
|
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
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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.
|
|
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 /
|
|
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"
|