@andresmassello/uscha 1.99.0 → 2.0.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 +1 -1
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +10 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +103 -3
- package/uscha-kit/.claude-plugin/plugin.json +1 -1
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +55 -2
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/install-uscha.py +119 -14
- package/uscha-kit/skills/uscha-devloop/SKILL.md +10 -1
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +103 -3
- package/uscha-kit/uscha.config.json +2 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Requires **Python 3.8+** on the machine (the engine is Python stdlib — no pip
|
|
|
40
40
|
runtime dependencies). The npm package is a thin router; the canonical installer is
|
|
41
41
|
`uscha-kit/install-uscha.py`.
|
|
42
42
|
|
|
43
|
-
**Kit
|
|
43
|
+
**Kit v2.0.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
|
|
44
44
|
[changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG.md)
|
|
45
45
|
(the per-release changelogs live in the repo, not in the npm tarball)
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Spec-driven development for LLM coding agents: 9 skills + a stdlib evidence engine. Facts block, guesses advise; the human approves.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Andres Massello",
|
|
@@ -314,7 +314,16 @@ profile A (trivial change) skip it. It **collapses into `readiness`** as a `gate
|
|
|
314
314
|
## Phase 3 — QA loop (per repo)
|
|
315
315
|
|
|
316
316
|
Run the tools in `config.defaults.qa_tools_order` (default: code-review → judgment-day
|
|
317
|
-
→ improve). One pass of all tools = one cycle.
|
|
317
|
+
→ improve). One pass of all tools = one cycle.
|
|
318
|
+
|
|
319
|
+
Read the EFFECTIVE order, not the config file: since 2.0.0 `uscha init` generates a minimal
|
|
320
|
+
config, so `qa_tools_order` is often absent there and resolves from `defaults.risk_profile`
|
|
321
|
+
(ADR-001) or from the engine default. `qa_ledger.py doctor --json` prints it as
|
|
322
|
+
`effective.qa_tools_order` with its `origin`; the ledger froze the same value at `init`. A
|
|
323
|
+
project on **profile A runs `code-review` only** — do NOT invoke judgment-day or improve there,
|
|
324
|
+
and convergence must not wait for them.
|
|
325
|
+
|
|
326
|
+
After **each** tool pass:
|
|
318
327
|
|
|
319
328
|
1. Apply only fixes at/above the severity gate. Send the rest to `ISSUES-DEFERRED.md`.
|
|
320
329
|
2. Run the repo test command. If red and the fix isn't obvious → escalate.
|
|
@@ -1911,6 +1911,53 @@ def _apply_risk_profile(defaults):
|
|
|
1911
1911
|
return defaults
|
|
1912
1912
|
|
|
1913
1913
|
|
|
1914
|
+
# The engine's OWN value for each knob a risk profile owns (kit 2.0.0): the bottom rung of
|
|
1915
|
+
# the precedence ladder -- explicit override > selected profile > engine default -- as `doctor`
|
|
1916
|
+
# reports it, so the origin of every effective value is visible without reading the source.
|
|
1917
|
+
#
|
|
1918
|
+
# It is a REPORTING table and is deliberately NEVER materialized into `defaults`. Writing an
|
|
1919
|
+
# engine default into the config would make the kit's own value indistinguishable from a human
|
|
1920
|
+
# declaration, which is precisely what `thresholds_declared` / `declared_caps` exist to keep
|
|
1921
|
+
# apart (provenance, kit 1.17.0) -- and precisely the confusion that let a COPIED config
|
|
1922
|
+
# outrank the profile until 2.0.0. The fix for that was to stop copying, not to start
|
|
1923
|
+
# injecting: `uscha init` now generates a minimal config (install-uscha.py), and a knob it
|
|
1924
|
+
# leaves out keeps meaning "not declared" everywhere the engine reads it.
|
|
1925
|
+
#
|
|
1926
|
+
# qa_tools_order is None on purpose: with no list declared, convergence falls back to a window
|
|
1927
|
+
# of --tools-per-cycle agent steps, so there is no default list to name and saying otherwise
|
|
1928
|
+
# would be a narrated claim.
|
|
1929
|
+
ENGINE_DEFAULTS = {
|
|
1930
|
+
"qa_tools_order": None,
|
|
1931
|
+
"coverage_threshold": 60,
|
|
1932
|
+
"golden_required": False,
|
|
1933
|
+
}
|
|
1934
|
+
ENGINE_DEFAULT_NOTES = {
|
|
1935
|
+
"qa_tools_order": "not declared - convergence uses a window of --tools-per-cycle "
|
|
1936
|
+
"agent steps",
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
def _resolved_defaults(cfg):
|
|
1941
|
+
"""(resolved defaults, origin per profile-owned key) for a RAW config -- the effective
|
|
1942
|
+
settings `doctor` reports. Origin is `override` (declared in defaults), `profile <X>`, or
|
|
1943
|
+
`default`. Read-only: never mutates cfg and never writes anything back."""
|
|
1944
|
+
raw = cfg.get("defaults") if isinstance(cfg, dict) else None
|
|
1945
|
+
raw = dict(raw) if isinstance(raw, dict) else {}
|
|
1946
|
+
declared = set(raw)
|
|
1947
|
+
profile = raw.get("risk_profile")
|
|
1948
|
+
expanded = _apply_risk_profile(dict(raw))
|
|
1949
|
+
resolved, origin = {}, {}
|
|
1950
|
+
for key, fallback in ENGINE_DEFAULTS.items():
|
|
1951
|
+
resolved[key] = expanded[key] if key in expanded else fallback
|
|
1952
|
+
if key in declared:
|
|
1953
|
+
origin[key] = "override"
|
|
1954
|
+
elif profile and key in RISK_PROFILES.get(profile, {}):
|
|
1955
|
+
origin[key] = "profile %s" % profile
|
|
1956
|
+
else:
|
|
1957
|
+
origin[key] = "default"
|
|
1958
|
+
return resolved, origin
|
|
1959
|
+
|
|
1960
|
+
|
|
1914
1961
|
def _validate_init_config(cfg):
|
|
1915
1962
|
"""Validate only the engine's core init contract before creating a ledger."""
|
|
1916
1963
|
if not isinstance(cfg, dict):
|
|
@@ -1921,6 +1968,9 @@ def _validate_init_config(cfg):
|
|
|
1921
1968
|
# expand a named risk profile into concrete knobs BEFORE validating them, so the merged
|
|
1922
1969
|
# values (qa_tools_order, coverage_threshold, golden_required) flow through the checks
|
|
1923
1970
|
# below. Explicit config wins per key; an unknown profile fails loud (INV-RISK-01).
|
|
1971
|
+
# Nothing else is written into `defaults`: a key the human did not declare and the profile
|
|
1972
|
+
# did not supply stays ABSENT, so provenance can still tell the kit's default apart from a
|
|
1973
|
+
# declaration (1.17.0). ENGINE_DEFAULTS is what `doctor` reports, never what init freezes.
|
|
1924
1974
|
_apply_risk_profile(defaults)
|
|
1925
1975
|
if "golden_required" in defaults and not isinstance(defaults["golden_required"], bool):
|
|
1926
1976
|
raise SystemExit("[qa_ledger] invalid config: golden_required must be a boolean")
|
|
@@ -12273,6 +12323,7 @@ def cmd_doctor(args):
|
|
|
12273
12323
|
|
|
12274
12324
|
# --- proyecto (si hay config aca) ---------------------------------------
|
|
12275
12325
|
qa_order = ["code-review", "judgment-day", "improve"] # default del kit
|
|
12326
|
+
effective, risk_profile = None, None
|
|
12276
12327
|
cfg_path = args.config or "uscha.config.json"
|
|
12277
12328
|
if os.path.isfile(cfg_path):
|
|
12278
12329
|
try:
|
|
@@ -12296,7 +12347,50 @@ def cmd_doctor(args):
|
|
|
12296
12347
|
warn(f"ACCEPTANCE {acc} has no criteria (zero checkboxes)")
|
|
12297
12348
|
elif acc:
|
|
12298
12349
|
warn(f"acceptance_file declared but missing: {acc}")
|
|
12299
|
-
|
|
12350
|
+
# effective settings, with the ORIGIN of each (2.0.0): the three-rung ladder --
|
|
12351
|
+
# explicit override > selected profile > engine default. Reported, never written
|
|
12352
|
+
# back. An override that supersedes the profile is reported as INFORMATION:
|
|
12353
|
+
# declaring a knob by hand is the documented way to bend a preset, never an error.
|
|
12354
|
+
#
|
|
12355
|
+
# An UNKNOWN profile is caught HERE rather than by the config-wide handler below.
|
|
12356
|
+
# `_apply_risk_profile` raises SystemExit on it (INV-RISK-01: a declared risk level
|
|
12357
|
+
# is never inert, and `init` refuses such a config) -- letting that escape would
|
|
12358
|
+
# abandon the toolchain, rubric and ledger checks that follow, so the diagnostic
|
|
12359
|
+
# would go blind on the first bad key instead of naming it. It is a WARN because
|
|
12360
|
+
# that is the verdict doctor gave this config before 2.0.0: resolving the profile
|
|
12361
|
+
# is a new REPORT, and a new report may not silently raise an existing exit code.
|
|
12362
|
+
risk_profile = defaults.get("risk_profile")
|
|
12363
|
+
try:
|
|
12364
|
+
resolved, origin = _resolved_defaults(cfg)
|
|
12365
|
+
except SystemExit:
|
|
12366
|
+
resolved, origin = None, None
|
|
12367
|
+
warn("unknown risk profile %s - no preset applied" % ascii(risk_profile),
|
|
12368
|
+
"valid: " + ", ".join(sorted(RISK_PROFILES))
|
|
12369
|
+
+ " - `init` refuses this config (INV-RISK-01); fix it before the loop "
|
|
12370
|
+
"runs. Every other check below still ran.")
|
|
12371
|
+
if origin is not None:
|
|
12372
|
+
effective = {k: {"value": resolved.get(k), "origin": origin[k]}
|
|
12373
|
+
for k in ENGINE_DEFAULTS}
|
|
12374
|
+
ok("risk profile: %s" % (risk_profile or "none declared"),
|
|
12375
|
+
"effective settings below - precedence: override > profile > default")
|
|
12376
|
+
for key in ENGINE_DEFAULTS:
|
|
12377
|
+
value = resolved.get(key)
|
|
12378
|
+
if isinstance(value, list):
|
|
12379
|
+
shown = ", ".join(value)
|
|
12380
|
+
elif value is None:
|
|
12381
|
+
shown = ENGINE_DEFAULT_NOTES.get(key, "not declared")
|
|
12382
|
+
else:
|
|
12383
|
+
shown = value
|
|
12384
|
+
detail = "origin: " + origin[key]
|
|
12385
|
+
if (origin[key] == "override" and risk_profile
|
|
12386
|
+
and key in RISK_PROFILES.get(risk_profile, {})):
|
|
12387
|
+
detail += (" - this override supersedes profile %s (information: an "
|
|
12388
|
+
"explicit declaration is how a preset is bent)"
|
|
12389
|
+
% risk_profile)
|
|
12390
|
+
ok("effective %s = %s" % (key, shown), detail)
|
|
12391
|
+
# the QA-skills check below asks for the tools actually in force; with no list
|
|
12392
|
+
# declared it keeps looking for the kit's three, as it always has
|
|
12393
|
+
qa_order = resolved.get("qa_tools_order") or qa_order
|
|
12300
12394
|
for r in repos:
|
|
12301
12395
|
tool = DOCTOR_TOOLS.get(r.get("type", ""))
|
|
12302
12396
|
if not tool:
|
|
@@ -12336,8 +12430,11 @@ def cmd_doctor(args):
|
|
|
12336
12430
|
err(f"{cfg_path} invalid", str(exc))
|
|
12337
12431
|
else:
|
|
12338
12432
|
warn(f"no {cfg_path} in this directory",
|
|
12339
|
-
"install:
|
|
12340
|
-
"
|
|
12433
|
+
"install: run `uscha init` here (or `python install-uscha.py init --repo .`) - it "
|
|
12434
|
+
"GENERATES a minimal one and detects the repo; then declare your quality bar. Do "
|
|
12435
|
+
"NOT copy the kit's uscha.config.json: it is a REFERENCE, and every knob in it "
|
|
12436
|
+
"would arrive as an explicit declaration outranking your risk_profile (ADR-001, "
|
|
12437
|
+
"as amended) - only needed to RUN the loop here")
|
|
12341
12438
|
|
|
12342
12439
|
# --- skills de QA del loop (externas al kit, se orquestan sin traerlas) --
|
|
12343
12440
|
# sin ellas la fase 3 (QA loop) no corre; chequeables con o sin config.
|
|
@@ -12361,6 +12458,9 @@ def cmd_doctor(args):
|
|
|
12361
12458
|
print(json.dumps({"verdict": "ERROR" if n_err else ("WARN" if n_warn else "OK"),
|
|
12362
12459
|
"ok": n_ok, "warnings": n_warn, "errors": n_err,
|
|
12363
12460
|
"global_install": is_global, "plugin_install": is_plugin,
|
|
12461
|
+
# effective settings + origin per knob (2.0.0); null when there is
|
|
12462
|
+
# no project config here to resolve them from
|
|
12463
|
+
"risk_profile": risk_profile, "effective": effective,
|
|
12364
12464
|
"checks": [{"level": lv, "title": t, "detail": d}
|
|
12365
12465
|
for lv, t, d in checks]},
|
|
12366
12466
|
indent=2, ensure_ascii=True))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"displayName": "Uscha",
|
|
6
6
|
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 53 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
|
|
7
7
|
"author": {
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:**
|
|
3
|
+
**Kit version:** v2.0.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`,
|
|
@@ -444,7 +444,60 @@ primary toolchain of each repo by type (its absence is a WARNING — it may live
|
|
|
444
444
|
|
|
445
445
|
## Configure
|
|
446
446
|
|
|
447
|
-
|
|
447
|
+
`uscha init` **generates** a minimal `uscha.config.json` for the project: its name, the repo it
|
|
448
|
+
detected (path, type, test command), `acceptance_file`, `id_granularity`, `max_iterations`, and
|
|
449
|
+
the `fast_path` block and the `integration` switch (no contract command: that names a build
|
|
450
|
+
system and is yours to add). Those last two are there because the engine defaults
|
|
451
|
+
both to OFF and the kit means them ON — without them `fastpath-eval` would answer
|
|
452
|
+
`DENY configured: false` on a fresh project and readiness would measure five dimensions instead
|
|
453
|
+
of six. Every other knob is absent on purpose and resolves to the engine default. The kit's own
|
|
454
|
+
`uscha-kit/uscha.config.json` is the **comprehensive reference** — every knob at the kit's
|
|
455
|
+
value, to read and copy from — and is no longer copied into projects.
|
|
456
|
+
|
|
457
|
+
That matters because of one rule: **a knob you declare > the preset named by
|
|
458
|
+
`defaults.risk_profile` > the engine default**. A copied default is an explicit declaration, so
|
|
459
|
+
a project holding the whole reference leaves its risk profile nothing to decide.
|
|
460
|
+
|
|
461
|
+
- `defaults.risk_profile`: `A`..`E` (ADR-001) — a named preset that expands into
|
|
462
|
+
`qa_tools_order`, `coverage_threshold` and `golden_required`. `A` (trivial) runs
|
|
463
|
+
`[code-review]` only; `E` (migration/legacy) requires the three tools, coverage 80 and an
|
|
464
|
+
approved golden. Absent = the kit defaults.
|
|
465
|
+
- Check what is actually in force, and where each value came from:
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
python3 ~/.claude/skills/uscha-devloop/qa_ledger.py doctor --json
|
|
469
|
+
# -> "risk_profile" and "effective": {knob: {value, origin}}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`origin` is `override` (you declared it), `profile <X>` (the preset supplied it), or
|
|
473
|
+
`default` (the engine's own value). An override that supersedes a profile is reported as
|
|
474
|
+
information, not an error — declaring a knob by hand is how a preset is bent. Nothing is
|
|
475
|
+
written back into your config: a knob nobody declared stays absent, which is what lets the
|
|
476
|
+
engine keep telling a requirement apart from a default. With no profile and no
|
|
477
|
+
`qa_tools_order`, that knob reads `not declared` and convergence uses a window of
|
|
478
|
+
`--tools-per-cycle` agent steps.
|
|
479
|
+
|
|
480
|
+
**Migrating a project initialised before 2.0.0.** Its config is the full copy, so
|
|
481
|
+
`coverage_threshold` and `qa_tools_order` read `origin: override` and no profile can move them.
|
|
482
|
+
(`golden_required` is the one the reference never declared, which is why a profile could still
|
|
483
|
+
supply it — that is the whole shape of the defect: the preset reached only the keys the copy
|
|
484
|
+
happened to omit.) Delete the ones you never meant to declare, then add the profile:
|
|
485
|
+
|
|
486
|
+
```diff
|
|
487
|
+
{
|
|
488
|
+
"defaults": {
|
|
489
|
+
- "coverage_threshold": 60,
|
|
490
|
+
- "qa_tools_order": ["code-review", "judgment-day", "improve"],
|
|
491
|
+
+ "risk_profile": "A",
|
|
492
|
+
"acceptance_file": "ACCEPTANCE.md"
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Nothing is deleted for you: a value equal to a former default cannot be told apart from a value
|
|
498
|
+
you chose, so existing configs keep behaving exactly as they did.
|
|
499
|
+
|
|
500
|
+
The full set of knobs, all optional:
|
|
448
501
|
|
|
449
502
|
- `repos[]`: name, `path` (relative to the primary repo), `type` (`maven`|`flutter`|`python`|`node`|`go`|`rust`|`dotnet`|`cpp`|`gradle`|`swift`).
|
|
450
503
|
- `defaults.coverage_threshold`: triggers the characterization phase if below it.
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit
|
|
1
|
+
uscha-kit 2.0.0
|
|
@@ -693,39 +693,144 @@ def _wire_statusline_settings(repo, force, dry_run):
|
|
|
693
693
|
return ops, wrote, conflicts
|
|
694
694
|
|
|
695
695
|
|
|
696
|
+
# The one marker file that names a repo's toolchain, most specific first: a flutter repo also
|
|
697
|
+
# has a pubspec, a gradle repo has no pom, and a python repo can carry a package.json for its
|
|
698
|
+
# tooling. `.csproj`/`.sln` need a glob, so dotnet is probed separately.
|
|
699
|
+
REPO_TYPE_MARKERS = (
|
|
700
|
+
("pubspec.yaml", "flutter"), ("pom.xml", "maven"), ("build.gradle", "gradle"),
|
|
701
|
+
("build.gradle.kts", "gradle"), ("go.mod", "go"), ("Cargo.toml", "rust"),
|
|
702
|
+
("Package.swift", "swift"), ("pyproject.toml", "python"), ("setup.py", "python"),
|
|
703
|
+
("CMakeLists.txt", "cpp"), ("package.json", "node"),
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
GENERATED_CONFIG_NOTE = (
|
|
707
|
+
"Minimal project config, generated by `uscha init`. Every knob NOT declared here resolves "
|
|
708
|
+
"to the engine default -- the kit's own uscha.config.json is the comprehensive reference "
|
|
709
|
+
"for what can be declared. Precedence is: a knob you write here > the preset named by "
|
|
710
|
+
"defaults.risk_profile (A..E, ADR-001) > the engine default. That is why this file is "
|
|
711
|
+
"minimal: a copied default is an explicit declaration, and an explicit declaration "
|
|
712
|
+
"outranks the preset you selected. The blocks that ARE here are the ones the kit means to "
|
|
713
|
+
"be on and the engine defaults to off (fast_path, integration) plus what the engine cannot "
|
|
714
|
+
"derive -- never a knob a risk profile owns."
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def detect_repo_type(repo):
|
|
719
|
+
"""The repo's toolchain, read off the one marker file each build system leaves at its root.
|
|
720
|
+
None when nothing is recognised -- the human declares `repos[]` by hand, and `init` says so
|
|
721
|
+
instead of guessing. Never raises: a missing directory (`--dry-run` on a path that does not
|
|
722
|
+
exist yet) simply has no markers."""
|
|
723
|
+
if not repo.is_dir():
|
|
724
|
+
return None
|
|
725
|
+
for marker, repo_type in REPO_TYPE_MARKERS:
|
|
726
|
+
if (repo / marker).is_file():
|
|
727
|
+
return repo_type
|
|
728
|
+
return "dotnet" if any(repo.glob("*.csproj")) or any(repo.glob("*.sln")) else None
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _kit_value(block, key, prefix=""):
|
|
732
|
+
"""One value the generated config copies from the kit reference. A missing key means the
|
|
733
|
+
kit's own uscha.config.json has been edited into something `init` cannot generate from --
|
|
734
|
+
a NAMED refusal, never a raw KeyError traceback out of the installer. `prefix` is only for
|
|
735
|
+
that message, so it names the path the human has to go and look at."""
|
|
736
|
+
if not isinstance(block, dict) or key not in block:
|
|
737
|
+
raise InstallError("[install-uscha] kit uscha.config.json has no %s%s - cannot "
|
|
738
|
+
"generate a project config from it" % (prefix, key))
|
|
739
|
+
return block[key]
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def project_config_bytes(repo):
|
|
743
|
+
"""The project's uscha.config.json, GENERATED rather than copied (kit 2.0.0).
|
|
744
|
+
|
|
745
|
+
Copying the kit's reference config turned every kit default into an explicit project
|
|
746
|
+
declaration, and an explicit declaration outranks the selected `risk_profile` by design
|
|
747
|
+
(ADR-001) -- so `risk_profile: "A"` still demanded judgment-day and improve, and `"E"`
|
|
748
|
+
still measured coverage against 60. The presets had never taken effect in a repo
|
|
749
|
+
initialised with `init`. What is written here is only what the engine cannot derive:
|
|
750
|
+
identity, the repos, the knobs whose kit intent differs from the engine default, and the
|
|
751
|
+
ones the SKILL reads (the engine has no fallback for those). Everything else is absent on
|
|
752
|
+
purpose, so the preset -- once the human declares one -- has something left to decide.
|
|
753
|
+
|
|
754
|
+
"Minimal" is not "empty", and the line is drawn at INTENT, never at size: a knob the kit
|
|
755
|
+
means to be on and the engine defaults to off must be written, or generating instead of
|
|
756
|
+
copying would silently turn features off. `fast_path` (engine: absent = fail-closed, so
|
|
757
|
+
`fastpath-eval` would answer DENY `configured: false` on every fresh project) and
|
|
758
|
+
`integration` (engine: disabled, so readiness would measure five dimensions instead of
|
|
759
|
+
six) are exactly that. `fast_path` is copied as a WHOLE block from the reference;
|
|
760
|
+
`integration` carries only its switch, because its contract command names a build system
|
|
761
|
+
and guessing one is what this generator exists to refuse. No knob any risk
|
|
762
|
+
profile owns is written here -- that is the defect this release exists to fix, and
|
|
763
|
+
`AC-RP-06` measures it against the engine's own table."""
|
|
764
|
+
kit_cfg = load_json(KIT_ROOT / "uscha.config.json", "kit uscha.config.json")
|
|
765
|
+
kit_defaults = kit_cfg.get("defaults")
|
|
766
|
+
defaults = {
|
|
767
|
+
# engine default is None: without it readiness has no acceptance list to measure
|
|
768
|
+
"acceptance_file": _kit_value(kit_defaults, "acceptance_file", "defaults."),
|
|
769
|
+
# engine default is "line"; the kit's chosen value is "file" (stable under refactors)
|
|
770
|
+
"id_granularity": _kit_value(kit_defaults, "id_granularity", "defaults."),
|
|
771
|
+
# read by the uscha-devloop SKILL, never by the engine, so it has no fallback
|
|
772
|
+
"max_iterations": _kit_value(kit_defaults, "max_iterations", "defaults."),
|
|
773
|
+
}
|
|
774
|
+
repos = []
|
|
775
|
+
repo_type = detect_repo_type(repo)
|
|
776
|
+
if repo_type:
|
|
777
|
+
key = "test_command_" + repo_type
|
|
778
|
+
defaults[key] = _kit_value(kit_defaults, key, "defaults.")
|
|
779
|
+
repos.append({"name": repo.name, "path": ".", "type": repo_type})
|
|
780
|
+
# engine default: absent means NOT configured, and fastpath-eval fails closed on it
|
|
781
|
+
defaults["fast_path"] = _kit_value(kit_defaults, "fast_path", "defaults.")
|
|
782
|
+
config = {"_comment": GENERATED_CONFIG_NOTE, "version": source_version(),
|
|
783
|
+
"project": repo.name, "defaults": defaults, "repos": repos,
|
|
784
|
+
# top-level, not a default: engine default is disabled, which drops the
|
|
785
|
+
# integration dimension out of readiness entirely
|
|
786
|
+
# integration dimension out of readiness entirely. Only the switch: the
|
|
787
|
+
# contract command is a build-system guess the kit refuses to make
|
|
788
|
+
"integration": {"enabled": True}}
|
|
789
|
+
# byte-identical across runs on the same repo + kit, so a second `init` reports `unchanged`
|
|
790
|
+
# instead of a conflict; LF explicitly, like every other artifact the kit writes.
|
|
791
|
+
return (json.dumps(config, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
|
|
792
|
+
|
|
793
|
+
|
|
696
794
|
def cmd_init(args):
|
|
697
795
|
repo, operations, conflicts = Path(args.repo).expanduser().resolve(), [], []
|
|
698
|
-
|
|
699
|
-
|
|
796
|
+
# (source path or None, payload bytes or None, target): the config is generated, the rest
|
|
797
|
+
# are copied from templates/. Exactly one of the first two is set per entry.
|
|
798
|
+
sources = ([(None, project_config_bytes(repo), repo / "uscha.config.json")]
|
|
799
|
+
+ [(KIT_ROOT / "templates" / name, None, repo / name)
|
|
700
800
|
# AGENTS.md (kit 1.50.2): the context file Codex/pi read (they do not read
|
|
701
801
|
# CLAUDE.md); shipped as a thin pointer to CLAUDE.md so there is ONE source.
|
|
702
802
|
for name in ("CLAUDE.md", "AGENTS.md", "CONSTITUTION.md", ".gitattributes")]
|
|
703
|
-
+ [(KIT_ROOT / "templates" / "scripts" / s, repo / ".claude" / "scripts" / s)
|
|
803
|
+
+ [(KIT_ROOT / "templates" / "scripts" / s, None, repo / ".claude" / "scripts" / s)
|
|
704
804
|
for s in STATUSLINE_SCRIPTS])
|
|
705
805
|
copies = []
|
|
706
|
-
for source, target in sources:
|
|
707
|
-
if not source.is_file():
|
|
806
|
+
for source, payload, target in sources:
|
|
807
|
+
if source is not None and not source.is_file():
|
|
708
808
|
raise InstallError("[install-uscha] init source missing: %s" % source)
|
|
709
809
|
if target.is_symlink():
|
|
710
810
|
raise InstallError("[install-uscha] init target must not be a symlink: %s" % target)
|
|
711
811
|
if target.exists() and not target.is_file():
|
|
712
812
|
raise InstallError("[install-uscha] init target must be a file: %s" % target)
|
|
713
|
-
if
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
813
|
+
want = payload if payload is not None else source.read_bytes()
|
|
814
|
+
label = str(source) if source is not None else "<generated>"
|
|
815
|
+
if target.exists() and target.read_bytes() != want and not args.force:
|
|
816
|
+
conflicts.append({"path": str(target), "source": label})
|
|
817
|
+
operations.append({"action": "conflict", "path": str(target), "source": label})
|
|
818
|
+
elif target.exists() and target.read_bytes() == want:
|
|
717
819
|
operations.append({"action": "unchanged", "path": str(target)})
|
|
718
820
|
else:
|
|
719
|
-
operations.append({"action": "copy-file", "path": str(target), "source":
|
|
720
|
-
copies.append((source, target))
|
|
821
|
+
operations.append({"action": "copy-file", "path": str(target), "source": label, "note": "force" if target.exists() else None})
|
|
822
|
+
copies.append((source, want, target))
|
|
721
823
|
# per-file, not all-or-nothing (kit 1.44.1): a differing CLAUDE.md (which EVERY repo
|
|
722
824
|
# already using Claude Code has) used to block ALL four copies. Now the non-conflicting
|
|
723
825
|
# files are written regardless; each conflict is reported and left untouched (resolve by
|
|
724
826
|
# hand, or re-run with --force). Exit stays nonzero while any conflict remains.
|
|
725
827
|
if not args.dry_run:
|
|
726
|
-
for source, target in copies:
|
|
828
|
+
for source, want, target in copies:
|
|
727
829
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
728
|
-
|
|
830
|
+
if source is None:
|
|
831
|
+
target.write_bytes(want)
|
|
832
|
+
else:
|
|
833
|
+
shutil.copy2(source, target)
|
|
729
834
|
# wire the statusline (kit 1.46.0): merge statusLine + Stop hook into the project's
|
|
730
835
|
# settings.json so the user never edits it by hand -- never clobbering existing keys.
|
|
731
836
|
sl_ops, sl_wrote, sl_conflicts = _wire_statusline_settings(repo, args.force, args.dry_run)
|
|
@@ -736,7 +841,7 @@ def cmd_init(args):
|
|
|
736
841
|
status = "conflicts" if args.dry_run else "partial"
|
|
737
842
|
else:
|
|
738
843
|
status = "planned" if args.dry_run else "initialized"
|
|
739
|
-
wrote = [str(t) for _, t in copies] if not args.dry_run else []
|
|
844
|
+
wrote = [str(t) for _, _, t in copies] if not args.dry_run else []
|
|
740
845
|
if sl_wrote:
|
|
741
846
|
wrote.append(str(repo / ".claude" / "settings.json"))
|
|
742
847
|
emit({"status": status, "dry_run": args.dry_run, "repo": str(repo),
|
|
@@ -314,7 +314,16 @@ profile A (trivial change) skip it. It **collapses into `readiness`** as a `gate
|
|
|
314
314
|
## Phase 3 — QA loop (per repo)
|
|
315
315
|
|
|
316
316
|
Run the tools in `config.defaults.qa_tools_order` (default: code-review → judgment-day
|
|
317
|
-
→ improve). One pass of all tools = one cycle.
|
|
317
|
+
→ improve). One pass of all tools = one cycle.
|
|
318
|
+
|
|
319
|
+
Read the EFFECTIVE order, not the config file: since 2.0.0 `uscha init` generates a minimal
|
|
320
|
+
config, so `qa_tools_order` is often absent there and resolves from `defaults.risk_profile`
|
|
321
|
+
(ADR-001) or from the engine default. `qa_ledger.py doctor --json` prints it as
|
|
322
|
+
`effective.qa_tools_order` with its `origin`; the ledger froze the same value at `init`. A
|
|
323
|
+
project on **profile A runs `code-review` only** — do NOT invoke judgment-day or improve there,
|
|
324
|
+
and convergence must not wait for them.
|
|
325
|
+
|
|
326
|
+
After **each** tool pass:
|
|
318
327
|
|
|
319
328
|
1. Apply only fixes at/above the severity gate. Send the rest to `ISSUES-DEFERRED.md`.
|
|
320
329
|
2. Run the repo test command. If red and the fix isn't obvious → escalate.
|
|
@@ -1911,6 +1911,53 @@ def _apply_risk_profile(defaults):
|
|
|
1911
1911
|
return defaults
|
|
1912
1912
|
|
|
1913
1913
|
|
|
1914
|
+
# The engine's OWN value for each knob a risk profile owns (kit 2.0.0): the bottom rung of
|
|
1915
|
+
# the precedence ladder -- explicit override > selected profile > engine default -- as `doctor`
|
|
1916
|
+
# reports it, so the origin of every effective value is visible without reading the source.
|
|
1917
|
+
#
|
|
1918
|
+
# It is a REPORTING table and is deliberately NEVER materialized into `defaults`. Writing an
|
|
1919
|
+
# engine default into the config would make the kit's own value indistinguishable from a human
|
|
1920
|
+
# declaration, which is precisely what `thresholds_declared` / `declared_caps` exist to keep
|
|
1921
|
+
# apart (provenance, kit 1.17.0) -- and precisely the confusion that let a COPIED config
|
|
1922
|
+
# outrank the profile until 2.0.0. The fix for that was to stop copying, not to start
|
|
1923
|
+
# injecting: `uscha init` now generates a minimal config (install-uscha.py), and a knob it
|
|
1924
|
+
# leaves out keeps meaning "not declared" everywhere the engine reads it.
|
|
1925
|
+
#
|
|
1926
|
+
# qa_tools_order is None on purpose: with no list declared, convergence falls back to a window
|
|
1927
|
+
# of --tools-per-cycle agent steps, so there is no default list to name and saying otherwise
|
|
1928
|
+
# would be a narrated claim.
|
|
1929
|
+
ENGINE_DEFAULTS = {
|
|
1930
|
+
"qa_tools_order": None,
|
|
1931
|
+
"coverage_threshold": 60,
|
|
1932
|
+
"golden_required": False,
|
|
1933
|
+
}
|
|
1934
|
+
ENGINE_DEFAULT_NOTES = {
|
|
1935
|
+
"qa_tools_order": "not declared - convergence uses a window of --tools-per-cycle "
|
|
1936
|
+
"agent steps",
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
def _resolved_defaults(cfg):
|
|
1941
|
+
"""(resolved defaults, origin per profile-owned key) for a RAW config -- the effective
|
|
1942
|
+
settings `doctor` reports. Origin is `override` (declared in defaults), `profile <X>`, or
|
|
1943
|
+
`default`. Read-only: never mutates cfg and never writes anything back."""
|
|
1944
|
+
raw = cfg.get("defaults") if isinstance(cfg, dict) else None
|
|
1945
|
+
raw = dict(raw) if isinstance(raw, dict) else {}
|
|
1946
|
+
declared = set(raw)
|
|
1947
|
+
profile = raw.get("risk_profile")
|
|
1948
|
+
expanded = _apply_risk_profile(dict(raw))
|
|
1949
|
+
resolved, origin = {}, {}
|
|
1950
|
+
for key, fallback in ENGINE_DEFAULTS.items():
|
|
1951
|
+
resolved[key] = expanded[key] if key in expanded else fallback
|
|
1952
|
+
if key in declared:
|
|
1953
|
+
origin[key] = "override"
|
|
1954
|
+
elif profile and key in RISK_PROFILES.get(profile, {}):
|
|
1955
|
+
origin[key] = "profile %s" % profile
|
|
1956
|
+
else:
|
|
1957
|
+
origin[key] = "default"
|
|
1958
|
+
return resolved, origin
|
|
1959
|
+
|
|
1960
|
+
|
|
1914
1961
|
def _validate_init_config(cfg):
|
|
1915
1962
|
"""Validate only the engine's core init contract before creating a ledger."""
|
|
1916
1963
|
if not isinstance(cfg, dict):
|
|
@@ -1921,6 +1968,9 @@ def _validate_init_config(cfg):
|
|
|
1921
1968
|
# expand a named risk profile into concrete knobs BEFORE validating them, so the merged
|
|
1922
1969
|
# values (qa_tools_order, coverage_threshold, golden_required) flow through the checks
|
|
1923
1970
|
# below. Explicit config wins per key; an unknown profile fails loud (INV-RISK-01).
|
|
1971
|
+
# Nothing else is written into `defaults`: a key the human did not declare and the profile
|
|
1972
|
+
# did not supply stays ABSENT, so provenance can still tell the kit's default apart from a
|
|
1973
|
+
# declaration (1.17.0). ENGINE_DEFAULTS is what `doctor` reports, never what init freezes.
|
|
1924
1974
|
_apply_risk_profile(defaults)
|
|
1925
1975
|
if "golden_required" in defaults and not isinstance(defaults["golden_required"], bool):
|
|
1926
1976
|
raise SystemExit("[qa_ledger] invalid config: golden_required must be a boolean")
|
|
@@ -12273,6 +12323,7 @@ def cmd_doctor(args):
|
|
|
12273
12323
|
|
|
12274
12324
|
# --- proyecto (si hay config aca) ---------------------------------------
|
|
12275
12325
|
qa_order = ["code-review", "judgment-day", "improve"] # default del kit
|
|
12326
|
+
effective, risk_profile = None, None
|
|
12276
12327
|
cfg_path = args.config or "uscha.config.json"
|
|
12277
12328
|
if os.path.isfile(cfg_path):
|
|
12278
12329
|
try:
|
|
@@ -12296,7 +12347,50 @@ def cmd_doctor(args):
|
|
|
12296
12347
|
warn(f"ACCEPTANCE {acc} has no criteria (zero checkboxes)")
|
|
12297
12348
|
elif acc:
|
|
12298
12349
|
warn(f"acceptance_file declared but missing: {acc}")
|
|
12299
|
-
|
|
12350
|
+
# effective settings, with the ORIGIN of each (2.0.0): the three-rung ladder --
|
|
12351
|
+
# explicit override > selected profile > engine default. Reported, never written
|
|
12352
|
+
# back. An override that supersedes the profile is reported as INFORMATION:
|
|
12353
|
+
# declaring a knob by hand is the documented way to bend a preset, never an error.
|
|
12354
|
+
#
|
|
12355
|
+
# An UNKNOWN profile is caught HERE rather than by the config-wide handler below.
|
|
12356
|
+
# `_apply_risk_profile` raises SystemExit on it (INV-RISK-01: a declared risk level
|
|
12357
|
+
# is never inert, and `init` refuses such a config) -- letting that escape would
|
|
12358
|
+
# abandon the toolchain, rubric and ledger checks that follow, so the diagnostic
|
|
12359
|
+
# would go blind on the first bad key instead of naming it. It is a WARN because
|
|
12360
|
+
# that is the verdict doctor gave this config before 2.0.0: resolving the profile
|
|
12361
|
+
# is a new REPORT, and a new report may not silently raise an existing exit code.
|
|
12362
|
+
risk_profile = defaults.get("risk_profile")
|
|
12363
|
+
try:
|
|
12364
|
+
resolved, origin = _resolved_defaults(cfg)
|
|
12365
|
+
except SystemExit:
|
|
12366
|
+
resolved, origin = None, None
|
|
12367
|
+
warn("unknown risk profile %s - no preset applied" % ascii(risk_profile),
|
|
12368
|
+
"valid: " + ", ".join(sorted(RISK_PROFILES))
|
|
12369
|
+
+ " - `init` refuses this config (INV-RISK-01); fix it before the loop "
|
|
12370
|
+
"runs. Every other check below still ran.")
|
|
12371
|
+
if origin is not None:
|
|
12372
|
+
effective = {k: {"value": resolved.get(k), "origin": origin[k]}
|
|
12373
|
+
for k in ENGINE_DEFAULTS}
|
|
12374
|
+
ok("risk profile: %s" % (risk_profile or "none declared"),
|
|
12375
|
+
"effective settings below - precedence: override > profile > default")
|
|
12376
|
+
for key in ENGINE_DEFAULTS:
|
|
12377
|
+
value = resolved.get(key)
|
|
12378
|
+
if isinstance(value, list):
|
|
12379
|
+
shown = ", ".join(value)
|
|
12380
|
+
elif value is None:
|
|
12381
|
+
shown = ENGINE_DEFAULT_NOTES.get(key, "not declared")
|
|
12382
|
+
else:
|
|
12383
|
+
shown = value
|
|
12384
|
+
detail = "origin: " + origin[key]
|
|
12385
|
+
if (origin[key] == "override" and risk_profile
|
|
12386
|
+
and key in RISK_PROFILES.get(risk_profile, {})):
|
|
12387
|
+
detail += (" - this override supersedes profile %s (information: an "
|
|
12388
|
+
"explicit declaration is how a preset is bent)"
|
|
12389
|
+
% risk_profile)
|
|
12390
|
+
ok("effective %s = %s" % (key, shown), detail)
|
|
12391
|
+
# the QA-skills check below asks for the tools actually in force; with no list
|
|
12392
|
+
# declared it keeps looking for the kit's three, as it always has
|
|
12393
|
+
qa_order = resolved.get("qa_tools_order") or qa_order
|
|
12300
12394
|
for r in repos:
|
|
12301
12395
|
tool = DOCTOR_TOOLS.get(r.get("type", ""))
|
|
12302
12396
|
if not tool:
|
|
@@ -12336,8 +12430,11 @@ def cmd_doctor(args):
|
|
|
12336
12430
|
err(f"{cfg_path} invalid", str(exc))
|
|
12337
12431
|
else:
|
|
12338
12432
|
warn(f"no {cfg_path} in this directory",
|
|
12339
|
-
"install:
|
|
12340
|
-
"
|
|
12433
|
+
"install: run `uscha init` here (or `python install-uscha.py init --repo .`) - it "
|
|
12434
|
+
"GENERATES a minimal one and detects the repo; then declare your quality bar. Do "
|
|
12435
|
+
"NOT copy the kit's uscha.config.json: it is a REFERENCE, and every knob in it "
|
|
12436
|
+
"would arrive as an explicit declaration outranking your risk_profile (ADR-001, "
|
|
12437
|
+
"as amended) - only needed to RUN the loop here")
|
|
12341
12438
|
|
|
12342
12439
|
# --- skills de QA del loop (externas al kit, se orquestan sin traerlas) --
|
|
12343
12440
|
# sin ellas la fase 3 (QA loop) no corre; chequeables con o sin config.
|
|
@@ -12361,6 +12458,9 @@ def cmd_doctor(args):
|
|
|
12361
12458
|
print(json.dumps({"verdict": "ERROR" if n_err else ("WARN" if n_warn else "OK"),
|
|
12362
12459
|
"ok": n_ok, "warnings": n_warn, "errors": n_err,
|
|
12363
12460
|
"global_install": is_global, "plugin_install": is_plugin,
|
|
12461
|
+
# effective settings + origin per knob (2.0.0); null when there is
|
|
12462
|
+
# no project config here to resolve them from
|
|
12463
|
+
"risk_profile": risk_profile, "effective": effective,
|
|
12364
12464
|
"checks": [{"level": lv, "title": t, "detail": d}
|
|
12365
12465
|
for lv, t, d in checks]},
|
|
12366
12466
|
indent=2, ensure_ascii=True))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"_comment": "COMPREHENSIVE REFERENCE, not a project config. Every knob the engine and the skills understand, at the kit's own value, so a human can read what can be declared. `uscha init` does NOT copy this file: it GENERATES a minimal project config, because a copied default is an explicit declaration and an explicit declaration outranks the preset named by defaults.risk_profile (ADR-001, as amended). Copy a block from here into your project only when you mean to override the engine default or the preset. NOTE: no version string may be written into this comment -- the release script requires exactly one occurrence of the version in this file (I3), and a second one refuses the next release.",
|
|
3
|
+
"version": "2.0.0",
|
|
3
4
|
"project": null,
|
|
4
5
|
"defaults": {
|
|
5
6
|
"coverage_threshold": 60,
|