@andresmassello/uscha 1.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/bin/README.md +6 -0
  4. package/bin/uscha.js +28 -0
  5. package/package.json +38 -0
  6. package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +161 -0
  7. package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +124 -0
  8. package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +490 -0
  9. package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +5602 -0
  10. package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +161 -0
  11. package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +145 -0
  12. package/uscha-kit/.claude/skills/uscha-mirador/mirador-render.py +112 -0
  13. package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.ps1 +22 -0
  14. package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.sh +26 -0
  15. package/uscha-kit/.claude/skills/uscha-mirador/mirador.template.html +586 -0
  16. package/uscha-kit/.claude/skills/uscha-mirador/telemetry-extract.py +130 -0
  17. package/uscha-kit/.claude/skills/uscha-reverse-discovery/SKILL.md +116 -0
  18. package/uscha-kit/.claude/skills/uscha-rubric/SKILL.md +79 -0
  19. package/uscha-kit/.claude/skills/uscha-sysdoc/SKILL.md +88 -0
  20. package/uscha-kit/.claude-plugin/plugin.json +24 -0
  21. package/uscha-kit/.codex-plugin/plugin.json +37 -0
  22. package/uscha-kit/CHANGELOG-1.10.0.md +84 -0
  23. package/uscha-kit/CHANGELOG-1.11.0.md +67 -0
  24. package/uscha-kit/CHANGELOG-1.12.0.md +46 -0
  25. package/uscha-kit/CHANGELOG-1.13.0.md +33 -0
  26. package/uscha-kit/CHANGELOG-1.14.0.md +42 -0
  27. package/uscha-kit/CHANGELOG-1.15.0.md +58 -0
  28. package/uscha-kit/CHANGELOG-1.16.0.md +55 -0
  29. package/uscha-kit/CHANGELOG-1.17.0.md +44 -0
  30. package/uscha-kit/CHANGELOG-1.18.0.md +42 -0
  31. package/uscha-kit/CHANGELOG-1.19.0.md +41 -0
  32. package/uscha-kit/CHANGELOG-1.2.2.md +16 -0
  33. package/uscha-kit/CHANGELOG-1.2.3.md +20 -0
  34. package/uscha-kit/CHANGELOG-1.2.4.md +10 -0
  35. package/uscha-kit/CHANGELOG-1.2.5.md +23 -0
  36. package/uscha-kit/CHANGELOG-1.2.6.md +11 -0
  37. package/uscha-kit/CHANGELOG-1.2.7.md +15 -0
  38. package/uscha-kit/CHANGELOG-1.2.8.md +24 -0
  39. package/uscha-kit/CHANGELOG-1.2.9.md +4 -0
  40. package/uscha-kit/CHANGELOG-1.20.0.md +29 -0
  41. package/uscha-kit/CHANGELOG-1.21.0.md +33 -0
  42. package/uscha-kit/CHANGELOG-1.22.0.md +60 -0
  43. package/uscha-kit/CHANGELOG-1.23.0.md +75 -0
  44. package/uscha-kit/CHANGELOG-1.24.0.md +50 -0
  45. package/uscha-kit/CHANGELOG-1.25.0.md +55 -0
  46. package/uscha-kit/CHANGELOG-1.26.0.md +70 -0
  47. package/uscha-kit/CHANGELOG-1.27.0.md +45 -0
  48. package/uscha-kit/CHANGELOG-1.28.0.md +35 -0
  49. package/uscha-kit/CHANGELOG-1.29.0.md +20 -0
  50. package/uscha-kit/CHANGELOG-1.3.0.md +74 -0
  51. package/uscha-kit/CHANGELOG-1.30.0.md +46 -0
  52. package/uscha-kit/CHANGELOG-1.31.0.md +59 -0
  53. package/uscha-kit/CHANGELOG-1.32.0.md +50 -0
  54. package/uscha-kit/CHANGELOG-1.33.0.md +46 -0
  55. package/uscha-kit/CHANGELOG-1.34.0.md +55 -0
  56. package/uscha-kit/CHANGELOG-1.35.0.md +30 -0
  57. package/uscha-kit/CHANGELOG-1.36.0.md +33 -0
  58. package/uscha-kit/CHANGELOG-1.37.0.md +41 -0
  59. package/uscha-kit/CHANGELOG-1.38.0.md +11 -0
  60. package/uscha-kit/CHANGELOG-1.39.0.md +14 -0
  61. package/uscha-kit/CHANGELOG-1.4.0.md +68 -0
  62. package/uscha-kit/CHANGELOG-1.40.0.md +16 -0
  63. package/uscha-kit/CHANGELOG-1.40.1.md +11 -0
  64. package/uscha-kit/CHANGELOG-1.5.0.md +64 -0
  65. package/uscha-kit/CHANGELOG-1.6.0.md +57 -0
  66. package/uscha-kit/CHANGELOG-1.7.0.md +74 -0
  67. package/uscha-kit/CHANGELOG-1.8.0.md +46 -0
  68. package/uscha-kit/CHANGELOG-1.9.0.md +112 -0
  69. package/uscha-kit/LICENSE +21 -0
  70. package/uscha-kit/README.md +497 -0
  71. package/uscha-kit/VERSION +1 -0
  72. package/uscha-kit/WORKBENCH.md +178 -0
  73. package/uscha-kit/hooks/block-approved-writes.ps1 +46 -0
  74. package/uscha-kit/hooks/hooks.json +15 -0
  75. package/uscha-kit/install-uscha.py +344 -0
  76. package/uscha-kit/skills/uscha-adr-refine/SKILL.md +161 -0
  77. package/uscha-kit/skills/uscha-characterize/SKILL.md +124 -0
  78. package/uscha-kit/skills/uscha-devloop/SKILL.md +490 -0
  79. package/uscha-kit/skills/uscha-devloop/qa_ledger.py +5602 -0
  80. package/uscha-kit/skills/uscha-discovery/SKILL.md +161 -0
  81. package/uscha-kit/skills/uscha-mirador/SKILL.md +145 -0
  82. package/uscha-kit/skills/uscha-mirador/mirador-render.py +112 -0
  83. package/uscha-kit/skills/uscha-mirador/mirador-watch.ps1 +22 -0
  84. package/uscha-kit/skills/uscha-mirador/mirador-watch.sh +26 -0
  85. package/uscha-kit/skills/uscha-mirador/mirador.template.html +586 -0
  86. package/uscha-kit/skills/uscha-mirador/telemetry-extract.py +130 -0
  87. package/uscha-kit/skills/uscha-reverse-discovery/SKILL.md +116 -0
  88. package/uscha-kit/skills/uscha-rubric/SKILL.md +79 -0
  89. package/uscha-kit/skills/uscha-sysdoc/SKILL.md +88 -0
  90. package/uscha-kit/templates/.gitattributes +6 -0
  91. package/uscha-kit/templates/CLAUDE.md +56 -0
  92. package/uscha-kit/templates/CONSTITUTION.md +149 -0
  93. package/uscha-kit/templates/RUBRIC.md +38 -0
  94. package/uscha-kit/templates/docs/adr/README.md +19 -0
  95. package/uscha-kit/templates/rubric-grader-prompt.md +63 -0
  96. package/uscha-kit/tests/smoke-engine.sh +1739 -0
  97. package/uscha-kit/uscha.config.json +181 -0
  98. package/uscha-kit/workbench-doctor.sh +45 -0
