@trycore/spec-build-harness 0.1.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/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +28 -0
- package/GOVERNANCE.md +48 -0
- package/INSTALL.md +295 -0
- package/METODOLOGIA.md +360 -0
- package/README.md +130 -0
- package/VERSION +1 -0
- package/agents/build/api-contract-tester.md +32 -0
- package/agents/build/build-orchestrator.md +50 -0
- package/agents/build/change-epic-coherence.md +41 -0
- package/agents/build/coherence-three-way.md +35 -0
- package/agents/build/data-consistency-checker.md +48 -0
- package/agents/build/dor-dod-gatekeeper.md +46 -0
- package/agents/build/security-reviewer.md +46 -0
- package/agents/build/simple-design-reviewer.md +33 -0
- package/agents/build/stack-guardian.md +41 -0
- package/agents/build/ux-krug-reviewer.md +33 -0
- package/commands/build/onboard.md +136 -0
- package/commands/opsx/apply.md +152 -0
- package/commands/opsx/archive.md +157 -0
- package/commands/opsx/bulk-archive.md +242 -0
- package/commands/opsx/continue.md +114 -0
- package/commands/opsx/explore.md +174 -0
- package/commands/opsx/ff.md +94 -0
- package/commands/opsx/new.md +69 -0
- package/commands/opsx/onboard.md +525 -0
- package/commands/opsx/sync.md +134 -0
- package/commands/opsx/verify.md +164 -0
- package/config/stack-allowlist.template.json +12 -0
- package/dist/cli.js +105 -0
- package/dist/commands/doctor.js +77 -0
- package/dist/commands/init.js +129 -0
- package/dist/commands/status.js +59 -0
- package/dist/commands/uninstall.js +52 -0
- package/dist/commands/update.js +11 -0
- package/dist/lib/install-engine.js +99 -0
- package/dist/lib/markers.js +81 -0
- package/dist/lib/paths.js +65 -0
- package/dist/lib/settings-merge.js +125 -0
- package/dist/lib/stack-prompt.js +69 -0
- package/dist/lib/state-seed.js +59 -0
- package/docs/agents.md +133 -0
- package/docs/commands.md +128 -0
- package/docs/customization/mcp-extensions.md +117 -0
- package/docs/examples/reference/data-consistency.example.md +61 -0
- package/docs/examples/reference/security-foco.example.md +39 -0
- package/docs/examples/reference/stack-allowlist.example.json +61 -0
- package/docs/getting-started.md +260 -0
- package/docs/hooks.md +143 -0
- package/hooks/build/build-gate-check.sh +24 -0
- package/hooks/build/coherence-flag.sh +23 -0
- package/hooks/build/gitflow-guard.sh +65 -0
- package/hooks/build/lint-typecheck.sh +33 -0
- package/hooks/build/load-build-state.sh +46 -0
- package/hooks/build/stack-guard.sh +63 -0
- package/hooks/build-harness.json +61 -0
- package/internal/skills/auditar-arnes/SKILL.md +29 -0
- package/package.json +67 -0
- package/scripts/check-agnostic.sh +81 -0
- package/scripts/check-state-clean.sh +48 -0
- package/scripts/check-version-sync.sh +51 -0
- package/scripts/denylist.txt +30 -0
- package/skills/building-a-slice/SKILL.md +82 -0
- package/skills/building-a-slice/references/data-consistency.md +34 -0
- package/skills/building-a-slice/references/dod.md +25 -0
- package/skills/building-a-slice/references/dor.md +17 -0
- package/skills/building-a-slice/references/gitflow.md +30 -0
- package/skills/building-a-slice/references/krug-ux.md +27 -0
- package/skills/building-a-slice/references/link-change-epic.md +34 -0
- package/skills/building-a-slice/references/mcp-map.md +29 -0
- package/skills/building-a-slice/references/newman-tests.md +47 -0
- package/skills/building-a-slice/references/simple-design.md +33 -0
- package/skills/building-a-slice/references/state-protocol.md +48 -0
- package/skills/openspec-apply-change/SKILL.md +156 -0
- package/skills/openspec-archive-change/SKILL.md +114 -0
- package/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/skills/openspec-continue-change/SKILL.md +118 -0
- package/skills/openspec-explore/SKILL.md +290 -0
- package/skills/openspec-ff-change/SKILL.md +101 -0
- package/skills/openspec-new-change/SKILL.md +74 -0
- package/skills/openspec-onboard/SKILL.md +529 -0
- package/skills/openspec-sync-specs/SKILL.md +138 -0
- package/skills/openspec-verify-change/SKILL.md +168 -0
- package/skills/releasing-a-version/SKILL.md +56 -0
- package/skills/releasing-a-version/references/release-dod.md +20 -0
- package/state/README.md +56 -0
- package/state/build-state.schema.json +111 -0
- package/state/build-state.template.json +7 -0
- package/templates/CLAUDE.md.template +57 -0
- package/templates/newman.collection.template.json +28 -0
- package/templates/settings-hooks.template.json +25 -0
- package/templates/waivers/WAIVER.template.md +27 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Getting Started — Tu primer ciclo end-to-end en ~15 min
|
|
2
|
+
|
|
3
|
+
Esta guía te lleva, de forma secuencial y práctica, desde cero hasta cerrar tu primer **slice**
|
|
4
|
+
(una épica `EP-XXX`) con el arnés de construcción de Trycore (`@trycore/spec-build-harness`).
|
|
5
|
+
|
|
6
|
+
El arnés es el **compañero de construcción** de `@trycore/spec-product-flow`: la vertical de
|
|
7
|
+
discovery produce los artefactos (`PRD → User Story Map → Backlog → Historias → Priorización →
|
|
8
|
+
Flows`); este arnés los lleva al **código** mediante un pipeline de **dos loops**.
|
|
9
|
+
|
|
10
|
+
| Loop | Skill | Frecuencia | Qué hace |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| **Inner** (rápido) | `building-a-slice` | por épica `EP-XXX` | DoR → OpenSpec change → TDD → journey-smoke → api/data → DoD reducido → PR + archive |
|
|
13
|
+
| **Outer** (pesado) | `releasing-a-version` | por release (línea del Story Map) | gates de seguridad, diseño, UX, coherencia triple, arquitectura e integración con deps reales — **una sola vez** |
|
|
14
|
+
|
|
15
|
+
Ambas verticales coexisten en el mismo `.claude/` sin colisión (namespaces disjuntos:
|
|
16
|
+
`trycore/` para discovery vs. `opsx/` + `build/` para construcción).
|
|
17
|
+
|
|
18
|
+
> **Regla de oro:** `METODOLOGIA.md` es la fuente de verdad. Si una skill la contradice, **gana la
|
|
19
|
+
> metodología**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Mapa de los 15 minutos
|
|
24
|
+
|
|
25
|
+
| Min | Paso | Quién lo corre |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| 0–2 | 1. Instalar el CLI + requisitos duros | terminal |
|
|
28
|
+
| 2–5 | 2. `trycore-build init` en tu proyecto | terminal |
|
|
29
|
+
| 5–6 | 3. `trycore-build doctor` (verificar) | terminal |
|
|
30
|
+
| 6–9 | 4. `/build:onboard` (parametrizar dominio) | Claude Code |
|
|
31
|
+
| 9–14 | 5. Abrir un slice con la skill `building-a-slice` | Claude Code |
|
|
32
|
+
| 14+ | 6. ¿Cuándo correr `releasing-a-version`? | Claude Code |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Paso 1 · Instalar el CLI y los requisitos duros (~2 min)
|
|
37
|
+
|
|
38
|
+
El CLI `trycore-build` siembra los archivos del arnés. Pero el arnés depende de **tres binarios
|
|
39
|
+
externos** que el CLI **no puede instalar por ti**. `init` y `doctor` **fallan (exit 1)** si falta
|
|
40
|
+
alguno:
|
|
41
|
+
|
|
42
|
+
| Requisito | Para qué | Cómo instalarlo |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `openspec` | columna vertebral de `/opsx:*` y las skills `openspec-*` | `npm i -g @fission-ai/openspec` |
|
|
45
|
+
| `python3` | los hooks parsean `build-state.json` con python3 | gestor del sistema (brew, apt, …) |
|
|
46
|
+
| `git` | los hooks y `gitflow-guard` resuelven la raíz y bloquean commits directos | gestor del sistema |
|
|
47
|
+
|
|
48
|
+
Instala el CLI y OpenSpec:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g @trycore/spec-build-harness # CLI: bin `trycore-build`
|
|
52
|
+
npm install -g @fission-ai/openspec # requisito duro
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> **Canal CLI vs. plugin.** El canal **npm CLI es el canónico** y el recomendado para operar en un
|
|
56
|
+
> proyecto: instala los comandos en `.claude/commands/{opsx,build}/`, namespaceados por subcarpeta
|
|
57
|
+
> (`/opsx:*`, `/build:onboard`), y referencia los agentes por su nombre. El canal **plugin nativo**
|
|
58
|
+
> (`/plugin marketplace add <repo> ; /plugin install trycore-spec-build-harness@trycore-build`) se
|
|
59
|
+
> ofrece como conveniencia a nivel usuario, pero Claude Code namespacea sus componentes bajo el
|
|
60
|
+
> nombre del plugin (`/trycore-spec-build-harness:*`); las cross-references internas (skills que
|
|
61
|
+
> invocan `/opsx:*`, agentes por nombre) están escritas para el canal CLI. **Para construir en un
|
|
62
|
+
> proyecto, usa el CLI.**
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Paso 2 · `trycore-build init` en tu proyecto (~3 min)
|
|
67
|
+
|
|
68
|
+
Sitúate en la raíz de tu proyecto y ejecuta:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
trycore-build init
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`init` es **idempotente** (re-correrlo es seguro) y siembra:
|
|
75
|
+
|
|
76
|
+
- **10 agentes** en `.claude/agents/build/` (build-orchestrator, dor-dod-gatekeeper,
|
|
77
|
+
security-reviewer, simple-design-reviewer, ux-krug-reviewer, coherence-three-way, stack-guardian,
|
|
78
|
+
api-contract-tester, data-consistency-checker, change-epic-coherence).
|
|
79
|
+
- **Comandos** `/opsx:*` (10) en `.claude/commands/opsx/` + `/build:onboard` en `.claude/commands/build/`.
|
|
80
|
+
- **12 skills** en `.claude/skills/` (`building-a-slice`, `releasing-a-version`, 10 `openspec-*`).
|
|
81
|
+
- **6 hooks** bash en `.claude/hooks/build/` + permisos mínimos en `settings.json`.
|
|
82
|
+
- **Estado**: `.claude/state/build-state.json` (sembrado **vacío** y **nunca** sobreescrito; va al
|
|
83
|
+
`.gitignore`), más el schema y el README versionados.
|
|
84
|
+
- **Config**: `.claude/config/stack-allowlist.json` (artefacto del consumidor; lo siembra el CLI y
|
|
85
|
+
lo puebla `/build:onboard`).
|
|
86
|
+
- **Bloque marcado en `CLAUDE.md`** entre `<!-- BEGIN trycore-build-harness ... -->` y
|
|
87
|
+
`<!-- END trycore-build-harness -->`, con `{{placeholders}}` **sin resolver** (los resuelve
|
|
88
|
+
`/build:onboard`).
|
|
89
|
+
|
|
90
|
+
### Onboarding en dos capas
|
|
91
|
+
|
|
92
|
+
El onboarding tiene **dos capas** por una razón técnica: un binario Node **no puede escribir la
|
|
93
|
+
auto-memory de Claude**.
|
|
94
|
+
|
|
95
|
+
1. **Capa mecánica (este CLI)** — captura el stack: lenguaje/deps, package manager, runtime y ruta
|
|
96
|
+
del PRD. Por **prompt TTY** interactivo, o **CI-safe** con flags y `--yes`.
|
|
97
|
+
2. **Capa semántica (`/build:onboard`, lo corre Claude)** — lee el PRD, pregunta por PII, capa de
|
|
98
|
+
servicios externos/IA, capa determinista, secretos y decisiones de alto impacto, resuelve los
|
|
99
|
+
`{{placeholders}}` y escribe la auto-memory. (Paso 4.)
|
|
100
|
+
|
|
101
|
+
#### Flags de `init` (modo no interactivo / CI)
|
|
102
|
+
|
|
103
|
+
| Flag | Para qué |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `--yes` | no interactivo: usa defaults para el stack |
|
|
106
|
+
| `--stack <deps>` | dependencias permitidas, coma-separadas |
|
|
107
|
+
| `--pkg-manager <pm>` | package manager (`npm` \| `pnpm` \| `yarn`) |
|
|
108
|
+
| `--runtime <semver>` | semver del runtime (ej. `">=18.18"`) |
|
|
109
|
+
| `--prd-path <path>` | ruta#ancla del PRD técnico (fuente del allowlist) |
|
|
110
|
+
| `--copy` | copiar archivos en vez de symlinkear (Windows sin admin, sandboxes) |
|
|
111
|
+
| `--force-init` | fuerza modo init aunque exista instalación previa |
|
|
112
|
+
| `--skip-doctor` | omite la verificación de requisitos (no recomendado) |
|
|
113
|
+
|
|
114
|
+
Ejemplo CI-safe:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
trycore-build init --yes \
|
|
118
|
+
--stack "express,zod,pg" --pkg-manager pnpm \
|
|
119
|
+
--runtime ">=18.18" --prd-path "docs/01-prd/prd.md#requisitos-tecnicos"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> Otros comandos del CLI: `trycore-build status` (estado de la instalación y fase del arnés),
|
|
123
|
+
> `trycore-build update` (refresca assets/schema tras actualizar el paquete; **no** pisa estado ni
|
|
124
|
+
> allowlist), `trycore-build uninstall` (quita el arnés **preservando** `state/` y `config/`).
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Paso 3 · `trycore-build doctor` (~1 min)
|
|
129
|
+
|
|
130
|
+
Verifica que el entorno esté listo **antes** de construir:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
trycore-build doctor
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Comprueba:
|
|
137
|
+
|
|
138
|
+
- Los **3 requisitos duros** (`git`, `python3`, `openspec`) — **falla con exit 1** si falta alguno.
|
|
139
|
+
- Que los **6 hooks** tengan bit ejecutable (si no, sugiere `trycore-build init --copy` o `chmod +x`).
|
|
140
|
+
- **Doble canal**: si detecta hooks del arnés en `settings.json` (canal CLI) y además instalaste el
|
|
141
|
+
plugin, te recuerda que la cadena de comando es idéntica en ambos canales y Claude Code
|
|
142
|
+
**deduplica** → el hook dispara **una sola vez**. No requiere acción.
|
|
143
|
+
|
|
144
|
+
Si ves `✓ Requisitos satisfechos.`, continúa.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Paso 4 · `/build:onboard` en Claude Code (~3 min)
|
|
149
|
+
|
|
150
|
+
Abre Claude Code en el proyecto y ejecuta:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
/build:onboard
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Este slash command (lo corre Claude, no el CLI) **parametriza el dominio**: lee tu PRD y/o
|
|
157
|
+
`openspec/project.md` y confirma contigo, vía `AskUserQuestion`, **6 puntos** que luego leen los
|
|
158
|
+
agentes de calidad:
|
|
159
|
+
|
|
160
|
+
| Placeholder | Qué define |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `PRD_TECH_PATH` | ruta#ancla del PRD técnico (fuente del stack) |
|
|
163
|
+
| `EXTERNAL_SERVICE_LAYER` | capa de servicios externos / IA y su frontera/aislamiento |
|
|
164
|
+
| `DETERMINISTIC_LAYER` | lógica que **no** puede delegarse a un servicio no determinista |
|
|
165
|
+
| `SENSITIVE_DATA_CATEGORIES` | categorías reguladas de datos / PII del dominio |
|
|
166
|
+
| `SERVER_SIDE_SECRETS` | claves/tokens que **jamás** van al cliente |
|
|
167
|
+
| `HIGH_STAKES_DECISIONS` | decisiones que exigen explicabilidad/justificación en la UI |
|
|
168
|
+
|
|
169
|
+
Al terminar, `/build:onboard` resuelve los `{{placeholders}}` del bloque marcado en `CLAUDE.md`,
|
|
170
|
+
opcionalmente puebla el `stack-allowlist.json`, y escribe la auto-memory (memorias tipo `project`).
|
|
171
|
+
Si un punto no aplica, se registra explícitamente como "no aplica" (no se deja como `{{...}}`).
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Paso 5 · Abrir un slice con `building-a-slice` (~5 min)
|
|
176
|
+
|
|
177
|
+
Esta es la unidad de trabajo del **inner loop**: **un slice = una épica = un OpenSpec change = una
|
|
178
|
+
rama = un PR**. Las HU de la épica (las que tienen `epica: EP-XXX` en `docs/04-historias/`) son el
|
|
179
|
+
**alcance interno** del change.
|
|
180
|
+
|
|
181
|
+
En Claude Code, invoca la skill (o pídelo en lenguaje natural: *"construye la épica EP-001"*):
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
skill building-a-slice
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
La skill conduce el pipeline (delega en el `build-orchestrator`). Las fases del inner loop:
|
|
188
|
+
|
|
189
|
+
| Fase | Acción | Delega en | Gate |
|
|
190
|
+
|---|---|---|---|
|
|
191
|
+
| 1 · dor | Validar Definition of Ready | `dor-dod-gatekeeper` | `dor` |
|
|
192
|
+
| 2 · change | `/opsx:new` + bloque `## Trazabilidad` que enlaza el change a la épica | `/opsx:new`, `change-epic-coherence` | `coherence_link` |
|
|
193
|
+
| 3 · tdd | red → green → refactor | `superpowers:test-driven-development` | `tdd` |
|
|
194
|
+
| 4 · smoke | recorrer el journey-hasta-aquí end-to-end | skill `verify`/`run` (+ MCP) | `journey_smoke` |
|
|
195
|
+
| 5 · api/data | contratos + consistencia (si aplican) | `api-contract-tester`, `data-consistency-checker` | `api`, `data` |
|
|
196
|
+
| 6 · dod | Definition of Done reducido por slice | `dor-dod-gatekeeper` | `dod` |
|
|
197
|
+
| 7 · pr | abrir PR + archivar el change en el mismo PR | `/opsx:archive`, `/opsx:sync` | — |
|
|
198
|
+
| 8 · release? | preguntar si correr el Release Gate ahora | usuario (default computado) | — |
|
|
199
|
+
|
|
200
|
+
Notas clave del inner loop:
|
|
201
|
+
|
|
202
|
+
- **Esqueleto que camina.** El **primer** slice de una release construye el journey completo más
|
|
203
|
+
delgado posible (aunque cada paso sea un stub). Cada épica posterior **engorda** un paso y mantiene
|
|
204
|
+
el `journey_smoke` verde. Nunca se construyen capas horizontales aisladas que "se juntan al final".
|
|
205
|
+
- **Estado = una sola fuente de verdad.** Todo se sincroniza en `.claude/state/build-state.json`
|
|
206
|
+
(schema y protocolo en `.claude/state/README.md`). Lee antes de actuar; escribe una vez por
|
|
207
|
+
transición. Un slice activo a la vez; un gate no se salta.
|
|
208
|
+
- **Gates pesados NO se cierran aquí.** `stack`, `security`, `smell`, `ux` y la coherencia triple
|
|
209
|
+
completa pertenecen al Release Gate. Lo que sí corre en tiempo real son los **hooks**:
|
|
210
|
+
`stack-guard.sh` (vigila deps fuera del allowlist), `lint-typecheck.sh`, `gitflow-guard.sh`
|
|
211
|
+
(bloquea commits/push directos a `main` — integras solo por **PR**).
|
|
212
|
+
- **El enlace change↔épica** va en el bloque `## Trazabilidad` del `proposal.md`, **nunca** en
|
|
213
|
+
frontmatter YAML (rompe `openspec validate`).
|
|
214
|
+
- Objetivo: **≤ ~20 min por épica**, y producto que **camina end-to-end en todo momento**.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Paso 6 · ¿Cuándo correr `releasing-a-version`? (outer loop)
|
|
219
|
+
|
|
220
|
+
Tras archivar la épica (fase 8), la skill te **pregunta** si correr el Release Gate, con un
|
|
221
|
+
**default computado** desde las líneas de release del Story Map (`docs/02-user-story-map/`):
|
|
222
|
+
|
|
223
|
+
- Si la épica **cierra una línea de release** (todas sus épicas ya están archivadas) → default
|
|
224
|
+
**"Sí, correr `releasing-a-version`"**.
|
|
225
|
+
- Si no la cierra → default **"Continuar a la siguiente épica"**. Excepción (*nudge*): si hay
|
|
226
|
+
**≥ 2 épicas** archivadas desde el último Release Gate, lo recomienda igual.
|
|
227
|
+
|
|
228
|
+
El humano siempre decide. El Release Gate corre las **revisiones pesadas una sola vez**, sobre el
|
|
229
|
+
**diff acumulado** de toda la release:
|
|
230
|
+
|
|
231
|
+
| Gate | Delega en |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `security` | `security-reviewer` |
|
|
234
|
+
| `smell` (4 reglas de Beck + code smells) | `simple-design-reviewer` |
|
|
235
|
+
| `ux` (Krug + lighthouse; `null` si sin UI) | `ux-krug-reviewer` |
|
|
236
|
+
| `coherence` (trazabilidad triple AC↔change↔código) | `coherence-three-way` |
|
|
237
|
+
| `stack_arch` (arquitectura del PRD) | `stack-guardian` |
|
|
238
|
+
| `integration` (journey completo con **deps reales**, no stubs) | skill `verify`/`run` |
|
|
239
|
+
|
|
240
|
+
Los resultados se registran en `build-state.json → releases[]`. **La integración con deps reales es
|
|
241
|
+
obligatoria** para `status: passed`: es el gate que garantiza que el producto realmente funciona al
|
|
242
|
+
terminar. Si algo falla, lo corriges como un slice normal en `building-a-slice` y re-corres el gate.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Resumen del primer ciclo
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
npm i -g @trycore/spec-build-harness @fission-ai/openspec # paso 1
|
|
250
|
+
trycore-build init # paso 2
|
|
251
|
+
trycore-build doctor # paso 3
|
|
252
|
+
# en Claude Code:
|
|
253
|
+
/build:onboard # paso 4
|
|
254
|
+
skill building-a-slice # EP-XXX: DoR → /opsx:new → TDD → smoke → DoD → PR # paso 5
|
|
255
|
+
# y cuando cierres una línea de release del Story Map:
|
|
256
|
+
skill releasing-a-version # paso 6
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
> El core del arnés es **100% agnóstico** al proyecto. El ejemplo de referencia completo vive
|
|
260
|
+
> aparte, en `docs/examples/reference/` (no forma parte del core).
|
package/docs/hooks.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Hooks del arnés de construcción
|
|
2
|
+
|
|
3
|
+
Este documento describe los **6 hooks** que `@trycore/spec-build-harness` instala en el proyecto del consumidor. Todos viven en `hooks/build/` (un script bash por hook) y se cablean a los eventos de Claude Code mediante una **cadena de comando única**, idéntica en ambos canales de instalación.
|
|
4
|
+
|
|
5
|
+
Los hooks son el sistema nervioso del arnés: vigilan GitFlow y el stack declarado (bloqueantes), inyectan el estado del build al iniciar la sesión, delegan estilo/typecheck a herramientas, y recuerdan validar trazabilidad y gates abiertos. No reemplazan a los agentes ni a las skills; los **complementan** liberando capacidad de razonamiento del modelo y poniendo barandillas mecánicas donde un olvido cuesta caro.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Resumen de los 6 hooks
|
|
10
|
+
|
|
11
|
+
| Hook | Evento | Matcher | Qué hace | ¿Bloqueante? |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| `load-build-state.sh` | `SessionStart` | `startup\|clear\|compact` | Inyecta al contexto la rama, la fase del arnés, el slice activo y los gates abiertos; sincroniza `harness_phase`. | No |
|
|
14
|
+
| `gitflow-guard.sh` | `PreToolUse` | `Bash` | Enforce GitHub Flow estricto sobre `git commit` / `git push`. | **Sí (exit 2)** |
|
|
15
|
+
| `stack-guard.sh` | `PreToolUse` | `Write\|Edit\|MultiEdit` | Bloquea dependencias en `package.json` fuera de la allowlist del stack del PRD. | **Sí (exit 2)** |
|
|
16
|
+
| `lint-typecheck.sh` | `PostToolUse` | `Write\|Edit\|MultiEdit` | Corre prettier/eslint/tsc sobre el archivo `.ts`/`.tsx` editado. | No |
|
|
17
|
+
| `coherence-flag.sh` | `PostToolUse` | `Write\|Edit\|MultiEdit` | Recuerda validar la trazabilidad de un `proposal.md` de OpenSpec recién tocado. | No |
|
|
18
|
+
| `build-gate-check.sh` | `Stop` | `.*` | Al cerrar el turno, avisa si el slice activo tiene gates abiertos. | No |
|
|
19
|
+
|
|
20
|
+
> Dos bloqueantes (`gitflow-guard`, `stack-guard`) y cuatro informativos. Los bloqueantes usan `exit 2`: Claude Code devuelve el `stderr` al modelo y aborta la herramienta; el resto siempre sale `0`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Detalle por hook
|
|
25
|
+
|
|
26
|
+
### 1. `load-build-state.sh` — `SessionStart` · no bloqueante
|
|
27
|
+
|
|
28
|
+
Se dispara al arrancar, limpiar o compactar la sesión (`matcher: startup|clear|compact`). Su trabajo:
|
|
29
|
+
|
|
30
|
+
- Resuelve la raíz del repo (`git rev-parse --show-toplevel`, con *fallback* a `$CLAUDE_PROJECT_DIR`).
|
|
31
|
+
- Determina la **fase real del arnés**: `active` si existe `package.json`, `authoring` si no (ver [auto-arme](#auto-arme-inertes-hasta-que-exista-packagejson)).
|
|
32
|
+
- Sincroniza `harness_phase` en `state/build-state.json` si cambió (solo si hay `python3` y el archivo existe).
|
|
33
|
+
- Imprime al contexto la rama actual, la fase, y el **slice activo** con sus **gates pendientes** (lee `active_slice.gates` y lista los que están en `false`). Si no hay slice activo, sugiere abrir uno con la skill `building-a-slice` empezando por el DoR.
|
|
34
|
+
|
|
35
|
+
Es el hook que le da a Claude "memoria de obra" al empezar: sabe en qué historia/épica está, qué change de OpenSpec le corresponde y qué le falta cerrar.
|
|
36
|
+
|
|
37
|
+
### 2. `gitflow-guard.sh` — `PreToolUse` · Bash · **bloqueante**
|
|
38
|
+
|
|
39
|
+
Enforce **GitHub Flow estricto** antes de ejecutar cualquier comando Bash. Parsea el `tool_input.command` del JSON del hook y solo actúa sobre comandos `git` de escritura. Bloquea con `exit 2` en tres casos:
|
|
40
|
+
|
|
41
|
+
1. **Commit directo a `main`/`master`** → exige `git switch -c feature/<slug>`.
|
|
42
|
+
2. **Commit desde una rama no tipada** → solo se permite commitear en `feature/*`, `fix/*` o `chore/*`.
|
|
43
|
+
3. **Push apuntando a `main`/`master`** → la integración va por Pull Request, nunca por push directo.
|
|
44
|
+
|
|
45
|
+
Cualquier otro comando (incluido cualquier `git` que no sea commit/push) sale `0`. La política está alineada con el inner loop (la skill `building-a-slice` cierra cada slice con PR + archive).
|
|
46
|
+
|
|
47
|
+
### 3. `stack-guard.sh` — `PreToolUse` · Write/Edit/MultiEdit · **bloqueante**
|
|
48
|
+
|
|
49
|
+
Vigila el **contrato de stack** declarado en el PRD. Solo le importan las ediciones que tocan `package.json`. Compara las dependencias del contenido entrante (`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`) contra `config/stack-allowlist.json` —usando *globs* (`fnmatch`)— y **bloquea con `exit 2`** si aparece alguna dependencia fuera de la allowlist.
|
|
50
|
+
|
|
51
|
+
- Sabe extraer nombres tanto de un `package.json` completo como de un fragmento de edición (regex sobre pares `"nombre": "versión"`).
|
|
52
|
+
- El mensaje guía a actualizar `config/stack-allowlist.json` y dejar nota en `GOVERNANCE.md`, o a consultar al agente `stack-guardian`.
|
|
53
|
+
- La allowlist es **artefacto del consumidor**: el CLI la siembra y `/build:onboard` la puebla a partir del PRD.
|
|
54
|
+
|
|
55
|
+
### 4. `lint-typecheck.sh` — `PostToolUse` · Write/Edit/MultiEdit · no bloqueante
|
|
56
|
+
|
|
57
|
+
Tras editar un archivo `.ts`/`.tsx`, delega el estilo a las herramientas para no gastar tokens del modelo en formateo. Si los binarios existen en `node_modules/.bin/`, corre:
|
|
58
|
+
|
|
59
|
+
- `prettier --write` sobre el archivo.
|
|
60
|
+
- `eslint --fix` sobre el archivo (primeras 20 líneas de salida a `stderr`).
|
|
61
|
+
- `tsc --noEmit` y filtra los errores que mencionan el archivo editado.
|
|
62
|
+
|
|
63
|
+
Nunca bloquea (siempre `exit 0`); reporta a `stderr` como información. Inerte si no hay `package.json` o si el archivo no es `.ts`/`.tsx`.
|
|
64
|
+
|
|
65
|
+
### 5. `coherence-flag.sh` — `PostToolUse` · Write/Edit/MultiEdit · no bloqueante
|
|
66
|
+
|
|
67
|
+
Cuando se edita un `openspec/changes/**/proposal.md`, imprime un recordatorio: correr el agente `change-epic-coherence` para validar el bloque `## Trazabilidad` (Épica `EP-XXX` / Historias `HU-XXX`) y ejecutar `openspec validate`. Es un *nudge* de coherencia change↔épica, no un control de bloqueo.
|
|
68
|
+
|
|
69
|
+
### 6. `build-gate-check.sh` — `Stop` · no bloqueante
|
|
70
|
+
|
|
71
|
+
Al cerrar el turno, si hay un slice activo con gates en `false`, avisa por `stderr` qué gates quedan abiertos y recuerda **no archivar ni abrir PR** hasta cerrarlos (ver skill `building-a-slice` / `dod.md`). Inerte si no hay `package.json`, ni estado, ni `python3`.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## La cadena de comando única (sin doble disparo entre canales)
|
|
76
|
+
|
|
77
|
+
Todos los hooks se invocan con **la misma cadena autorresolutiva**, idéntica en el canal CLI y en el canal plugin:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/<script>.sh"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- Con el **plugin** instalado, `CLAUDE_PLUGIN_ROOT` apunta a la raíz del plugin → los scripts se resuelven dentro de él.
|
|
84
|
+
- Sin plugin (**CLI puro**), el *fallback* `$CLAUDE_PROJECT_DIR/.claude` resuelve los scripts sembrados por `trycore-build init`.
|
|
85
|
+
|
|
86
|
+
Como la cadena es **carácter por carácter idéntica** en ambos canales, si el consumidor instala **CLI y plugin a la vez**, Claude Code las **deduplica** y cada hook **dispara una sola vez**. Por eso el merge del CLI compara por *cadena exacta* (no por substring de ruta): así el alta es idempotente y la baja (`uninstall`) remueve **solo** lo que el paquete inyectó, preservando los hooks propios del consumidor.
|
|
87
|
+
|
|
88
|
+
> Las comillas envuelven la cadena para soportar rutas con espacios.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## La guarda `python3` (fail-closed dirigido)
|
|
93
|
+
|
|
94
|
+
Los hooks parsean el JSON del evento con `python3`. Qué pasa si **falta** `python3` depende de si el hook es bloqueante:
|
|
95
|
+
|
|
96
|
+
- **Bloqueantes** (`gitflow-guard`, `stack-guard`) → **fail-closed dirigido**: si no pueden analizar el comando/edición, solo bloquean (`exit 2`) cuando la entrada *cruda* parece relevante (un `git commit`/`push`, o una edición que menciona `package.json`); en cualquier otro caso salen `0`. Esto evita falsos negativos peligrosos sin frenar el trabajo no relacionado. El mensaje pide instalar `python3` (verificable con `trycore-build doctor`).
|
|
97
|
+
- **No bloqueantes** (`load-build-state`, `lint-typecheck`, `coherence-flag`, `build-gate-check`) → si falta `python3`, simplemente **omiten** su trabajo y salen `0`.
|
|
98
|
+
|
|
99
|
+
> `python3` es un **requisito duro**: `trycore-build init` y `trycore-build doctor` **fallan** si no está presente, justo porque toda la cadena de hooks depende de él para leer el JSON del evento.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Auto-arme (inertes hasta que exista `package.json`)
|
|
104
|
+
|
|
105
|
+
Los hooks que tocan el código construido se **auto-arman**: permanecen **inertes durante la fase `authoring`** (mientras no exista `package.json` en disco) y se activan solos al aparecer el primer `package.json` (fase `active`):
|
|
106
|
+
|
|
107
|
+
| Hook | Comportamiento sin `package.json` |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `load-build-state.sh` | Funciona siempre; reporta fase `authoring`. |
|
|
110
|
+
| `gitflow-guard.sh` | Funciona siempre (GitFlow aplica desde el primer commit). |
|
|
111
|
+
| `stack-guard.sh` | Inerte si no existe la allowlist; sin `package.json` que comparar, no hay violación que detectar. |
|
|
112
|
+
| `lint-typecheck.sh` | Inerte (sale `0` de inmediato). |
|
|
113
|
+
| `coherence-flag.sh` | Funciona siempre (depende de OpenSpec, no del código). |
|
|
114
|
+
| `build-gate-check.sh` | Inerte (sale `0` de inmediato). |
|
|
115
|
+
|
|
116
|
+
Así el arnés convive sin fricción con la fase de *discovery* y se "enciende" cuando empieza la construcción real.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Cómo se instalan
|
|
121
|
+
|
|
122
|
+
Una sola definición de hooks, expresada en dos archivos espejo según el canal:
|
|
123
|
+
|
|
124
|
+
### Canal CLI (canónico) — merge en `settings.json`
|
|
125
|
+
|
|
126
|
+
`trycore-build init` hace un **merge idempotente y aditivo** en el `settings.json` del consumidor (lógica en `src/lib/settings-merge.ts`; espejo documental en `templates/settings-hooks.template.json`):
|
|
127
|
+
|
|
128
|
+
- Agrega las 5 agrupaciones de hooks (las 6 invocaciones: `PostToolUse` agrupa `lint-typecheck` + `coherence-flag`) sin pisar lo que ya exista.
|
|
129
|
+
- Agrega **permisos mínimos y enumerados** (sin `mcp__*` ni rutas absolutas):
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Bash(openspec validate *)
|
|
133
|
+
Bash(openspec list *)
|
|
134
|
+
Bash(openspec show *)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `trycore-build uninstall` remueve **solo** las cadenas que el paquete inyectó (por set exacto) y preserva el resto de la config, incluidos `state/` y `config/`.
|
|
138
|
+
|
|
139
|
+
### Canal plugin — `hooks/build-harness.json`
|
|
140
|
+
|
|
141
|
+
El plugin declara los hooks en `hooks/build-harness.json` (referenciado desde `.claude-plugin/plugin.json` con `"hooks": "./hooks/build-harness.json"`). El contenido es **equivalente** al bloque que mergea el CLI y usa la **misma cadena de comando**.
|
|
142
|
+
|
|
143
|
+
> **Caveat de canales:** el canal CLI es el **canónico** para operar en un proyecto. El plugin namespacea los componentes bajo el nombre del plugin (`/trycore-spec-build-harness:*`) por diseño de Claude Code; las cross-references internas (skills que invocan `/opsx:*`, agentes por nombre) están escritas para el canal CLI. Los **hooks**, en cambio, son idénticos en ambos canales y se deduplican si coexisten.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# build-gate-check.sh — Stop
|
|
3
|
+
# AUTO-ARME: inerte mientras no exista package.json.
|
|
4
|
+
# No bloquea: al cerrar el turno, avisa si hay un slice activo con gates abiertos.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-$(pwd)}")"
|
|
8
|
+
[ -f "$ROOT/package.json" ] || exit 0
|
|
9
|
+
STATE="$ROOT/.claude/state/build-state.json"
|
|
10
|
+
[ -f "$STATE" ] || exit 0
|
|
11
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
12
|
+
|
|
13
|
+
python3 - "$STATE" <<'PY' 2>/dev/null || true
|
|
14
|
+
import json,sys
|
|
15
|
+
d=json.load(open(sys.argv[1]))
|
|
16
|
+
s=d.get("active_slice")
|
|
17
|
+
if not s: sys.exit(0)
|
|
18
|
+
g=s.get("gates",{})
|
|
19
|
+
abiertos=[k for k,v in g.items() if v is False]
|
|
20
|
+
if abiertos:
|
|
21
|
+
print(f"build-gate-check: slice {s.get('hu')} en fase '{s.get('phase')}' con gates abiertos: {', '.join(abiertos)}.", file=sys.stderr)
|
|
22
|
+
print(" No archives ni abras PR hasta cerrarlos (ver building-a-slice / dod.md).", file=sys.stderr)
|
|
23
|
+
PY
|
|
24
|
+
exit 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# coherence-flag.sh — PostToolUse · Write/Edit en openspec/changes/**/proposal.md
|
|
3
|
+
# No bloquea: recuerda validar la trazabilidad del change recién tocado.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
|
|
6
|
+
INPUT="$(cat)"
|
|
7
|
+
|
|
8
|
+
# Guarda python3 [H4]: hook informativo no-bloqueante; si falta python3, omite el recordatorio.
|
|
9
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
10
|
+
|
|
11
|
+
FILE="$(printf '%s' "$INPUT" | python3 -c 'import sys,json
|
|
12
|
+
try:
|
|
13
|
+
ti=json.load(sys.stdin).get("tool_input",{})
|
|
14
|
+
print(ti.get("file_path") or ti.get("path") or "")
|
|
15
|
+
except Exception:
|
|
16
|
+
print("")' 2>/dev/null)"
|
|
17
|
+
|
|
18
|
+
case "$FILE" in
|
|
19
|
+
*openspec/changes/*/proposal.md)
|
|
20
|
+
echo "🔗 proposal.md modificado: corre el agente change-epic-coherence para validar el bloque '## Trazabilidad' (Épica EP-XXX / Historias HU-XXX) y 'openspec validate'." >&2
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
exit 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# gitflow-guard.sh — PreToolUse · Bash
|
|
3
|
+
# Enforce GitHub Flow estricto:
|
|
4
|
+
# - Prohíbe `git commit` directo en main/master.
|
|
5
|
+
# - Prohíbe `git push` que apunte a main/master (la integración va por PR).
|
|
6
|
+
# - Exige trabajar en rama feature/* | fix/* | chore/* para commitear.
|
|
7
|
+
# Bloquea con exit 2 (stderr se devuelve a Claude). Cualquier otra cosa -> exit 0.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
INPUT="$(cat)"
|
|
11
|
+
|
|
12
|
+
# Guarda python3 [H4]: si falta, no podemos parsear el JSON del hook. Fail-closed DIRIGIDO:
|
|
13
|
+
# solo bloqueamos (exit 2) si el comando crudo parece un git commit/push; si no, exit 0.
|
|
14
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
15
|
+
if printf '%s' "$INPUT" | grep -Eq 'git[^"]*(commit|push)'; then
|
|
16
|
+
echo "⛔ gitflow-guard: python3 no disponible; no puedo analizar el comando git. Instala python3 (trycore-build doctor)." >&2
|
|
17
|
+
exit 2
|
|
18
|
+
fi
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
CMD="$(printf '%s' "$INPUT" | python3 -c 'import sys,json
|
|
23
|
+
try:
|
|
24
|
+
print(json.load(sys.stdin).get("tool_input",{}).get("command",""))
|
|
25
|
+
except Exception:
|
|
26
|
+
print("")')"
|
|
27
|
+
|
|
28
|
+
# Sólo nos interesan comandos git de escritura.
|
|
29
|
+
if ! printf '%s' "$CMD" | grep -Eq '(^|[;&| ])git[ ]'; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
is_commit=false
|
|
34
|
+
is_push=false
|
|
35
|
+
printf '%s' "$CMD" | grep -Eq '(^|[;&| ])git[ ]+(.*[ ])?commit([ ]|$)' && is_commit=true
|
|
36
|
+
printf '%s' "$CMD" | grep -Eq '(^|[;&| ])git[ ]+(.*[ ])?push([ ]|$)' && is_push=true
|
|
37
|
+
|
|
38
|
+
if [ "$is_commit" = false ] && [ "$is_push" = false ]; then
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
|
|
43
|
+
|
|
44
|
+
block() {
|
|
45
|
+
echo "⛔ gitflow-guard (GitHub Flow estricto): $1" >&2
|
|
46
|
+
echo " Política: main protegida · trabajar en feature/* | fix/* | chore/* · integrar por PR." >&2
|
|
47
|
+
exit 2
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# 1) Commit directo a main/master.
|
|
51
|
+
if [ "$is_commit" = true ] && printf '%s' "$BRANCH" | grep -Eq '^(main|master)$'; then
|
|
52
|
+
block "commit directo a '$BRANCH' no permitido. Crea una rama: git switch -c feature/<slug>"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# 2) Commit desde una rama no tipada.
|
|
56
|
+
if [ "$is_commit" = true ] && [ -n "$BRANCH" ] && ! printf '%s' "$BRANCH" | grep -Eq '^(feature|fix|chore)/'; then
|
|
57
|
+
block "rama '$BRANCH' no sigue la convención. Usa feature/* | fix/* | chore/* para commitear."
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# 3) Push apuntando a main/master.
|
|
61
|
+
if [ "$is_push" = true ] && printf '%s' "$CMD" | grep -Eq 'push[^|;&]*[ :](main|master)([ ]|$|:)'; then
|
|
62
|
+
block "push directo a main/master no permitido. Abre un Pull Request desde tu rama feature/*."
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lint-typecheck.sh — PostToolUse · Write/Edit en *.ts/*.tsx
|
|
3
|
+
# AUTO-ARME: inerte mientras no exista package.json (fase authoring).
|
|
4
|
+
# No bloquea: reporta lint/format/typecheck del archivo editado para liberar
|
|
5
|
+
# capacidad de razonamiento del modelo (estilo delegado a herramientas).
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-$(pwd)}")"
|
|
9
|
+
[ -f "$ROOT/package.json" ] || exit 0 # guard de auto-arme
|
|
10
|
+
|
|
11
|
+
# Guarda python3 [H4]: hook no-bloqueante; sin python3 no podemos extraer el archivo → omite.
|
|
12
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
13
|
+
|
|
14
|
+
INPUT="$(cat)"
|
|
15
|
+
FILE="$(printf '%s' "$INPUT" | python3 -c 'import sys,json
|
|
16
|
+
try:
|
|
17
|
+
ti=json.load(sys.stdin).get("tool_input",{})
|
|
18
|
+
print(ti.get("file_path") or ti.get("path") or "")
|
|
19
|
+
except Exception:
|
|
20
|
+
print("")' 2>/dev/null)"
|
|
21
|
+
|
|
22
|
+
case "$FILE" in
|
|
23
|
+
*.ts|*.tsx) : ;;
|
|
24
|
+
*) exit 0 ;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
cd "$ROOT" || exit 0
|
|
28
|
+
HAS() { [ -d node_modules ] && [ -x "node_modules/.bin/$1" ]; }
|
|
29
|
+
|
|
30
|
+
if HAS prettier; then node_modules/.bin/prettier --write "$FILE" >/dev/null 2>&1 || true; fi
|
|
31
|
+
if HAS eslint; then node_modules/.bin/eslint --fix "$FILE" 2>&1 | sed -n '1,20p' >&2 || true; fi
|
|
32
|
+
if HAS tsc; then node_modules/.bin/tsc --noEmit 2>&1 | grep -F "$FILE" | sed -n '1,20p' >&2 || true; fi
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# load-build-state.sh — SessionStart
|
|
3
|
+
# Inyecta al contexto: rama actual, harness_phase, slice activo y gates abiertos.
|
|
4
|
+
# Además sincroniza harness_phase (authoring -> active) según exista package.json.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-$(pwd)}")"
|
|
8
|
+
STATE="$ROOT/.claude/state/build-state.json"
|
|
9
|
+
BRANCH="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'desconocida')"
|
|
10
|
+
|
|
11
|
+
# Determina la fase real del arnés.
|
|
12
|
+
if [ -f "$ROOT/package.json" ]; then PHASE="active"; else PHASE="authoring"; fi
|
|
13
|
+
|
|
14
|
+
# Sincroniza harness_phase en el estado (si python3 disponible y el archivo existe).
|
|
15
|
+
if [ -f "$STATE" ] && command -v python3 >/dev/null 2>&1; then
|
|
16
|
+
python3 - "$STATE" "$PHASE" <<'PY' 2>/dev/null || true
|
|
17
|
+
import json,sys
|
|
18
|
+
path,phase=sys.argv[1],sys.argv[2]
|
|
19
|
+
try:
|
|
20
|
+
d=json.load(open(path))
|
|
21
|
+
if d.get("harness_phase")!=phase:
|
|
22
|
+
d["harness_phase"]=phase
|
|
23
|
+
json.dump(d,open(path,"w"),indent=2,ensure_ascii=False)
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
PY
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
echo "🏗️ Arnés de construcción"
|
|
30
|
+
echo " Rama: $BRANCH | Fase del arnés: $PHASE"
|
|
31
|
+
|
|
32
|
+
if [ -f "$STATE" ] && command -v python3 >/dev/null 2>&1; then
|
|
33
|
+
python3 - "$STATE" <<'PY' 2>/dev/null || true
|
|
34
|
+
import json,sys
|
|
35
|
+
d=json.load(open(sys.argv[1]))
|
|
36
|
+
s=d.get("active_slice")
|
|
37
|
+
if not s:
|
|
38
|
+
print(" Slice activo: ninguno. Usa la skill building-a-slice para abrir uno (empieza por DoR).")
|
|
39
|
+
else:
|
|
40
|
+
g=s.get("gates",{})
|
|
41
|
+
abiertos=[k for k,v in g.items() if v is False]
|
|
42
|
+
print(f" Slice activo: {s.get('hu')} ({s.get('epica')}) · change={s.get('openspec_change')} · fase={s.get('phase')}")
|
|
43
|
+
print(f" Gates pendientes: {', '.join(abiertos) if abiertos else 'ninguno ✅'}")
|
|
44
|
+
PY
|
|
45
|
+
fi
|
|
46
|
+
exit 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# stack-guard.sh — PreToolUse · Write/Edit en package.json
|
|
3
|
+
# AUTO-ARME: si aún no existe package.json en disco (primer scaffold), no hay
|
|
4
|
+
# qué comparar -> exit 0. Una vez existe, bloquea (exit 2) la introducción de
|
|
5
|
+
# dependencias fuera de .claude/config/stack-allowlist.json (el contrato de stack del PRD).
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-$(pwd)}")"
|
|
9
|
+
ALLOW="$ROOT/.claude/config/stack-allowlist.json"
|
|
10
|
+
[ -f "$ALLOW" ] || exit 0
|
|
11
|
+
|
|
12
|
+
INPUT="$(cat)"
|
|
13
|
+
|
|
14
|
+
# Guarda python3 [H4]: si falta, no podemos verificar las deps. Fail-closed DIRIGIDO:
|
|
15
|
+
# solo bloqueamos (exit 2) si la edición toca package.json; si no, exit 0.
|
|
16
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
17
|
+
if printf '%s' "$INPUT" | grep -q 'package\.json'; then
|
|
18
|
+
echo "⛔ stack-guard: python3 no disponible; no puedo verificar las dependencias de package.json. Instala python3 (trycore-build doctor)." >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# El script va por heredoc (= stdin), así que el JSON del hook se pasa por la
|
|
25
|
+
# variable de entorno INPUT, no por stdin.
|
|
26
|
+
VIOL="$(ALLOW="$ALLOW" INPUT="$INPUT" python3 <<'PY' 2>/dev/null
|
|
27
|
+
import os,sys,json,re,fnmatch
|
|
28
|
+
try:
|
|
29
|
+
data=json.loads(os.environ.get("INPUT") or "{}")
|
|
30
|
+
except Exception:
|
|
31
|
+
sys.exit(0)
|
|
32
|
+
ti=data.get("tool_input",{})
|
|
33
|
+
fp=ti.get("file_path") or ti.get("path") or ""
|
|
34
|
+
if not fp.endswith("package.json"):
|
|
35
|
+
sys.exit(0)
|
|
36
|
+
content=ti.get("content") or ti.get("new_string") or ""
|
|
37
|
+
if not content.strip():
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
allow=json.load(open(os.environ["ALLOW"])).get("allow",[])
|
|
40
|
+
def ok(name):
|
|
41
|
+
return any(fnmatch.fnmatch(name, pat) for pat in allow)
|
|
42
|
+
# Extrae nombres de deps tanto de JSON completo como de fragmentos de edición.
|
|
43
|
+
deps=set()
|
|
44
|
+
try:
|
|
45
|
+
pkg=json.loads(content)
|
|
46
|
+
for sec in ("dependencies","devDependencies","peerDependencies","optionalDependencies"):
|
|
47
|
+
deps.update((pkg.get(sec) or {}).keys())
|
|
48
|
+
except Exception:
|
|
49
|
+
for m in re.finditer(r'"((?:@[\w.-]+/)?[\w.-]+)"\s*:\s*"[\^~>=<*\d][^"]*"', content):
|
|
50
|
+
deps.add(m.group(1))
|
|
51
|
+
bad=sorted(d for d in deps if d and not ok(d))
|
|
52
|
+
if bad:
|
|
53
|
+
print(" ".join(bad))
|
|
54
|
+
PY
|
|
55
|
+
)"
|
|
56
|
+
|
|
57
|
+
if [ -n "$VIOL" ]; then
|
|
58
|
+
echo "⛔ stack-guard: dependencias fuera del stack declarado del PRD (allowlist): $VIOL" >&2
|
|
59
|
+
echo " Si es intencional, actualiza .claude/config/stack-allowlist.json y deja nota en GOVERNANCE.md," >&2
|
|
60
|
+
echo " o consulta al agente stack-guardian. Allowlist: .claude/config/stack-allowlist.json" >&2
|
|
61
|
+
exit 2
|
|
62
|
+
fi
|
|
63
|
+
exit 0
|