@andresmassello/uscha 1.65.0 → 1.67.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 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 v1.65.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.67.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": "1.65.0",
3
+ "version": "1.67.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",
@@ -488,6 +488,19 @@ count, plateau flag per repo — to `ledger["measured"]` (what the statusline re
488
488
  Without it the trail starves: the mirador shows "no history yet" and the statusline
489
489
  falls back to counting checkboxes. Recording is append-only facts, never a gate.
490
490
 
491
+ Right after readiness, run the spec-maintenance advisory (kit 1.66.0):
492
+
493
+ ```bash
494
+ python3 $QL spec-drift --repo <REPO>
495
+ ```
496
+
497
+ Milliseconds, deterministic, exit 0 always — it never gates, so running it every pass
498
+ adds zero ceremony. What it adds is VISIBILITY: the run lands in the ledger, so the
499
+ mirador card and `/uscha-status` show drift without anyone remembering the command —
500
+ the user this advisory exists for is precisely the one who never types it. If any doc
501
+ reads `SPEC_STALE`, mention it in the close block's `Blocks:` line as advisory context
502
+ (it blocks nothing; it informs the human's next conversation).
503
+
491
504
  **Single-verdict view (kit 1.25.0, anti-ceremony).** By default `readiness` is ONE
492
505
  screen: the verdict line, any conditional warning that actually fired (it speaks only
493
506
  when it matters), and a `--- gates:` line that COLLAPSES every persisted gate record —
@@ -1868,6 +1868,15 @@ def _origin_label(origin):
1868
1868
  return "%s/%s" % (sha, state)
1869
1869
 
1870
1870
 
1871
+ def _scope_path(ledger, name):
1872
+ """Repo path for a scope, guarding the SYNTHETIC `integration` scope -- never present
1873
+ in config["repos"], where _repo_cfg exits on unknown names. This crash class shipped
1874
+ once (the clean-room gate, 1.63.0) and nearly shipped twice (spec-drift at pass close,
1875
+ caught by fresh review): two recurrences make it a helper, not a per-site pattern."""
1876
+ cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": "."}
1877
+ return cfg.get("path", ".")
1878
+
1879
+
1871
1880
  def _evidence_origin(repo_path):
1872
1881
  """WHERE the evidence came from: the commit it was measured at, and whether the tree
1873
1882
  was clean (ADR-007). The engine's freshness check compares file MTIMES, so until now a
@@ -3145,7 +3154,7 @@ def cmd_spec_drift(args):
3145
3154
  cfg = ledger["config"].get("defaults", {}).get("spec_drift") or {}
3146
3155
  lag_days = int(args.max_lag_days if args.max_lag_days is not None
3147
3156
  else cfg.get("max_lag_days", 30))
3148
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3157
+ repo_path = _scope_path(ledger, args.repo)
3149
3158
 
3150
3159
  # The spec surface is fixed by ADR-005: the repo SPEC.md plus every ADR.
3151
3160
  spec_files = []
@@ -3756,7 +3765,7 @@ def cmd_roundtrip(args):
3756
3765
  (ADR-011). Advisory end to end: exit 0 always, a report, never a gate."""
3757
3766
  ledger = _load(args.ledger)
3758
3767
  _repo_node(ledger, args.repo)
3759
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3768
+ repo_path = _scope_path(ledger, args.repo)
3760
3769
  st = _curation_state(repo_path)
3761
3770
  if st is None:
3762
3771
  print("ROUNDTRIP %s: no %s/ directory -- feature unused, nothing to trace."
@@ -3790,6 +3799,12 @@ def cmd_roundtrip(args):
3790
3799
  out = {"repo": args.repo, "promoted": len(promoted), "covered": len(covered),
3791
3800
  "missing": missing, "advisory": True,
3792
3801
  "coverage_pct": round(100.0 * len(covered) / len(promoted), 1) if promoted else None}
3802
+ # Latest-state record so the mirador/status can surface it without anyone re-running the
3803
+ # command (the spec_drift pattern). A report that evaporates on exit is invisible to
3804
+ # every read surface -- which defeats the point of an advisory (found by auditing which
3805
+ # features actually REACH the user). Advisory data: no step counter, no gate record.
3806
+ ledger["roundtrip"] = dict(out, at=_now())
3807
+ _save(args.ledger, ledger)
3793
3808
  if args.json:
3794
3809
  print(json.dumps(out, indent=2, ensure_ascii=False))
3795
3810
  else:
@@ -3812,7 +3827,7 @@ def cmd_curation_check(args):
3812
3827
  candidate judged, or the feature unused."""
3813
3828
  ledger = _load(args.ledger)
3814
3829
  _repo_node(ledger, args.repo)
3815
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3830
+ repo_path = _scope_path(ledger, args.repo)
3816
3831
  st = _curation_state(repo_path)
3817
3832
  if st is None:
3818
3833
  if args.json:
@@ -4896,6 +4911,8 @@ def cmd_dashboard(args):
4896
4911
  if ledger.get(CLEAN_ROOM_KEY):
4897
4912
  out["clean_room"] = {r: [e for e in ledger[CLEAN_ROOM_KEY] if e.get("repo") == r][-1]
4898
4913
  for r in {e.get("repo") for e in ledger[CLEAN_ROOM_KEY]}}
4914
+ if ledger.get("roundtrip"):
4915
+ out["roundtrip"] = ledger["roundtrip"]
4899
4916
  if getattr(args, "json", False):
4900
4917
  print(json.dumps(out, indent=2, ensure_ascii=False))
4901
4918
  return
@@ -665,8 +665,8 @@ function renderModes(){
665
665
  ledger fact visible. BOTH keys are conditional in dashboard --json, so a project
666
666
  that never used either sees no card at all (absent block = identical view). */
667
667
  const card=$("#card-modes");if(!card)return;
668
- const fp=DATA.fast_path,sd=DATA.spec_drift;
669
- if(!fp&&!sd){card.hidden=true;return;}
668
+ const fp=DATA.fast_path,sd=DATA.spec_drift,cr=DATA.clean_room;
669
+ if(!fp&&!sd&&!cr){card.hidden=true;return;}
670
670
  const host=$("#modes");host.replaceChildren();
671
671
  const parts=[];
672
672
  if(fp){
@@ -677,6 +677,16 @@ function renderModes(){
677
677
  node("span","chip "+(cls[e.verdict]||"st-todo"),String(e.verdict||"?"))));});
678
678
  parts.push(Object.keys(fp).length+" repo"+(Object.keys(fp).length>1?"s":""));
679
679
  }
680
+ if(cr){
681
+ const cls={GREEN:"st-done",RED:"st-block"};
682
+ Object.keys(cr).forEach(r=>{const e=cr[r]||{};
683
+ const sha=(e.ref||"").slice(0,8)||"?";
684
+ const wall=(typeof e.wall_ms==="number")?(" · "+(e.wall_ms/1000).toFixed(1)+"s"):"";
685
+ append(host,append(node("div","spec"),node("span","id","clean-room"),
686
+ node("span","t",r+" @ "+sha+wall),
687
+ node("span","chip "+(cls[e.status]||"st-prog"),String(e.status||"?"))));});
688
+ parts.push("clean-room ×"+Object.keys(cr).length);
689
+ }
680
690
  if(sd){
681
691
  const cls={SPEC_STALE:"st-block",CLEAN:"st-done",UNMAPPED:"st-todo",UNTRACKED:"st-todo"};
682
692
  const res=sd.results||[];
@@ -100,6 +100,15 @@ nothing and gates nothing.
100
100
  `spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
101
101
  stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
102
102
 
103
+ **Clean-room (ADR-008):** if the ledger carries `clean_room` runs, add ONE line with the
104
+ latest run per repo: `clean-room: GREEN @ <sha8> (12.3s)` / `RED @ <sha8>`. When the gate is
105
+ declared (`mode: "final"`) and the latest run does not match the current HEAD, append
106
+ `(stale for gate)`. Absent key → no line.
107
+
108
+ **Roundtrip (ADR-009 slice 2):** if the ledger carries a `roundtrip` run, add ONE line:
109
+ `roundtrip: N/M promoted traceable by uscha-spec id (advisory)`. Absent key → no line —
110
+ silence is honest when the loop was never measured.
111
+
103
112
  ## Degradation (honest, specific)
104
113
 
105
114
  - `measured` missing entirely → print: *"No measurement recorded yet — the trail
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "uscha",
4
- "version": "1.65.0",
4
+ "version": "1.67.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, 35 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
7
7
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "1.65.0",
3
+ "version": "1.67.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:** v1.65.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.67.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`,
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 1.65.0
1
+ uscha-kit 1.67.0
@@ -1 +1 @@
1
- {"AC-RD-07": true, "AC-RD-01": true, "AC-RD-02": true, "AC-RD-03": true, "AC-RD-06": true, "AC-RD-04": true, "AC-RD-05": true}
1
+ {"AC-RD-07": true, "AC-RD-01": true, "AC-RD-02": true, "AC-RD-03": true, "AC-RD-06": true, "AC-RD-04": true, "AC-RD-05": true, "AC-RD-13": true}
@@ -1 +1 @@
1
- {"AC-RD-08": true, "AC-RD-09": true, "AC-RD-10": true, "AC-RD-11": true}
1
+ {"AC-RD-08": true, "AC-RD-09": true, "AC-RD-10": true, "AC-RD-11": true, "AC-RD-12": true}
@@ -488,6 +488,19 @@ count, plateau flag per repo — to `ledger["measured"]` (what the statusline re
488
488
  Without it the trail starves: the mirador shows "no history yet" and the statusline
489
489
  falls back to counting checkboxes. Recording is append-only facts, never a gate.
490
490
 
491
+ Right after readiness, run the spec-maintenance advisory (kit 1.66.0):
492
+
493
+ ```bash
494
+ python3 $QL spec-drift --repo <REPO>
495
+ ```
496
+
497
+ Milliseconds, deterministic, exit 0 always — it never gates, so running it every pass
498
+ adds zero ceremony. What it adds is VISIBILITY: the run lands in the ledger, so the
499
+ mirador card and `/uscha-status` show drift without anyone remembering the command —
500
+ the user this advisory exists for is precisely the one who never types it. If any doc
501
+ reads `SPEC_STALE`, mention it in the close block's `Blocks:` line as advisory context
502
+ (it blocks nothing; it informs the human's next conversation).
503
+
491
504
  **Single-verdict view (kit 1.25.0, anti-ceremony).** By default `readiness` is ONE
492
505
  screen: the verdict line, any conditional warning that actually fired (it speaks only
493
506
  when it matters), and a `--- gates:` line that COLLAPSES every persisted gate record —
@@ -1868,6 +1868,15 @@ def _origin_label(origin):
1868
1868
  return "%s/%s" % (sha, state)
1869
1869
 
1870
1870
 
1871
+ def _scope_path(ledger, name):
1872
+ """Repo path for a scope, guarding the SYNTHETIC `integration` scope -- never present
1873
+ in config["repos"], where _repo_cfg exits on unknown names. This crash class shipped
1874
+ once (the clean-room gate, 1.63.0) and nearly shipped twice (spec-drift at pass close,
1875
+ caught by fresh review): two recurrences make it a helper, not a per-site pattern."""
1876
+ cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": "."}
1877
+ return cfg.get("path", ".")
1878
+
1879
+
1871
1880
  def _evidence_origin(repo_path):
1872
1881
  """WHERE the evidence came from: the commit it was measured at, and whether the tree
1873
1882
  was clean (ADR-007). The engine's freshness check compares file MTIMES, so until now a
@@ -3145,7 +3154,7 @@ def cmd_spec_drift(args):
3145
3154
  cfg = ledger["config"].get("defaults", {}).get("spec_drift") or {}
3146
3155
  lag_days = int(args.max_lag_days if args.max_lag_days is not None
3147
3156
  else cfg.get("max_lag_days", 30))
3148
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3157
+ repo_path = _scope_path(ledger, args.repo)
3149
3158
 
3150
3159
  # The spec surface is fixed by ADR-005: the repo SPEC.md plus every ADR.
3151
3160
  spec_files = []
@@ -3756,7 +3765,7 @@ def cmd_roundtrip(args):
3756
3765
  (ADR-011). Advisory end to end: exit 0 always, a report, never a gate."""
3757
3766
  ledger = _load(args.ledger)
3758
3767
  _repo_node(ledger, args.repo)
3759
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3768
+ repo_path = _scope_path(ledger, args.repo)
3760
3769
  st = _curation_state(repo_path)
3761
3770
  if st is None:
3762
3771
  print("ROUNDTRIP %s: no %s/ directory -- feature unused, nothing to trace."
@@ -3790,6 +3799,12 @@ def cmd_roundtrip(args):
3790
3799
  out = {"repo": args.repo, "promoted": len(promoted), "covered": len(covered),
3791
3800
  "missing": missing, "advisory": True,
3792
3801
  "coverage_pct": round(100.0 * len(covered) / len(promoted), 1) if promoted else None}
3802
+ # Latest-state record so the mirador/status can surface it without anyone re-running the
3803
+ # command (the spec_drift pattern). A report that evaporates on exit is invisible to
3804
+ # every read surface -- which defeats the point of an advisory (found by auditing which
3805
+ # features actually REACH the user). Advisory data: no step counter, no gate record.
3806
+ ledger["roundtrip"] = dict(out, at=_now())
3807
+ _save(args.ledger, ledger)
3793
3808
  if args.json:
3794
3809
  print(json.dumps(out, indent=2, ensure_ascii=False))
3795
3810
  else:
@@ -3812,7 +3827,7 @@ def cmd_curation_check(args):
3812
3827
  candidate judged, or the feature unused."""
3813
3828
  ledger = _load(args.ledger)
3814
3829
  _repo_node(ledger, args.repo)
3815
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3830
+ repo_path = _scope_path(ledger, args.repo)
3816
3831
  st = _curation_state(repo_path)
3817
3832
  if st is None:
3818
3833
  if args.json:
@@ -4896,6 +4911,8 @@ def cmd_dashboard(args):
4896
4911
  if ledger.get(CLEAN_ROOM_KEY):
4897
4912
  out["clean_room"] = {r: [e for e in ledger[CLEAN_ROOM_KEY] if e.get("repo") == r][-1]
4898
4913
  for r in {e.get("repo") for e in ledger[CLEAN_ROOM_KEY]}}
4914
+ if ledger.get("roundtrip"):
4915
+ out["roundtrip"] = ledger["roundtrip"]
4899
4916
  if getattr(args, "json", False):
4900
4917
  print(json.dumps(out, indent=2, ensure_ascii=False))
4901
4918
  return
@@ -665,8 +665,8 @@ function renderModes(){
665
665
  ledger fact visible. BOTH keys are conditional in dashboard --json, so a project
666
666
  that never used either sees no card at all (absent block = identical view). */
667
667
  const card=$("#card-modes");if(!card)return;
668
- const fp=DATA.fast_path,sd=DATA.spec_drift;
669
- if(!fp&&!sd){card.hidden=true;return;}
668
+ const fp=DATA.fast_path,sd=DATA.spec_drift,cr=DATA.clean_room;
669
+ if(!fp&&!sd&&!cr){card.hidden=true;return;}
670
670
  const host=$("#modes");host.replaceChildren();
671
671
  const parts=[];
672
672
  if(fp){
@@ -677,6 +677,16 @@ function renderModes(){
677
677
  node("span","chip "+(cls[e.verdict]||"st-todo"),String(e.verdict||"?"))));});
678
678
  parts.push(Object.keys(fp).length+" repo"+(Object.keys(fp).length>1?"s":""));
679
679
  }
680
+ if(cr){
681
+ const cls={GREEN:"st-done",RED:"st-block"};
682
+ Object.keys(cr).forEach(r=>{const e=cr[r]||{};
683
+ const sha=(e.ref||"").slice(0,8)||"?";
684
+ const wall=(typeof e.wall_ms==="number")?(" · "+(e.wall_ms/1000).toFixed(1)+"s"):"";
685
+ append(host,append(node("div","spec"),node("span","id","clean-room"),
686
+ node("span","t",r+" @ "+sha+wall),
687
+ node("span","chip "+(cls[e.status]||"st-prog"),String(e.status||"?"))));});
688
+ parts.push("clean-room ×"+Object.keys(cr).length);
689
+ }
680
690
  if(sd){
681
691
  const cls={SPEC_STALE:"st-block",CLEAN:"st-done",UNMAPPED:"st-todo",UNTRACKED:"st-todo"};
682
692
  const res=sd.results||[];
@@ -100,6 +100,15 @@ nothing and gates nothing.
100
100
  `spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
101
101
  stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
102
102
 
103
+ **Clean-room (ADR-008):** if the ledger carries `clean_room` runs, add ONE line with the
104
+ latest run per repo: `clean-room: GREEN @ <sha8> (12.3s)` / `RED @ <sha8>`. When the gate is
105
+ declared (`mode: "final"`) and the latest run does not match the current HEAD, append
106
+ `(stale for gate)`. Absent key → no line.
107
+
108
+ **Roundtrip (ADR-009 slice 2):** if the ledger carries a `roundtrip` run, add ONE line:
109
+ `roundtrip: N/M promoted traceable by uscha-spec id (advisory)`. Absent key → no line —
110
+ silence is honest when the loop was never measured.
111
+
103
112
  ## Degradation (honest, specific)
104
113
 
105
114
  - `measured` missing entirely → print: *"No measurement recorded yet — the trail
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.65.0",
2
+ "version": "1.67.0",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,