@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.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/bin/README.md +6 -0
- package/bin/uscha.js +28 -0
- package/package.json +38 -0
- package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +161 -0
- package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +124 -0
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +490 -0
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +5602 -0
- package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +161 -0
- package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +145 -0
- package/uscha-kit/.claude/skills/uscha-mirador/mirador-render.py +112 -0
- package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.ps1 +22 -0
- package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.sh +26 -0
- package/uscha-kit/.claude/skills/uscha-mirador/mirador.template.html +586 -0
- package/uscha-kit/.claude/skills/uscha-mirador/telemetry-extract.py +130 -0
- package/uscha-kit/.claude/skills/uscha-reverse-discovery/SKILL.md +116 -0
- package/uscha-kit/.claude/skills/uscha-rubric/SKILL.md +79 -0
- package/uscha-kit/.claude/skills/uscha-sysdoc/SKILL.md +88 -0
- package/uscha-kit/.claude-plugin/plugin.json +24 -0
- package/uscha-kit/.codex-plugin/plugin.json +37 -0
- package/uscha-kit/CHANGELOG-1.10.0.md +84 -0
- package/uscha-kit/CHANGELOG-1.11.0.md +67 -0
- package/uscha-kit/CHANGELOG-1.12.0.md +46 -0
- package/uscha-kit/CHANGELOG-1.13.0.md +33 -0
- package/uscha-kit/CHANGELOG-1.14.0.md +42 -0
- package/uscha-kit/CHANGELOG-1.15.0.md +58 -0
- package/uscha-kit/CHANGELOG-1.16.0.md +55 -0
- package/uscha-kit/CHANGELOG-1.17.0.md +44 -0
- package/uscha-kit/CHANGELOG-1.18.0.md +42 -0
- package/uscha-kit/CHANGELOG-1.19.0.md +41 -0
- package/uscha-kit/CHANGELOG-1.2.2.md +16 -0
- package/uscha-kit/CHANGELOG-1.2.3.md +20 -0
- package/uscha-kit/CHANGELOG-1.2.4.md +10 -0
- package/uscha-kit/CHANGELOG-1.2.5.md +23 -0
- package/uscha-kit/CHANGELOG-1.2.6.md +11 -0
- package/uscha-kit/CHANGELOG-1.2.7.md +15 -0
- package/uscha-kit/CHANGELOG-1.2.8.md +24 -0
- package/uscha-kit/CHANGELOG-1.2.9.md +4 -0
- package/uscha-kit/CHANGELOG-1.20.0.md +29 -0
- package/uscha-kit/CHANGELOG-1.21.0.md +33 -0
- package/uscha-kit/CHANGELOG-1.22.0.md +60 -0
- package/uscha-kit/CHANGELOG-1.23.0.md +75 -0
- package/uscha-kit/CHANGELOG-1.24.0.md +50 -0
- package/uscha-kit/CHANGELOG-1.25.0.md +55 -0
- package/uscha-kit/CHANGELOG-1.26.0.md +70 -0
- package/uscha-kit/CHANGELOG-1.27.0.md +45 -0
- package/uscha-kit/CHANGELOG-1.28.0.md +35 -0
- package/uscha-kit/CHANGELOG-1.29.0.md +20 -0
- package/uscha-kit/CHANGELOG-1.3.0.md +74 -0
- package/uscha-kit/CHANGELOG-1.30.0.md +46 -0
- package/uscha-kit/CHANGELOG-1.31.0.md +59 -0
- package/uscha-kit/CHANGELOG-1.32.0.md +50 -0
- package/uscha-kit/CHANGELOG-1.33.0.md +46 -0
- package/uscha-kit/CHANGELOG-1.34.0.md +55 -0
- package/uscha-kit/CHANGELOG-1.35.0.md +30 -0
- package/uscha-kit/CHANGELOG-1.36.0.md +33 -0
- package/uscha-kit/CHANGELOG-1.37.0.md +41 -0
- package/uscha-kit/CHANGELOG-1.38.0.md +11 -0
- package/uscha-kit/CHANGELOG-1.39.0.md +14 -0
- package/uscha-kit/CHANGELOG-1.4.0.md +68 -0
- package/uscha-kit/CHANGELOG-1.40.0.md +16 -0
- package/uscha-kit/CHANGELOG-1.40.1.md +11 -0
- package/uscha-kit/CHANGELOG-1.5.0.md +64 -0
- package/uscha-kit/CHANGELOG-1.6.0.md +57 -0
- package/uscha-kit/CHANGELOG-1.7.0.md +74 -0
- package/uscha-kit/CHANGELOG-1.8.0.md +46 -0
- package/uscha-kit/CHANGELOG-1.9.0.md +112 -0
- package/uscha-kit/LICENSE +21 -0
- package/uscha-kit/README.md +497 -0
- package/uscha-kit/VERSION +1 -0
- package/uscha-kit/WORKBENCH.md +178 -0
- package/uscha-kit/hooks/block-approved-writes.ps1 +46 -0
- package/uscha-kit/hooks/hooks.json +15 -0
- package/uscha-kit/install-uscha.py +344 -0
- package/uscha-kit/skills/uscha-adr-refine/SKILL.md +161 -0
- package/uscha-kit/skills/uscha-characterize/SKILL.md +124 -0
- package/uscha-kit/skills/uscha-devloop/SKILL.md +490 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +5602 -0
- package/uscha-kit/skills/uscha-discovery/SKILL.md +161 -0
- package/uscha-kit/skills/uscha-mirador/SKILL.md +145 -0
- package/uscha-kit/skills/uscha-mirador/mirador-render.py +112 -0
- package/uscha-kit/skills/uscha-mirador/mirador-watch.ps1 +22 -0
- package/uscha-kit/skills/uscha-mirador/mirador-watch.sh +26 -0
- package/uscha-kit/skills/uscha-mirador/mirador.template.html +586 -0
- package/uscha-kit/skills/uscha-mirador/telemetry-extract.py +130 -0
- package/uscha-kit/skills/uscha-reverse-discovery/SKILL.md +116 -0
- package/uscha-kit/skills/uscha-rubric/SKILL.md +79 -0
- package/uscha-kit/skills/uscha-sysdoc/SKILL.md +88 -0
- package/uscha-kit/templates/.gitattributes +6 -0
- package/uscha-kit/templates/CLAUDE.md +56 -0
- package/uscha-kit/templates/CONSTITUTION.md +149 -0
- package/uscha-kit/templates/RUBRIC.md +38 -0
- package/uscha-kit/templates/docs/adr/README.md +19 -0
- package/uscha-kit/templates/rubric-grader-prompt.md +63 -0
- package/uscha-kit/tests/smoke-engine.sh +1739 -0
- package/uscha-kit/uscha.config.json +181 -0
- 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,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()
|