@@ -0,0 +1,178 @@
1
+ # Workbench — armado, verificación y actualización
2
+
3
+ La capa **genérica** que hace correr la metodología en cualquier proyecto: Claude Code +
4
+ Python + git/gh + los skills del kit. Lo **específico de cada stack** (JDK/Maven, MSSQL,
5
+ los linters del static gate, drivers) es el *adapter* del proyecto y vive en el
6
+ `CLAUDE.md`/`AGENTS.md` de cada repo — **no entra acá**.
7
+
8
+ > Fuente de los datos de instalación de Claude Code: docs oficiales
9
+ > (https://docs.claude.com/en/docs/claude-code/overview). Verificá con `claude doctor`.
10
+
11
+ ---
12
+
13
+ ## 1. Componentes del workbench
14
+
15
+ | Componente | Para qué | Mínimo |
16
+ |---|---|---|
17
+ | **Claude Code** | el agente / orquestador | cuenta Pro, Max, Team, Enterprise o Console |
18
+ | **Python 3.8+** | corre `qa_ledger.py` (stdlib pura, sin dependencias) | `python3` en PATH |
19
+ | **git** | versionado | 2.x, con `user.name`/`user.email` |
20
+ | **gh** (GitHub CLI) | crear repo / abrir PR | opcional pero recomendado |
21
+ | **skills del kit** | `discovery`, `adr-refine`, `dev-loop`, `sys-doc` | copiados a `~/.claude/skills/` |
22
+ | **skills de QA** | `code-review`, `judgment-day`, `improve` | tus skills globales (el dev-loop los **orquesta**, no los trae) |
23
+
24
+ ---
25
+
26
+ ## 2. Instalar Claude Code
27
+
28
+ **Native installer (recomendado — no requiere Node, se auto-actualiza):**
29
+
30
+ ```bash
31
+ # macOS / Linux / WSL
32
+ curl -fsSL https://claude.ai/install.sh | bash
33
+
34
+ # Windows PowerShell
35
+ irm https://claude.ai/install.ps1 | iex
36
+
37
+ # Windows CMD
38
+ curl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd
39
+
40
+ # Homebrew (macOS / Linux)
41
+ brew install --cask claude-code
42
+ ```
43
+
44
+ **npm (alternativa — requiere Node.js 18+; útil para fijar versión en CI):**
45
+
46
+ ```bash
47
+ npm install -g @anthropic-ai/claude-code
48
+ # NO usar sudo. Si hay EACCES: nvm, o npm config set prefix '~/.npm-global'
49
+ ```
50
+
51
+ **Login:** ejecutá `claude` y seguí el OAuth del navegador (cuenta con suscripción).
52
+ Para headless/servidor: `export ANTHROPIC_API_KEY=...`.
53
+
54
+ **Windows:** WSL2 es el camino recomendado (instalás y corrés `claude` *dentro* de WSL).
55
+ Alternativa: Git for Windows (provee Git Bash para la tool Bash; la tool PowerShell se
56
+ habilita con `CLAUDE_CODE_USE_POWERSHELL_TOOL=1`).
57
+
58
+ **Verificar:** `claude --version` y `claude doctor`.
59
+
60
+ ---
61
+
62
+ ## 3. Python (para el ledger)
63
+
64
+ `qa_ledger.py` usa **solo la librería estándar**, Python 3.8+. No hay `pip` ni venv.
65
+
66
+ - **Linux/macOS:** suele venir `python3`. Verificá `python3 --version`; si falta, instalá
67
+ con el gestor del SO (`apt`, `brew`, etc.).
68
+ - **Windows nativo:** no existe `python3` por defecto. Dos opciones:
69
+ 1. **WSL2** (recomendado): adentro `python3` existe.
70
+ 2. **Python de python.org** + shim. En PowerShell:
71
+ ```powershell
72
+ '@py -3 %*' | Out-File -Encoding ascii "$env:USERPROFILE\.local\bin\python3.cmd"
73
+ ```
74
+ y agregá esa carpeta al `PATH`.
75
+
76
+ ---
77
+
78
+ ## 4. git + gh
79
+
80
+ ```bash
81
+ git config --global user.name "Tu Nombre"
82
+ git config --global user.email "tu@mail.com"
83
+ gh auth login # habilita gh repo create / gh pr
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 5. Instalar los skills del kit
89
+
90
+ **Global (todos los repos):**
91
+
92
+ ```bash
93
+ mkdir -p ~/.claude/skills
94
+ cp -r uscha-kit/.claude/skills/* ~/.claude/skills/
95
+ # Windows: %USERPROFILE%\.claude\skills\
96
+ ```
97
+
98
+ **Per-repo (solo este repo):**
99
+
100
+ ```bash
101
+ cp -r uscha-kit/.claude/skills <repo>/.claude/
102
+ cp uscha-kit/uscha.config.json <repo>/ # config en la raíz del repo
103
+ ```
104
+
105
+ **Verificar:** abrí `claude` y mirá `/help` — deberían aparecer `/discovery`,
106
+ `/adr-refine`, `/dev-loop`, `/sys-doc`. O `ls ~/.claude/skills`.
107
+
108
+ ---
109
+
110
+ ## 6. Los skills de QA externos (dependencia)
111
+
112
+ El `dev-loop` **orquesta** `code-review` / `judgment-day` / `improve` — **no los empaqueta**.
113
+ Tienen que estar en `~/.claude/skills/` (tus skills globales). Si usás otros nombres,
114
+ editá `qa_tools_order` en `uscha.config.json`. Si no los tenés, el loop no encuentra
115
+ las tools de juicio (igual corre el static gate determinístico).
116
+
117
+ ---
118
+
119
+ ## 7. Permisos (settings.local.json)
120
+
121
+ Para que el loop no frene pidiendo permiso en cada comando, configurá
122
+ `<repo>/.claude/settings.local.json` con lo que aceptes ejecutar, por ejemplo:
123
+
124
+ ```json
125
+ { "permissions": { "allow": [ "Bash(python3:*)", "Bash(git:*)", "Bash(gh:*)" ] } }
126
+ ```
127
+
128
+ Granular o con wildcards, según tu confianza en el repo.
129
+
130
+ ---
131
+
132
+ ## 8. Qué NO entra acá (adapter por proyecto)
133
+
134
+ JDK/Maven, MSSQL y drivers, los linters del static gate (Checkstyle/PMD/SpotBugs/
135
+ FindSecBugs), Node del app, etc. Eso es el *adapter* del stack y se documenta en el
136
+ `CLAUDE.md`/`AGENTS.md` de cada repo. El workbench es lo que **no** cambia entre proyectos.
137
+
138
+ ---
139
+
140
+ ## 9. Cómo sé qué tengo (doctor)
141
+
142
+ ```bash
143
+ bash uscha-kit/workbench-doctor.sh
144
+ ```
145
+
146
+ Reporta: versión del kit, Claude Code, Python, git, gh, Node (si aplica), y qué skills
147
+ están instalados en `~/.claude/skills/`. Complementá con la salud nativa:
148
+
149
+ ```bash
150
+ claude --version # versión instalada
151
+ claude doctor # diagnóstico de instalación/config
152
+ claude whoami # cuenta autenticada
153
+ ```
154
+
155
+ ---
156
+
157
+ ## 10. Cómo actualizo el workbench
158
+
159
+ - **Claude Code:** el native installer se auto-actualiza en background. Forzar ya:
160
+ `claude update`. Homebrew/WinGet/Linux pkg: actualización manual. npm:
161
+ `npm update -g @anthropic-ai/claude-code` (o `@latest`). Controlar el auto-update:
162
+ `DISABLE_AUTOUPDATER` / `DISABLE_UPDATES` en el `env` de `settings.json`.
163
+ - **Skills del kit:** re-copiá la versión nueva a `~/.claude/skills/` (o `git pull` si lo
164
+ tenés en repo). Mirá `VERSION` para saber qué versión del kit corrés.
165
+ - **Python / git / gh:** gestor del SO o nvm.
166
+ - **Después de cualquier update:** corré `workbench-doctor.sh` otra vez.
167
+
168
+ ---
169
+
170
+ ## Checklist mínimo
171
+
172
+ - [ ] `claude --version` OK y `claude doctor` sin errores
173
+ - [ ] `python3 --version` ≥ 3.8
174
+ - [ ] git configurado (`user.name` / `user.email`)
175
+ - [ ] `gh auth login` hecho (si vas a abrir PRs)
176
+ - [ ] `/discovery` `/adr-refine` `/dev-loop` `/sys-doc` aparecen en `/help`
177
+ - [ ] `code-review` / `judgment-day` / `improve` presentes (o `qa_tools_order` ajustado)
178
+ - [ ] `uscha.config.json` en la raíz del repo
@@ -0,0 +1,46 @@
1
+ # block-approved-writes.ps1 — PreToolUse hook (INV-GOLDEN-01).
2
+ # Forbids the agent from writing/renaming any *.approved.* golden. The golden is field
3
+ # truth, approved by a HUMAN — never authored by the agent. On the first failing diff the
4
+ # agent will try to "fix" the test by editing the golden; this makes that impossible.
5
+ #
6
+ # Wire it in settings.json:
7
+ # "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [
8
+ # { "type": "command",
9
+ # "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"<path>/hooks/block-approved-writes.ps1\"" }
10
+ # ] } ] }
11
+ # Exit 2 = block the tool call; the message on stderr is fed back to the agent.
12
+
13
+ $ErrorActionPreference = 'SilentlyContinue'
14
+ $raw = [Console]::In.ReadToEnd()
15
+ if (-not $raw) { exit 0 }
16
+ try { $data = $raw | ConvertFrom-Json } catch { exit 0 }
17
+
18
+ $tool = $data.tool_name
19
+ $block = $false
20
+ $target = ''
21
+
22
+ if ($tool -in @('Write', 'Edit', 'NotebookEdit', 'MultiEdit')) {
23
+ $target = [string]$data.tool_input.file_path
24
+ if ($target -match '\.approved(\.|$)') { $block = $true }
25
+ }
26
+ elseif ($tool -eq 'Bash') {
27
+ $cmd = [string]$data.tool_input.command
28
+ # Block ANY Bash command that references a .approved path. The old keyword
29
+ # co-occurrence check (redirect/mv/cp/...) was bypassable via indirection —
30
+ # python -c "open(p,'w')", a variable-built path, dd, etc. The agent has no
31
+ # legitimate Bash reason to touch an .approved (reading goes through the Read
32
+ # tool), so the coarse rule is safer than the clever one.
33
+ if ($cmd -match '\.approved') {
34
+ $block = $true
35
+ $target = 'Bash command referencing a .approved path'
36
+ }
37
+ }
38
+
39
+ if ($block) {
40
+ [Console]::Error.WriteLine(
41
+ "BLOCKED by INV-GOLDEN-01: the agent may not write or rename an .approved golden " +
42
+ "($target). The .approved is field truth - a HUMAN approves it. Emit a .received " +
43
+ "instead and stop for human approval.")
44
+ exit 2
45
+ }
46
+ exit 0
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${CLAUDE_PLUGIN_ROOT}/hooks/block-approved-writes.ps1\""
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env python3
2
+ """Universal Uscha machine installer.
3
+
4
+ One small public interface, two adapters inside:
5
+ - Codex: personal local plugin at ~/plugins/uscha + ~/.agents/plugins/marketplace.json
6
+ - Claude: global skills/hook at ~/.claude/skills and ~/.claude/hooks
7
+
8
+ Stdlib only. Safe to test with --home and --dry-run.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ KIT_ROOT = Path(__file__).resolve().parent
22
+ PLUGIN_NAME = "uscha"
23
+ SKILLS = [
24
+ "uscha-discovery",
25
+ "uscha-adr-refine",
26
+ "uscha-reverse-discovery",
27
+ "uscha-characterize",
28
+ "uscha-devloop",
29
+ "uscha-sysdoc",
30
+ "uscha-rubric",
31
+ "uscha-mirador",
32
+ ]
33
+ TARGETS = ("codex", "claude")
34
+
35
+
36
+ def source_version() -> str:
37
+ raw = (KIT_ROOT / "VERSION").read_text(encoding="utf-8").strip()
38
+ return raw.split()[-1]
39
+
40
+
41
+ def now_iso() -> str:
42
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
43
+
44
+
45
+ def home_path(args) -> Path:
46
+ return Path(args.home).expanduser().resolve() if args.home else Path.home().resolve()
47
+
48
+
49
+ def selected_targets(value: str) -> list[str]:
50
+ return list(TARGETS) if value == "both" else [value]
51
+
52
+
53
+ class Plan:
54
+ def __init__(self, dry_run: bool):
55
+ self.dry_run = dry_run
56
+ self.operations: list[dict] = []
57
+
58
+ def add(self, action: str, path: Path, source: Path | None = None, note: str | None = None):
59
+ row = {"action": action, "path": str(path)}
60
+ if source is not None:
61
+ row["source"] = str(source)
62
+ if note:
63
+ row["note"] = note
64
+ self.operations.append(row)
65
+
66
+ def ensure_dir(self, path: Path):
67
+ self.add("mkdir", path)
68
+ if not self.dry_run:
69
+ path.mkdir(parents=True, exist_ok=True)
70
+
71
+ def write_json(self, path: Path, data: dict):
72
+ self.add("write-json", path)
73
+ if not self.dry_run:
74
+ path.parent.mkdir(parents=True, exist_ok=True)
75
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
76
+
77
+ def copy_file(self, src: Path, dst: Path):
78
+ self.add("copy-file", dst, src)
79
+ if not self.dry_run:
80
+ dst.parent.mkdir(parents=True, exist_ok=True)
81
+ shutil.copy2(src, dst)
82
+
83
+ def copy_dir(self, src: Path, dst: Path, mode: str):
84
+ self.add("copy-dir" if mode == "copy" else "link-dir", dst, src)
85
+ if self.dry_run:
86
+ return
87
+ if dst.exists() or dst.is_symlink():
88
+ if dst.is_symlink() or dst.is_file():
89
+ dst.unlink()
90
+ else:
91
+ shutil.rmtree(dst)
92
+ dst.parent.mkdir(parents=True, exist_ok=True)
93
+ if mode == "copy":
94
+ shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo"))
95
+ else:
96
+ link_dir(src, dst)
97
+
98
+
99
+ def link_dir(src: Path, dst: Path):
100
+ """Create a directory link. On Windows prefer junctions for non-admin installs."""
101
+ if os.name == "nt":
102
+ cmd = ["cmd", "/c", "mklink", "/J", str(dst), str(src)]
103
+ res = subprocess.run(cmd, text=True, capture_output=True)
104
+ if res.returncode != 0:
105
+ raise SystemExit("[install-uscha] cannot create junction %s -> %s: %s" %
106
+ (dst, src, (res.stderr or res.stdout).strip()))
107
+ else:
108
+ os.symlink(src, dst, target_is_directory=True)
109
+
110
+
111
+ def plugin_manifest() -> dict:
112
+ return {
113
+ "name": PLUGIN_NAME,
114
+ "version": source_version(),
115
+ "description": "Uscha spec-driven development methodology for coding agents.",
116
+ "author": {"name": "Andres Massello", "url": "https://github.com/andresmassello"},
117
+ "homepage": "https://github.com/andresmassello/uscha",
118
+ "repository": "https://github.com/andresmassello/uscha",
119
+ "license": "MIT",
120
+ "keywords": ["spec-driven", "qa", "gates", "golden-testing", "readiness"],
121
+ "skills": "./skills/",
122
+ "interface": {
123
+ "displayName": "Uscha",
124
+ "shortDescription": "Spec-driven development with fact gates and readiness.",
125
+ "longDescription": "Uscha installs discovery, ADR, characterization, devloop, rubric, sysdoc and Mirador skills plus the qa_ledger.py evidence engine.",
126
+ "developerName": "Andres Massello",
127
+ "category": "Productivity",
128
+ "capabilities": ["Write", "Interactive"],
129
+ "defaultPrompt": [
130
+ "Run Uscha discovery for this feature.",
131
+ "Use Uscha devloop to verify this change.",
132
+ "Show the Uscha readiness for this repo."
133
+ ],
134
+ "brandColor": "#7C3AED"
135
+ }
136
+ }
137
+
138
+
139
+ def marker(target: str, install_root: Path, mode: str) -> dict:
140
+ return {
141
+ "name": PLUGIN_NAME,
142
+ "target": target,
143
+ "version": source_version(),
144
+ "mode": mode,
145
+ "installed_at": now_iso(),
146
+ "source": str(KIT_ROOT),
147
+ "install_root": str(install_root),
148
+ }
149
+
150
+
151
+ def install_codex(plan: Plan, home: Path, mode: str):
152
+ plugin_root = home / "plugins" / PLUGIN_NAME
153
+ plan.ensure_dir(plugin_root / ".codex-plugin")
154
+ plan.write_json(plugin_root / ".codex-plugin" / "plugin.json", plugin_manifest())
155
+ skills_dst = plugin_root / "skills"
156
+ plan.ensure_dir(skills_dst)
157
+ for skill in SKILLS:
158
+ plan.copy_dir(KIT_ROOT / ".claude" / "skills" / skill, skills_dst / skill, mode)
159
+ plan.copy_file(KIT_ROOT / "VERSION", plugin_root / "VERSION")
160
+ plan.copy_file(KIT_ROOT / "uscha.config.json", plugin_root / "uscha.config.json")
161
+ plan.write_json(plugin_root / "uscha-install.json", marker("codex", plugin_root, mode))
162
+ write_marketplace(plan, home, plugin_root)
163
+ return plugin_root
164
+
165
+
166
+ def write_marketplace(plan: Plan, home: Path, plugin_root: Path):
167
+ market = home / ".agents" / "plugins" / "marketplace.json"
168
+ data = {"name": "personal", "interface": {"displayName": "Personal"}, "plugins": []}
169
+ if market.exists() and not plan.dry_run:
170
+ try:
171
+ data = json.loads(market.read_text(encoding="utf-8"))
172
+ except json.JSONDecodeError:
173
+ raise SystemExit("[install-uscha] marketplace.json is invalid: %s" % market)
174
+ plugins = [p for p in data.get("plugins", []) if p.get("name") != PLUGIN_NAME]
175
+ plugins.append({
176
+ "name": PLUGIN_NAME,
177
+ "source": {"source": "local", "path": "./plugins/uscha"},
178
+ "policy": {"installation": "AVAILABLE", "authentication": "ON_INSTALL"},
179
+ "category": "Productivity",
180
+ })
181
+ data["plugins"] = plugins
182
+ data.setdefault("name", "personal")
183
+ data.setdefault("interface", {"displayName": "Personal"})
184
+ plan.write_json(market, data)
185
+
186
+
187
+ def install_claude(plan: Plan, home: Path, mode: str):
188
+ skills_root = home / ".claude" / "skills"
189
+ plan.ensure_dir(skills_root)
190
+ for skill in SKILLS:
191
+ plan.copy_dir(KIT_ROOT / ".claude" / "skills" / skill, skills_root / skill, mode)
192
+ hooks_root = home / ".claude" / "hooks"
193
+ plan.ensure_dir(hooks_root)
194
+ plan.copy_file(KIT_ROOT / "hooks" / "block-approved-writes.ps1",
195
+ hooks_root / "block-approved-writes.ps1")
196
+ plan.write_json(home / ".claude" / "uscha-install.json", marker("claude", home / ".claude", mode))
197
+ return home / ".claude"
198
+
199
+
200
+ def target_status(home: Path, target: str) -> dict:
201
+ if target == "codex":
202
+ root = home / "plugins" / PLUGIN_NAME
203
+ manifest = root / ".codex-plugin" / "plugin.json"
204
+ engine = root / "skills" / "uscha-devloop" / "qa_ledger.py"
205
+ market = home / ".agents" / "plugins" / "marketplace.json"
206
+ marker_path = root / "uscha-install.json"
207
+ else:
208
+ root = home / ".claude"
209
+ manifest = root / "uscha-install.json"
210
+ engine = root / "skills" / "uscha-devloop" / "qa_ledger.py"
211
+ market = root / "hooks" / "block-approved-writes.ps1"
212
+ marker_path = root / "uscha-install.json"
213
+ installed = engine.exists() and manifest.exists()
214
+ installed_version = None
215
+ if marker_path.exists():
216
+ try:
217
+ installed_version = json.loads(marker_path.read_text(encoding="utf-8")).get("version")
218
+ except json.JSONDecodeError:
219
+ installed_version = None
220
+ return {
221
+ "installed": installed,
222
+ "install_root": str(root),
223
+ "engine": str(engine),
224
+ "marketplace_or_hook": str(market),
225
+ "installed_version": installed_version,
226
+ "source_version": source_version(),
227
+ "version_match": installed_version == source_version(),
228
+ }
229
+
230
+
231
+ def cmd_version(args):
232
+ out = {"name": PLUGIN_NAME, "source_version": source_version(), "targets": list(TARGETS)}
233
+ emit(out, args.json)
234
+
235
+
236
+ def cmd_install(args):
237
+ home = home_path(args)
238
+ plan = Plan(args.dry_run)
239
+ installed = {}
240
+ for target in selected_targets(args.target):
241
+ if target == "codex":
242
+ installed[target] = str(install_codex(plan, home, args.mode))
243
+ else:
244
+ installed[target] = str(install_claude(plan, home, args.mode))
245
+ out = {"status": "planned" if args.dry_run else "installed", "dry_run": args.dry_run,
246
+ "source_version": source_version(), "home": str(home), "installed": installed,
247
+ "operations": plan.operations,
248
+ "next": next_steps(args.target)}
249
+ emit(out, args.json)
250
+
251
+
252
+ def cmd_doctor(args):
253
+ home = home_path(args)
254
+ targets = {t: target_status(home, t) for t in selected_targets(args.target)}
255
+ ok = all(v["installed"] and v["version_match"] for v in targets.values())
256
+ out = {"ok": ok, "source_version": source_version(), "home": str(home),
257
+ "python": sys.version.split()[0], "targets": targets}
258
+ emit(out, args.json)
259
+ if not ok and not args.json:
260
+ sys.exit(1)
261
+
262
+
263
+ def cmd_init(args):
264
+ repo = Path(args.repo).expanduser().resolve()
265
+ plan = Plan(args.dry_run)
266
+ plan.copy_file(KIT_ROOT / "uscha.config.json", repo / "uscha.config.json")
267
+ for name in ("CLAUDE.md", "CONSTITUTION.md", ".gitattributes"):
268
+ src = KIT_ROOT / "templates" / name
269
+ if src.exists():
270
+ plan.copy_file(src, repo / name)
271
+ out = {"status": "planned" if args.dry_run else "initialized", "dry_run": args.dry_run,
272
+ "repo": str(repo), "operations": plan.operations}
273
+ emit(out, args.json)
274
+
275
+
276
+ def next_steps(target: str) -> list[str]:
277
+ steps = []
278
+ if target in ("codex", "both"):
279
+ steps.append("Codex: restart or open a new thread, then install/use uscha from the Personal marketplace if needed.")
280
+ if target in ("claude", "both"):
281
+ steps.append("Claude: restart Claude Code so global skills/hooks are reloaded.")
282
+ steps.append("Run: python install-uscha.py doctor --target %s" % target)
283
+ return steps
284
+
285
+
286
+ def emit(data: dict, as_json: bool):
287
+ if as_json:
288
+ print(json.dumps(data, indent=2, ensure_ascii=False))
289
+ return
290
+ if "targets" in data and isinstance(data["targets"], dict):
291
+ print("Uscha %s" % data.get("source_version"))
292
+ for name, st in data["targets"].items():
293
+ mark = "OK" if st["installed"] and st["version_match"] else "WARN"
294
+ print(" %s: %s installed=%s version=%s" %
295
+ (mark, name, st["installed"], st.get("installed_version")))
296
+ elif "operations" in data:
297
+ print("Uscha %s: %s (%s operations)" %
298
+ (data.get("source_version", source_version()), data["status"], len(data["operations"])))
299
+ for op in data["operations"][:20]:
300
+ print(" - {action}: {path}".format(**op))
301
+ if len(data["operations"]) > 20:
302
+ print(" ... %d more" % (len(data["operations"]) - 20))
303
+ else:
304
+ print("Uscha %s" % data.get("source_version", source_version()))
305
+
306
+
307
+ def build_parser():
308
+ p = argparse.ArgumentParser(description="Install/update Uscha for Codex and Claude machines")
309
+ sub = p.add_subparsers(dest="cmd", required=True)
310
+
311
+ pv = sub.add_parser("version", help="show source version and supported targets")
312
+ pv.add_argument("--json", action="store_true")
313
+ pv.set_defaults(func=cmd_version)
314
+
315
+ pi = sub.add_parser("install", help="install Uscha globally for a machine")
316
+ pi.add_argument("--target", choices=["codex", "claude", "both"], default="both")
317
+ pi.add_argument("--mode", choices=["copy", "link"], default="copy")
318
+ pi.add_argument("--home", default=None, help="override home dir; useful for tests")
319
+ pi.add_argument("--dry-run", action="store_true")
320
+ pi.add_argument("--json", action="store_true")
321
+ pi.set_defaults(func=cmd_install)
322
+
323
+ pd = sub.add_parser("doctor", help="check installed Uscha and version drift")
324
+ pd.add_argument("--target", choices=["codex", "claude", "both"], default="both")
325
+ pd.add_argument("--home", default=None)
326
+ pd.add_argument("--json", action="store_true")
327
+ pd.set_defaults(func=cmd_doctor)
328
+
329
+ pn = sub.add_parser("init", help="prepare a repo with Uscha config/templates")
330
+ pn.add_argument("--repo", default=".")
331
+ pn.add_argument("--dry-run", action="store_true")
332
+ pn.add_argument("--json", action="store_true")
333
+ pn.set_defaults(func=cmd_init)
334
+ return p
335
+
336
+
337
+ def main(argv=None):
338
+ parser = build_parser()
339
+ args = parser.parse_args(argv)
340
+ args.func(args)
341
+
342
+
343
+ if __name__ == "__main__":
344
+ main()