@trycore/spec-build-harness 0.8.2 → 0.8.4
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/plugin.json +5 -2
- package/METODOLOGIA.md +29 -4
- package/README.md +3 -1
- package/VERSION +1 -1
- package/agents/build/dor-dod-gatekeeper.md +26 -0
- package/commands/build/onboard.md +43 -0
- package/commands/build/slice.md +5 -0
- package/commands/build/work.md +9 -0
- package/dist/commands/doctor.js +8 -0
- package/dist/commands/init.js +3 -1
- package/dist/commands/status.js +19 -0
- package/dist/lib/state-seed.js +95 -1
- package/package.json +1 -1
- package/scripts/tests/test-install.sh +88 -0
- package/scripts/tests/test-schema.sh +21 -0
- package/skills/building-a-slice/references/dor.md +10 -0
- package/skills/building-a-slice/references/foundation-contract.md +44 -0
- package/skills/building-a-slice/workflows/README.md +7 -1
- package/skills/building-a-slice/workflows/dor-fanout.workflow.js +98 -0
- package/skills/releasing-a-version/SKILL.md +4 -1
- package/skills/releasing-a-version/workflows/README.md +6 -1
- package/skills/releasing-a-version/workflows/release-gate.workflow.js +59 -4
- package/state/README.md +22 -0
- package/state/build-state.schema.json +40 -1
- package/state/build-state.template.json +8 -0
- package/templates/CLAUDE.md.template +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "trycore-spec-build-harness",
|
|
4
4
|
"displayName": "Trycore — Spec & Build Harness",
|
|
5
|
-
"version": "0.8.
|
|
5
|
+
"version": "0.8.4",
|
|
6
6
|
"description": "Arnés de construcción de dos loops (slice por épica + release gate) para Claude Code, con gates de calidad, estado compartido y OpenSpec. Compañero de @trycore/spec-product-flow. Agnóstico al proyecto.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Trycore",
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
"trycore"
|
|
23
23
|
],
|
|
24
24
|
"skills": "./skills/",
|
|
25
|
-
"commands": [
|
|
25
|
+
"commands": [
|
|
26
|
+
"./commands/opsx/",
|
|
27
|
+
"./commands/build/"
|
|
28
|
+
],
|
|
26
29
|
"agents": "./agents/build/",
|
|
27
30
|
"hooks": "./hooks/build-harness.json"
|
|
28
31
|
}
|
package/METODOLOGIA.md
CHANGED
|
@@ -124,6 +124,18 @@ sola pasada**: a medida que crece el contexto, la atención se degrada ("context
|
|
|
124
124
|
> construye **encima** del scaffold: el scaffold es el shell vacío que arranca; el walking skeleton
|
|
125
125
|
> es el primer journey real más delgado. Sin scaffold confirmado no se abre ningún slice.
|
|
126
126
|
|
|
127
|
+
> **Paso 1-bis — la épica caparazón (solo greenfield).** En un proyecto **nuevo**
|
|
128
|
+
> (`project_kind: "greenfield"`), entre el scaffold y las épicas de negocio existe un tercer
|
|
129
|
+
> concepto: el **caparazón** — navegación/menús, layout/panel central, homepage, login/authN y
|
|
130
|
+
> redirecciones/guards (contrato podable en
|
|
131
|
+
> `skills/building-a-slice/references/foundation-contract.md`). Se modela como **épica
|
|
132
|
+
> `layer: foundational` obligatoria y primera**: el DoR bloquea **proactivamente** toda épica
|
|
133
|
+
> `business` hasta archivarla con su checklist **evidenciada** (gate de proyecto `foundation`).
|
|
134
|
+
> El arnés **propone** su borrador y solo lo escribe con aprobación humana (§9.2); si se
|
|
135
|
+
> rechaza, se crea en discovery. En **brownfield** el mecanismo es N/A y el arnés no pregunta.
|
|
136
|
+
> Scaffold = shell vacío que arranca; caparazón = shell con contenido fundacional; walking
|
|
137
|
+
> skeleton = primer journey de negocio más delgado, construido **sobre** ambos.
|
|
138
|
+
|
|
127
139
|
El arnés prohíbe construir capas horizontales aisladas que "se juntan al final" (el anti-patrón que
|
|
128
140
|
hace que todos los gates por-slice estén en verde y el producto aun así no funcione de punta a
|
|
129
141
|
punta). En su lugar:
|
|
@@ -188,6 +200,11 @@ Una épica **no entra a construcción** hasta cumplir todo (lo valida `dor-dod-g
|
|
|
188
200
|
acceso a datos, arquitectura base, design-system/componentes base) ya existe como épica(s)
|
|
189
201
|
`layer: foundational` **archivada(s)**. Si arrastra cimiento no construido → STOP: se extrae a una
|
|
190
202
|
épica fundacional previa. La cláusula "no bloquean" de dependencias **no aplica al cimiento**.
|
|
203
|
+
- **Caparazón construido (solo greenfield — PROACTIVO)**: con `project_kind: "greenfield"` y
|
|
204
|
+
`foundation.required: true`, ninguna épica `business` abre hasta que la épica caparazón esté
|
|
205
|
+
archivada con su checklist evidenciada (`foundation.completed_at`); las fundacionales sí abren.
|
|
206
|
+
No depende de detectar arrastre (eso es "Cimiento construido"): en greenfield bloquea siempre.
|
|
207
|
+
Brownfield → N/A.
|
|
191
208
|
- **Tamaño acotado (gate de descomposición)**: si la épica supera el umbral —por defecto **> 3 HU** ó
|
|
192
209
|
**≥ 3 capas tocadas**, configurable— se descompone en `sub_slices[]` construidos de a uno
|
|
193
210
|
(`journey_smoke` verde entre cada uno). Umbral proporcional, no cuota rígida.
|
|
@@ -476,10 +493,13 @@ La construcción **consume** los artefactos de discovery y los trata como entrad
|
|
|
476
493
|
|
|
477
494
|
El arnés **no escribe** en los artefactos de discovery (`docs/01-prd/` … `docs/04-historias/`); cuando
|
|
478
495
|
una HU no cumple DoR, devuelve el trabajo a discovery (`/trycore:*`). Los contactos de escritura del
|
|
479
|
-
arnés en `docs/` son
|
|
480
|
-
archivar (§6.4),
|
|
481
|
-
arquitectura (§9.3)
|
|
482
|
-
|
|
496
|
+
arnés en `docs/` son **tres** y están acotados: la **back-reference** del change en la épica y las HU
|
|
497
|
+
al archivar (§6.4), el subárbol **`docs/adr/`** —propiedad de construcción— que produce la capa de
|
|
498
|
+
arquitectura (§9.3), y la **épica caparazón** en `docs/03-backlog/epicas.md` — únicamente esa épica,
|
|
499
|
+
únicamente con **aprobación humana explícita** del borrador propuesto en `/build:onboard` Fase 2c
|
|
500
|
+
(frontmatter `origin: harness-draft`); si el humano rechaza, la épica se crea en discovery como
|
|
501
|
+
siempre. `docs/adr/` **no** es un artefacto de discovery: es la salida de construcción que traza
|
|
502
|
+
*hacia* discovery (HU/PRD) sin modificarla.
|
|
483
503
|
|
|
484
504
|
### 9.3 Capa de arquitectura (ADD) — entre discovery y construcción
|
|
485
505
|
|
|
@@ -544,3 +564,8 @@ salida es file-based y la propuesta se materializa en git, mañana irá por API
|
|
|
544
564
|
disjuntas en `files_scope` se paralelizan; una épica `layer: foundational` nunca entra al front y
|
|
545
565
|
lo pone en `draining` hasta que se vacía; el merge exige re-smoke del journey completo tras cada
|
|
546
566
|
integración.
|
|
567
|
+
13. **Caparazón primero en greenfield (§2 Paso 1-bis)**: en un proyecto nuevo, la épica caparazón
|
|
568
|
+
(`foundation.epic`) se construye y archiva **antes** que cualquier épica `business`; su DoD
|
|
569
|
+
exige evidencia de ejecución por cada ítem `applies: true` del contrato. En brownfield el
|
|
570
|
+
mecanismo es N/A y no se pregunta. El arnés propone el borrador de la épica; solo lo escribe
|
|
571
|
+
con aprobación humana explícita (§9.2).
|
package/README.md
CHANGED
|
@@ -183,7 +183,9 @@ El core no menciona ningún dominio de cliente. Toda parametrización entra por
|
|
|
183
183
|
- ✅ **v0.7.0** — **orquestación con workflows dinámicos + hardening** (de una evaluación adversarial del propio arnés): **3 plantillas `*.workflow.js`** opt-in y read-only (`explore-fanout`, `wiring-verify`, `release-gate`) que entran **solo donde aportan valor** y nunca en el camino caliente del inner loop; **3 comandos nuevos** `/build:slice` (entrada del inner loop), `/build:release` (outer loop) y `/build:work` (router *classify-and-act*); hook **`release-gate-nudge.sh`** (Stop, determinista: solo sugiere el Release Gate). Rename de los gates de los 5 reviewers pesados → `releases[].gates.{security,smell,ux,coherence,stack_arch}` (`stack`→`stack_arch`; separación `coherence` (release) / `coherence_link` (inner)). Hardening: degradación segura en 8 agentes, escritura atómica del estado, cierre del bypass de specs no-semver, `wiring` exige evidencia ejecutada y `check-agnostic` barre `*.js`. Total: **12 agentes**, **10 hooks**, **5 comandos `/build:*`**.
|
|
184
184
|
- ✅ **v0.8.0** — **motor de contexto + estado anclado a disco + front paralelo inter-épica**: hooks **`statusline-bridge.sh`** (canal CLI) + **`context-monitor.sh`** (umbrales `context.warning_pct`/`context.critical_pct` configurables, 35%/25% por defecto) con **auto-handoff** a `session_continuity` en critical/`PreCompact` y comando **`/build:resume`** para rehidratar desde disco; config **`context.auto_checkpoint`** (opt-in). Reconciliador **`reconcile-build-state.py`** (`SessionStart`): deriva de git + evidencia de tests, degrada `wiring_checklist` sin evidencia, anota *branch drift*, ratchet de gates, fail-open. **Front paralelo** (`parallel_front` en el estado): comando **`/build:front`** + skill **`managing-parallel-front`** + `scripts/lib/front-plan.py` (disjunción por `files_scope`, foundational-first). Schema nuevo: `slice.layer`, `slice.files_scope`, `slice.branch_drift`, `slice.session_continuity`. **Caveat:** `statusLine` es solo canal CLI; en plugin-only el motor de contexto degrada fail-open. Total: **12 agentes**, **13 hooks**, **7 comandos `/build:*`**, **14 skills**.
|
|
185
185
|
- ✅ **v0.8.1** — **hotfix del motor de contexto**: `context-monitor.sh` re-inyectaba `additionalContext` en cada evento `Stop`, lo que re-lanzaba el turno en bucle hasta el tope `CLAUDE_CODE_STOP_HOOK_BLOCK_CAP` (9→override). Ahora en `Stop` re-lanza como mucho una vez por sesión (solo la transición a crítico que graba el handoff) y `warning` nunca inyecta en `Stop`. Cubierto por `test-context-monitor.sh`.
|
|
186
|
-
- ✅ **v0.8.
|
|
186
|
+
- ✅ **v0.8.4 (actual)** — **épica caparazón obligatoria en greenfield**: nuevo campo `project_kind` con detección automática conservadora en `trycore-build init` (brownfield seguro = manifiesto + código + **historial git con commits de código**; ambigüedad → una sola pregunta en `/build:onboard`; **en brownfield el mecanismo es N/A total, sin preguntar**) y gate de proyecto **`foundation`**: contrato del caparazón (navegación/menús · layout/panel central · homepage · login/authN · redirecciones/guards) como checklist podable con **evidencia de ejecución por ítem** (patrón `wiring_checklist`, contrato en `skills/building-a-slice/references/foundation-contract.md`). `/build:onboard` **Fase 2c** poda el contrato y redacta el **borrador híbrido** de la épica (solo se escribe en `epicas.md` con aprobación humana explícita — carve-out §9.2). **DoR 7-bis proactivo**: en greenfield ninguna épica `business` abre slice hasta que la caparazón esté **archivada con checklist evidenciada** (`foundation.completed_at`); las fundacionales y el carril micro-change nunca se bloquean. Línea `Caparazón` en `status`/`doctor`. Retrocompatible: campos opcionales del schema.
|
|
187
|
+
- ✅ **v0.8.3** — **gates de validación paralelizados (sharding lossless)**: el carril `coherence` del Release Gate se shardea **por HU** (≥ 3 HUs, `args.hus[]`) — de un solo agente opus O(HUs) a un shard por HU en paralelo con consolidación **fail-closed** y cobertura completa; nueva plantilla **`dor-fanout.workflow.js`** para los chequeos per-HU del DoR (frontmatter/G-W-T/INVEST en paralelo; el nivel épica sigue en `dor-dod-gatekeeper`, único emisor del veredicto). `security`/`smell`/`ux`/`stack_arch` quedan monolíticos a propósito (riesgo cross-cutting); `integration` sigue secuencial (regla dura §5).
|
|
188
|
+
- ✅ **v0.8.2** — **capa de arquitectura (ADD)** entre discovery y construcción: comando **`/build:architect`** + skill **`setup-architecture`** que aplica el método **Attribute-Driven Design** (Len Bass) leyendo `docs/` (solo lectura) y produciendo `docs/adr/` (drivers/ASRs → tácticas → estilos → vistas → ATAM-lite → stack), con **mínimo HITL** (autónomo, una revisión final; propone, no publica). Dos agentes nuevos (`asr-extractor`, `architecture-evaluator`), plantillas ADD embebidas en la skill (`skills/setup-architecture/assets/`) y **`asset-types.json`** (forward-compat runtime v0.9: `arch.drivers`/`arch.adr`/`arch.backlog`). Cierre del lazo: los ADRs se vuelven criterios — el **DoR** exige cobertura para el cimiento fundacional (opt-in, retrocompatible) y el gate **`stack_arch`** audita conformidad contra `docs/adr/`. Total: **14 agentes**, **13 hooks**, **8 comandos `/build:*`**, **15 skills**.
|
|
187
189
|
|
|
188
190
|
## Licencia
|
|
189
191
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.8.
|
|
1
|
+
0.8.4
|
|
@@ -19,6 +19,14 @@ las fases de código.)
|
|
|
19
19
|
La unidad es la **épica**. Identifica `EP-XXX` en `docs/03-backlog/epicas.md` y el conjunto de HU
|
|
20
20
|
que la componen (las que tienen `epica: EP-XXX` en `docs/04-historias/`). Pasa SOLO si **todas** se
|
|
21
21
|
cumplen; lista cada una con ✓/✗:
|
|
22
|
+
|
|
23
|
+
> **Conducción opcional — fan-out per-HU (épicas con ≥ 3 HUs).** Los criterios 3-5 (frontmatter, AC
|
|
24
|
+
> G/W/T, INVEST) son por-HU e independientes: puedes conducirlos con la plantilla
|
|
25
|
+
> `building-a-slice/workflows/dor-fanout.workflow.js` (read-only; `args: {epica, hus[]}`) para validarlos
|
|
26
|
+
> **en paralelo** en vez de HU por HU. La plantilla devuelve el diagnóstico per-HU con consolidación
|
|
27
|
+
> fail-closed (HU fallida o shard ausente = false); tú **combinas** ese diagnóstico con los criterios de
|
|
28
|
+
> nivel épica (1-2 y 6-10, que valides tú en sesión) y sigues siendo el **único** que emite el veredicto
|
|
29
|
+
> DoR y abre `active_slice`. Con < 3 HUs la plantilla se auto-salta: valida secuencial como siempre.
|
|
22
30
|
1. La épica existe en `docs/03-backlog/epicas.md` con su trazabilidad a objetivos del PRD.
|
|
23
31
|
2. Tiene **al menos una HU** asociada y todas se enumeran en `hus[]`.
|
|
24
32
|
3. **Cada HU** de la épica: frontmatter YAML completo (`id, titulo, epica, prioridad, complejidad,
|
|
@@ -33,6 +41,15 @@ cumplen; lista cada una con ✓/✗:
|
|
|
33
41
|
como épica(s) `layer: foundational` **archivada(s)** en `history[]`. Si arrastra cimiento no construido,
|
|
34
42
|
**NO abras el slice**: instruye extraerlo a una épica fundacional previa y construirla primero. Aquí la
|
|
35
43
|
cláusula "explícitamente no bloquean" del criterio 6 **NO aplica**: el cimiento bloquea siempre.
|
|
44
|
+
7-bis. **Caparazón construido (solo greenfield — PROACTIVO)**: lee `project_kind` y `foundation`
|
|
45
|
+
en `build-state.json`. Si `project_kind === "greenfield"` y `foundation.required === true` y
|
|
46
|
+
la épica evaluada es `layer: business`: exige `foundation.completed_at` estampado (la épica
|
|
47
|
+
caparazón, `foundation.epic`, archivada y con su checklist evidenciada). Si no está estampado,
|
|
48
|
+
**NO abras el slice**: instruye construir primero la épica caparazón (`foundation.epic`; si es
|
|
49
|
+
`null`, correr `/build:onboard` Fase 2c o crearla en discovery). Las épicas `layer: foundational`
|
|
50
|
+
no se bloquean por este criterio. Brownfield o `foundation.required !== true` → **N/A** (no
|
|
51
|
+
bloquea, no preguntes). A diferencia del criterio 7, aquí NO evalúas arrastre: en greenfield
|
|
52
|
+
el bloqueo es incondicional hasta que el cimiento exista archivado.
|
|
36
53
|
8. **Tamaño acotado**: si la épica supera el umbral del gate de descomposición —heurística por defecto
|
|
37
54
|
**> 3 HU** ó **≥ 3 capas tocadas** (configurable por proyecto)— **no la abras como slice único**:
|
|
38
55
|
instruye descomponerla en `sub_slices[]` construidos de a uno (`journey_smoke` verde entre cada uno).
|
|
@@ -72,6 +89,15 @@ Pasa SOLO si **todos** estos gates del **inner loop** están en `true` (o `null`
|
|
|
72
89
|
6-bis. **Sub-slices completos**: si `active_slice.sub_slices[]` no está vacío, **todos** deben estar en
|
|
73
90
|
`status: done` (cada uno con su `journey_smoke` verde). Una épica descompuesta no cierra `dod` con
|
|
74
91
|
sub-slices pendientes (sería cierre prematuro de alcance).
|
|
92
|
+
6-ter. **Checklist del caparazón evidenciada (solo la épica caparazón)**: si la épica que cierra
|
|
93
|
+
es `foundation.epic`, **todos** los ítems de `foundation.checklist[]` con `applies: true`
|
|
94
|
+
deben tener `evidence` no vacía de **ejecución real** (test corrido, comando, screenshot MCP
|
|
95
|
+
— nunca inspección). Si falta evidencia, `dod` NO cierra: enumera los ítems pendientes. Al
|
|
96
|
+
cerrar `dod` y archivarse la épica, propone estampar `foundation.completed_at` (ISO-8601 UTC)
|
|
97
|
+
— con eso las épicas `layer: business` quedan desbloqueadas del criterio 7-bis. Para
|
|
98
|
+
cualquier otra épica este punto es N/A. La `evidence` de cada ítem la escribe el
|
|
99
|
+
`build-orchestrator` durante la construcción (misma disciplina que `wiring_checklist[]`: prueba
|
|
100
|
+
real ejecutada); tú solo la verificas y, al cerrar, estampas `foundation.completed_at`.
|
|
75
101
|
7. **`wiring_verified`** — `true`. Prerequisito **duro** de `dod`: lo cierra el `wiring-adversarial-verifier`
|
|
76
102
|
(subagente **independiente**, contexto virgen) tras intentar refutar el slice (stubs, rutas sin cablear,
|
|
77
103
|
AC sin test, items de `wiring_checklist[]` aún `failing`) y no hallar huecos. **Tu DoD declarativo es un
|
|
@@ -46,6 +46,7 @@ Voy a leer tu PRD/openspec para proponer valores y confirmar contigo:
|
|
|
46
46
|
6. Decisiones de alto impacto que exigen explicabilidad en UX
|
|
47
47
|
7. Fuente de diseño / referencia visual (prototipo/export) y pantallas — o "N/A" si no hay UI
|
|
48
48
|
8. Capa de cada épica del backlog: **fundacional** (cimiento) vs **negocio**
|
|
49
|
+
9. (Solo proyecto nuevo) Contrato del caparazón: checklist de la épica fundacional de app shell
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
---
|
|
@@ -72,6 +73,15 @@ Voy a leer tu PRD/openspec para proponer valores y confirmar contigo:
|
|
|
72
73
|
|
|
73
74
|
## Fase 2b: Clasificar la capa de las épicas (cimiento vs negocio)
|
|
74
75
|
|
|
76
|
+
**Primero, resuelve `project_kind`** (lee `.claude/state/build-state.json`):
|
|
77
|
+
- `"brownfield"` → el mecanismo del caparazón es **N/A**: no preguntes ni exijas nada de la
|
|
78
|
+
Fase 2c; clasifica capas como siempre y sigue.
|
|
79
|
+
- `null` / ausente (detección ambigua del CLI) → pregunta **una sola vez** vía AskUserQuestion:
|
|
80
|
+
*"¿Este proyecto es una app nueva (greenfield: el caparazón — menús, layout, homepage, login,
|
|
81
|
+
redirecciones — aún no existe) o ya construida (brownfield)?"*. Escribe `project_kind` y
|
|
82
|
+
`project_kind_source: "human"` en el estado (valida contra el schema tras escribir).
|
|
83
|
+
- `"greenfield"` → tras clasificar capas (abajo), continúa a la **Fase 2c**.
|
|
84
|
+
|
|
75
85
|
El factor que más reduce el consumo de contexto por slice es que el **cimiento** ya esté construido y
|
|
76
86
|
abstraído antes de que el loop tome historias de negocio. Para habilitar el gate de DoR "Cimiento
|
|
77
87
|
construido":
|
|
@@ -87,6 +97,38 @@ No inventes la clasificación: derívala del PRD/Story Map y confírmala con el
|
|
|
87
97
|
|
|
88
98
|
---
|
|
89
99
|
|
|
100
|
+
## Fase 2c: (Solo greenfield) Contrato del caparazón y su épica
|
|
101
|
+
|
|
102
|
+
Contrato completo en `.claude/skills/building-a-slice/references/foundation-contract.md`.
|
|
103
|
+
Si `project_kind !== "greenfield"`, salta esta fase (N/A total).
|
|
104
|
+
|
|
105
|
+
1. **Podar la checklist** vía AskUserQuestion (multiSelect) partiendo de los 5 ítems canónicos
|
|
106
|
+
(`navegacion-menus`, `layout-panel-central`, `homepage`, `login-authn`,
|
|
107
|
+
`redirecciones-guards`) según el tipo de app (una API sin UI poda los de UI y conserva
|
|
108
|
+
`login-authn`). Ítem podado = `applies: false` (se conserva como decisión, no se borra).
|
|
109
|
+
El usuario puede añadir ítems propios (id kebab-case). Si el humano poda **todos** los ítems
|
|
110
|
+
(ningún `applies: true`), no hay caparazón exigible: escribe `foundation.required: false` y el
|
|
111
|
+
mecanismo queda N/A (el DoR no bloqueará por caparazón).
|
|
112
|
+
2. **Identificar la épica caparazón** en `docs/03-backlog/epicas.md`: una épica
|
|
113
|
+
`layer: foundational` cuyo alcance cubra los ítems `applies: true`.
|
|
114
|
+
- **Existe** → propónla al usuario y fija `foundation.epic`.
|
|
115
|
+
- **No existe** → **borrador híbrido**: redacta la épica caparazón (título, objetivo, una HU
|
|
116
|
+
por ítem `applies: true` con AC en Given/When/Then) y preséntala vía AskUserQuestion.
|
|
117
|
+
- **Aprueba** → escríbela en `docs/03-backlog/epicas.md` con frontmatter
|
|
118
|
+
`layer: foundational` y `origin: harness-draft`, y fija `foundation.epic`.
|
|
119
|
+
**Este es el ÚNICO caso en que el arnés escribe una épica** (carve-out de METODOLOGIA
|
|
120
|
+
§9.2: solo la épica caparazón, solo con aprobación explícita).
|
|
121
|
+
- **Rechaza** → **STOP** de la fase: deja `foundation.epic: null`, instruye crearla en
|
|
122
|
+
discovery (`/trycore:*`) con la checklist como alcance. El gate del DoR bloqueará las
|
|
123
|
+
épicas de negocio igual hasta que exista y se archive.
|
|
124
|
+
3. **Persistir en el estado**: escribe `foundation.required: true`, `foundation.checklist[]`
|
|
125
|
+
(todos los ítems con su `applies`, `evidence: ""`), `foundation.epic` (o `null`). Escribe
|
|
126
|
+
**solo** los campos del schema (`foundation` es `additionalProperties: false`) y **valida
|
|
127
|
+
contra `build-state.schema.json` tras escribir** (aborta si no valida). Una transición = una
|
|
128
|
+
escritura.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
90
132
|
## Fase 3: Resolver el bloque de CLAUDE.md
|
|
91
133
|
|
|
92
134
|
Lee `CLAUDE.md`. Encuentra el bloque entre `<!-- BEGIN trycore-build-harness` y `<!-- END trycore-build-harness -->`.
|
|
@@ -162,6 +204,7 @@ PII/datos: <...>
|
|
|
162
204
|
Secretos: <...>
|
|
163
205
|
Decisiones clave: <...>
|
|
164
206
|
Fuente diseño: <...>
|
|
207
|
+
Caparazón: <N/A (brownfield) | EP-XXX con N ítems aplicables | pendiente en discovery>
|
|
165
208
|
|
|
166
209
|
CLAUDE.md actualizado. Auto-memory escrita. Los agentes de calidad ya leen tu dominio.
|
|
167
210
|
|
package/commands/build/slice.md
CHANGED
|
@@ -59,6 +59,11 @@ Antes de escribir código de slice, verifica los gates de proyecto:
|
|
|
59
59
|
- `scaffold.confirmed` debe ser `true`. Si es `false` → **STOP**: delega en la **Fase 0** de `building-a-slice`
|
|
60
60
|
(pregunta explícita; el arnés **no genera** el scaffold). No abras el slice.
|
|
61
61
|
- Si el proyecto tiene UI, `design_source.confirmed` debe ser `true` (Fase 0-bis). Si no → **STOP** igual.
|
|
62
|
+
- **Caparazón (solo greenfield)**: si `project_kind === "greenfield"` y `foundation.required === true`
|
|
63
|
+
y la épica objetivo es `layer: business` mientras `foundation.completed_at` no esté estampado (la
|
|
64
|
+
caparazón `foundation.epic` archivada con evidencia) → **STOP**: solo la épica caparazón
|
|
65
|
+
(`foundation.epic`) u otra fundacional puede abrir. Lo valida en detalle el `dor-dod-gatekeeper`
|
|
66
|
+
(criterio 7-bis). Brownfield → N/A, no preguntes.
|
|
62
67
|
|
|
63
68
|
El hook `scaffold-guard.sh` respalda esto en tiempo real.
|
|
64
69
|
|
package/commands/build/work.md
CHANGED
|
@@ -28,6 +28,15 @@ Si `NOT_INSTALLED` → ejecuta `trycore-build init` y vuelve.
|
|
|
28
28
|
|
|
29
29
|
Aplica las reglas en orden:
|
|
30
30
|
|
|
31
|
+
0. **¿Greenfield sin caparazón construido?** — si `build-state.json` tiene
|
|
32
|
+
`project_kind: "greenfield"`, `foundation.required: true` y `foundation.completed_at` sin
|
|
33
|
+
estampar → **enruta directo a construir la épica caparazón**
|
|
34
|
+
(`foundation.epic`) por el carril **`building-a-slice`**. Si `foundation.epic` es `null`,
|
|
35
|
+
enruta a `/build:onboard` (Fase 2c) o a discovery para definirla. Ninguna épica de negocio
|
|
36
|
+
pasa por delante. Brownfield o `foundation.required` ausente/false → esta regla es N/A.
|
|
37
|
+
Esta regla gatea **aperturas de slice** (épicas): el carril de **mantenimiento**
|
|
38
|
+
(`building-a-micro-change`, regla 1) no se bloquea — un typo/copy/config fix sigue su carril
|
|
39
|
+
normal aun sin caparazón construida.
|
|
31
40
|
1. **¿Mantenimiento sin capacidad nueva?** — typo, ajuste de copy/config/docs, bump de dependencia **ya
|
|
32
41
|
permitida**, o fix de **pocas líneas** sin nueva capacidad **Y** sin ninguno de los límites duros del
|
|
33
42
|
paso 3 → carril **`building-a-micro-change`** (`fix/*`|`chore/*` → PR, **sin** abrir `active_slice`).
|
package/dist/commands/doctor.js
CHANGED
|
@@ -66,6 +66,14 @@ export async function doctor(opts) {
|
|
|
66
66
|
const st = JSON.parse(fs.readFileSync(t.stateFile, 'utf8'));
|
|
67
67
|
const confirmed = st?.scaffold?.confirmed === true;
|
|
68
68
|
console.log(`Scaffold (Paso 1): ${confirmed ? '✓ confirmado' : '✗ pendiente — confírmalo en building-a-slice (Fase 0) antes de fases de código'}`);
|
|
69
|
+
const pk = st?.project_kind ?? null;
|
|
70
|
+
const fnd = st?.foundation ?? null;
|
|
71
|
+
if (pk === 'greenfield' && fnd?.required === true && !fnd?.completed_at) {
|
|
72
|
+
console.log(`Caparazón (1-bis): ✗ pendiente — ${fnd?.epic ? `construye ${fnd.epic} antes de épicas de negocio` : 'define la épica caparazón en /build:onboard (Fase 2c)'}`);
|
|
73
|
+
}
|
|
74
|
+
else if (pk === 'greenfield' && fnd?.completed_at) {
|
|
75
|
+
console.log(`Caparazón (1-bis): ✓ construido (${fnd.epic})`);
|
|
76
|
+
}
|
|
69
77
|
const hist = Array.isArray(st?.history) ? st.history : [];
|
|
70
78
|
const pend = hist.filter((h) => h?.reflected !== true).length;
|
|
71
79
|
if (pend > 0) {
|
package/dist/commands/init.js
CHANGED
|
@@ -9,7 +9,7 @@ import path from 'node:path';
|
|
|
9
9
|
import { ASSETS, readPackageVersion, targetPaths } from '../lib/paths.js';
|
|
10
10
|
import { linkChildren, countChildren, chmodExec } from '../lib/install-engine.js';
|
|
11
11
|
import { upsertMarkedBlock } from '../lib/markers.js';
|
|
12
|
-
import { seedState, seedConfig, seedBuildConfig } from '../lib/state-seed.js';
|
|
12
|
+
import { seedState, seedConfig, seedBuildConfig, stampProjectKind } from '../lib/state-seed.js';
|
|
13
13
|
import { mergeHarnessSettings } from '../lib/settings-merge.js';
|
|
14
14
|
import { captureStack, renderAllowlist } from '../lib/stack-prompt.js';
|
|
15
15
|
import { checkDeps } from './doctor.js';
|
|
@@ -57,6 +57,7 @@ export async function init(opts) {
|
|
|
57
57
|
}
|
|
58
58
|
// 2) Estado (schema/README versionados; build-state.json vacío solo si falta) [C1]
|
|
59
59
|
const { stateSeeded } = seedState(targetDir);
|
|
60
|
+
const projectKind = stampProjectKind(targetDir);
|
|
60
61
|
// 3) stack-allowlist.json del consumidor (artefacto del consumidor)
|
|
61
62
|
let allowlist = null;
|
|
62
63
|
if (!fs.existsSync(t.configFile) && mode === 'init') {
|
|
@@ -87,6 +88,7 @@ export async function init(opts) {
|
|
|
87
88
|
console.log(` Estado: ${stateSeeded ? 'build-state.json sembrado (vacío)' : 'build-state.json preservado'}`);
|
|
88
89
|
console.log(` Config: ${configSeeded ? 'stack-allowlist.json sembrado' : 'stack-allowlist.json preservado'}`);
|
|
89
90
|
console.log(` Config: ${buildConfigSeeded ? 'build-config.json sembrado' : 'build-config.json preservado'}`);
|
|
91
|
+
console.log(` Proyecto: ${projectKind === 'brownfield' ? 'brownfield (épica caparazón N/A)' : projectKind === 'greenfield' ? 'greenfield (épica caparazón requerida — se define en /build:onboard)' : 'indeterminado (se confirma en /build:onboard)'}`);
|
|
90
92
|
console.log('');
|
|
91
93
|
console.log('Siguiente paso — abre Claude Code y ejecuta:');
|
|
92
94
|
console.log(' /build:onboard # parametriza el dominio (PII, capa IA, capa determinista) y escribe memoria');
|
package/dist/commands/status.js
CHANGED
|
@@ -40,6 +40,25 @@ export async function status(opts) {
|
|
|
40
40
|
console.log('Estado del arnés:');
|
|
41
41
|
console.log(` Fase: ${st.harness_phase ?? '?'}`);
|
|
42
42
|
console.log(` Scaffold: ${st.scaffold?.confirmed === true ? '✓ confirmado (Paso 1)' : '✗ pendiente (Paso 1)'}`);
|
|
43
|
+
const pk = st.project_kind ?? null;
|
|
44
|
+
const fnd = st.foundation ?? null;
|
|
45
|
+
let capLine;
|
|
46
|
+
if (pk === 'brownfield') {
|
|
47
|
+
capLine = 'N/A (brownfield)';
|
|
48
|
+
}
|
|
49
|
+
else if (pk === 'greenfield' && fnd?.required !== true) {
|
|
50
|
+
capLine = '✗ contrato sin definir — /build:onboard (Fase 2c)';
|
|
51
|
+
}
|
|
52
|
+
else if (fnd?.required === true && fnd?.completed_at) {
|
|
53
|
+
capLine = `✓ construido${fnd.epic ? ` (${fnd.epic})` : ''}`;
|
|
54
|
+
}
|
|
55
|
+
else if (fnd?.required === true) {
|
|
56
|
+
capLine = `✗ pendiente${fnd?.epic ? ` (${fnd.epic})` : ' — define la épica en /build:onboard'}`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
capLine = 'N/A';
|
|
60
|
+
}
|
|
61
|
+
console.log(` Caparazón: ${capLine}`);
|
|
43
62
|
const slice = st.active_slice;
|
|
44
63
|
if (slice) {
|
|
45
64
|
const openGates = Object.entries(slice.gates ?? {})
|
package/dist/lib/state-seed.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// y NUNCA se sobreescribe (es propiedad del equipo).
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
7
8
|
import { ASSETS, targetPaths } from './paths.js';
|
|
8
9
|
import { ensureDir } from './install-engine.js';
|
|
9
10
|
/** Estado inicial vacío (fallback si falta el template del paquete). */
|
|
@@ -12,6 +13,9 @@ const EMPTY_STATE = {
|
|
|
12
13
|
harness_phase: 'authoring',
|
|
13
14
|
scaffold: { confirmed: false, confirmed_by: null, confirmed_at: null, notes: '' },
|
|
14
15
|
design_source: { applies: false, confirmed: false, confirmed_by: null, confirmed_at: null, source: '', notes: '' },
|
|
16
|
+
project_kind: null,
|
|
17
|
+
project_kind_source: null,
|
|
18
|
+
foundation: { required: false, epic: null, completed_at: null, checklist: [] },
|
|
15
19
|
active_slice: null,
|
|
16
20
|
history: [],
|
|
17
21
|
releases: [],
|
|
@@ -56,7 +60,6 @@ export function seedConfig(targetDir, allowlist) {
|
|
|
56
60
|
else {
|
|
57
61
|
return { configSeeded: false };
|
|
58
62
|
}
|
|
59
|
-
void path;
|
|
60
63
|
return { configSeeded: true };
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
@@ -73,3 +76,94 @@ export function seedBuildConfig(targetDir) {
|
|
|
73
76
|
fs.copyFileSync(ASSETS.buildConfigTemplate, t.buildConfigFile);
|
|
74
77
|
return { buildConfigSeeded: true };
|
|
75
78
|
}
|
|
79
|
+
const MANIFESTS = [
|
|
80
|
+
'package.json', 'pom.xml', 'build.gradle', 'build.gradle.kts', 'go.mod',
|
|
81
|
+
'Cargo.toml', 'composer.json', 'pyproject.toml', 'requirements.txt', 'Gemfile',
|
|
82
|
+
];
|
|
83
|
+
const CODE_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.java', '.kt', '.go', '.rs', '.php', '.py', '.cs', '.rb', '.swift', '.vue', '.svelte'];
|
|
84
|
+
const CODE_DIRS = ['src', 'app', 'lib', 'apps', 'packages'];
|
|
85
|
+
function dirHasCode(dir, depth = 0) {
|
|
86
|
+
if (depth > 3 || !fs.existsSync(dir))
|
|
87
|
+
return false;
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
for (const e of entries) {
|
|
96
|
+
if (e.isFile() && CODE_EXTS.some((x) => e.name.endsWith(x)))
|
|
97
|
+
return true;
|
|
98
|
+
if (e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.')) {
|
|
99
|
+
if (dirHasCode(path.join(dir, e.name), depth + 1))
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
/** ¿El repo git del consumidor tiene commits que tocan código? Señal del spec para
|
|
106
|
+
* brownfield SEGURO. Acotado: mira los últimos 30 commits. Sin repo git → false. */
|
|
107
|
+
function hasGitCodeHistory(dir) {
|
|
108
|
+
const inRepo = spawnSync('git', ['-C', dir, 'rev-parse', '--git-dir'], { stdio: 'ignore' }).status === 0;
|
|
109
|
+
if (!inRepo)
|
|
110
|
+
return false;
|
|
111
|
+
const r = spawnSync('git', ['-C', dir, 'log', '-n', '30', '--name-only', '--pretty=format:'], { encoding: 'utf8' });
|
|
112
|
+
if (r.status !== 0 || !r.stdout)
|
|
113
|
+
return false;
|
|
114
|
+
return r.stdout.split('\n').some((f) => CODE_EXTS.some((x) => f.trim().endsWith(x)));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Detección greenfield/brownfield a partir de tres señales: manifiesto de paquete,
|
|
118
|
+
* presencia de código (raíz o directorios convencionales) e historial git que toca
|
|
119
|
+
* código. Brownfield CIERTO requiere las tres señales; greenfield CIERTO requiere la
|
|
120
|
+
* ausencia total de manifiesto y código. Señales mixtas (p.ej. scaffold recién creado
|
|
121
|
+
* sin historial git) devuelven null — ambiguo, lo resuelve `/build:onboard`.
|
|
122
|
+
*/
|
|
123
|
+
export function detectProjectKind(targetDir) {
|
|
124
|
+
let rootEntries = [];
|
|
125
|
+
try {
|
|
126
|
+
rootEntries = fs.readdirSync(targetDir);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const hasManifest = MANIFESTS.some((m) => fs.existsSync(path.join(targetDir, m))) ||
|
|
132
|
+
rootEntries.some((n) => n.endsWith('.csproj') || n.endsWith('.sln'));
|
|
133
|
+
const rootCode = rootEntries.some((n) => CODE_EXTS.some((x) => n.endsWith(x)));
|
|
134
|
+
const hasCode = rootCode || CODE_DIRS.some((d) => dirHasCode(path.join(targetDir, d)));
|
|
135
|
+
if (hasManifest && hasCode && hasGitCodeHistory(targetDir))
|
|
136
|
+
return 'brownfield';
|
|
137
|
+
if (!hasManifest && !hasCode)
|
|
138
|
+
return 'greenfield';
|
|
139
|
+
return null; // señales mixtas (p.ej. scaffold recién creado sin historial) → ambiguo, lo resuelve onboard
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Estampa project_kind en build-state.json SOLO si está ausente o null (migración
|
|
143
|
+
* aditiva; nunca pisa una decisión previa auto o humana) y solo con detección no-null.
|
|
144
|
+
*/
|
|
145
|
+
export function stampProjectKind(targetDir) {
|
|
146
|
+
const t = targetPaths(targetDir);
|
|
147
|
+
if (!fs.existsSync(t.stateFile))
|
|
148
|
+
return null;
|
|
149
|
+
const detected = detectProjectKind(targetDir);
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = JSON.parse(fs.readFileSync(t.stateFile, 'utf8'));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return detected;
|
|
156
|
+
}
|
|
157
|
+
// JSON.parse puede devolver con éxito algo que no es un objeto plano (null, string,
|
|
158
|
+
// número, boolean, array) si el archivo está corrupto — degradar igual que un parse fallido.
|
|
159
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
160
|
+
return detected;
|
|
161
|
+
}
|
|
162
|
+
const st = parsed;
|
|
163
|
+
if (detected !== null && (st.project_kind === undefined || st.project_kind === null)) {
|
|
164
|
+
st.project_kind = detected;
|
|
165
|
+
st.project_kind_source = 'auto';
|
|
166
|
+
fs.writeFileSync(t.stateFile, JSON.stringify(st, null, 2) + '\n', 'utf8');
|
|
167
|
+
}
|
|
168
|
+
return st.project_kind ?? detected;
|
|
169
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trycore/spec-build-harness",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Arnés agéntico de construcción de Trycore para Claude Code: pipeline de dos loops (slice por épica + release gate) con gates de calidad, estado compartido y OpenSpec. Compañero de @trycore/spec-product-flow. Agnóstico al proyecto.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -86,4 +86,92 @@ check "hooks/build/reconcile-build-state.py"
|
|
|
86
86
|
check "hooks/build/lib/state-io.sh"
|
|
87
87
|
check "scripts/lib/front-plan.py"
|
|
88
88
|
|
|
89
|
+
# project_kind: el TMP de init es un directorio vacío → greenfield detectado (auto)
|
|
90
|
+
if python3 -c "
|
|
91
|
+
import json,sys
|
|
92
|
+
d=json.load(open('$TMP/.claude/state/build-state.json'))
|
|
93
|
+
assert d.get('project_kind')=='greenfield', d.get('project_kind')
|
|
94
|
+
assert d.get('project_kind_source')=='auto', d.get('project_kind_source')
|
|
95
|
+
" 2>/dev/null; then
|
|
96
|
+
echo "OK install: project_kind=greenfield detectado (auto)"
|
|
97
|
+
else
|
|
98
|
+
echo "FAIL install: project_kind no detectado como greenfield/auto"
|
|
99
|
+
fail=1
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# 5) Brownfield seguro: manifiesto + código + historial git que toca código.
|
|
103
|
+
TMP_BF="$(mktemp -d)"
|
|
104
|
+
echo '{}' > "$TMP_BF/package.json"
|
|
105
|
+
mkdir -p "$TMP_BF/src"
|
|
106
|
+
echo 'export const x = 1;' > "$TMP_BF/src/index.ts"
|
|
107
|
+
(
|
|
108
|
+
cd "$TMP_BF" &&
|
|
109
|
+
git init -q &&
|
|
110
|
+
git add -A &&
|
|
111
|
+
git -c user.email=t@t -c user.name=t commit -qm x
|
|
112
|
+
) >/dev/null 2>&1
|
|
113
|
+
|
|
114
|
+
node "$ROOT/dist/cli.js" init "$TMP_BF" \
|
|
115
|
+
--copy \
|
|
116
|
+
--skip-doctor \
|
|
117
|
+
--yes \
|
|
118
|
+
--stack "" \
|
|
119
|
+
--pkg-manager npm \
|
|
120
|
+
--runtime ">=18.18" \
|
|
121
|
+
--prd-path "docs/01-prd/x.md#req" \
|
|
122
|
+
>"$TMP_BF/.init.log" 2>&1
|
|
123
|
+
rc=$?
|
|
124
|
+
if [ $rc -ne 0 ]; then
|
|
125
|
+
echo "FAIL install: \`trycore-build init\` (brownfield) salió con código $rc"
|
|
126
|
+
cat "$TMP_BF/.init.log"
|
|
127
|
+
fail=1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if python3 -c "
|
|
131
|
+
import json,sys
|
|
132
|
+
d=json.load(open('$TMP_BF/.claude/state/build-state.json'))
|
|
133
|
+
assert d.get('project_kind')=='brownfield', d.get('project_kind')
|
|
134
|
+
assert d.get('project_kind_source')=='auto', d.get('project_kind_source')
|
|
135
|
+
" 2>/dev/null; then
|
|
136
|
+
echo "OK install: project_kind=brownfield con historial git"
|
|
137
|
+
else
|
|
138
|
+
echo "FAIL install: project_kind no detectado como brownfield con historial git"
|
|
139
|
+
fail=1
|
|
140
|
+
fi
|
|
141
|
+
rm -rf "$TMP_BF"
|
|
142
|
+
|
|
143
|
+
# 6) Ambiguo: manifiesto + código pero SIN repo git (sin historial) → project_kind null.
|
|
144
|
+
TMP_AMB="$(mktemp -d)"
|
|
145
|
+
echo '{}' > "$TMP_AMB/package.json"
|
|
146
|
+
mkdir -p "$TMP_AMB/src"
|
|
147
|
+
echo 'export const x = 1;' > "$TMP_AMB/src/index.ts"
|
|
148
|
+
|
|
149
|
+
node "$ROOT/dist/cli.js" init "$TMP_AMB" \
|
|
150
|
+
--copy \
|
|
151
|
+
--skip-doctor \
|
|
152
|
+
--yes \
|
|
153
|
+
--stack "" \
|
|
154
|
+
--pkg-manager npm \
|
|
155
|
+
--runtime ">=18.18" \
|
|
156
|
+
--prd-path "docs/01-prd/x.md#req" \
|
|
157
|
+
>"$TMP_AMB/.init.log" 2>&1
|
|
158
|
+
rc=$?
|
|
159
|
+
if [ $rc -ne 0 ]; then
|
|
160
|
+
echo "FAIL install: \`trycore-build init\` (ambiguo) salió con código $rc"
|
|
161
|
+
cat "$TMP_AMB/.init.log"
|
|
162
|
+
fail=1
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
if python3 -c "
|
|
166
|
+
import json,sys
|
|
167
|
+
d=json.load(open('$TMP_AMB/.claude/state/build-state.json'))
|
|
168
|
+
assert d.get('project_kind') is None, d.get('project_kind')
|
|
169
|
+
" 2>/dev/null; then
|
|
170
|
+
echo "OK install: project_kind=null (ambiguo sin historial git)"
|
|
171
|
+
else
|
|
172
|
+
echo "FAIL install: project_kind no quedó null en caso ambiguo"
|
|
173
|
+
fail=1
|
|
174
|
+
fi
|
|
175
|
+
rm -rf "$TMP_AMB"
|
|
176
|
+
|
|
89
177
|
exit $fail
|
|
@@ -43,8 +43,29 @@ cat > "$TMP/bad-layer.json" <<'JSON'
|
|
|
43
43
|
"layer":"WRONG","gates":{"dor":true,"tdd":false,"dod":false},"updated_at":"2026-07-03T00:00:00Z","updated_by":"t"},
|
|
44
44
|
"history":[],"releases":[]}
|
|
45
45
|
JSON
|
|
46
|
+
cat > "$TMP/valid-foundation.json" <<'JSON'
|
|
47
|
+
{"version":"1.0","harness_phase":"authoring","scaffold":{"confirmed":false},
|
|
48
|
+
"project_kind":"greenfield","project_kind_source":"auto",
|
|
49
|
+
"foundation":{"required":true,"epic":"EP-001","completed_at":null,
|
|
50
|
+
"checklist":[{"item":"navegacion-menus","applies":true,"evidence":""},
|
|
51
|
+
{"item":"login-authn","applies":true,"evidence":"smoke login OK"}]},
|
|
52
|
+
"active_slice":null,"history":[],"releases":[]}
|
|
53
|
+
JSON
|
|
54
|
+
cat > "$TMP/bad-project-kind.json" <<'JSON'
|
|
55
|
+
{"version":"1.0","harness_phase":"authoring","scaffold":{"confirmed":false},
|
|
56
|
+
"project_kind":"unknown","active_slice":null,"history":[],"releases":[]}
|
|
57
|
+
JSON
|
|
58
|
+
cat > "$TMP/bad-foundation-item.json" <<'JSON'
|
|
59
|
+
{"version":"1.0","harness_phase":"authoring","scaffold":{"confirmed":false},
|
|
60
|
+
"foundation":{"required":true,"epic":null,"completed_at":null,
|
|
61
|
+
"checklist":[{"applies":true}]},
|
|
62
|
+
"active_slice":null,"history":[],"releases":[]}
|
|
63
|
+
JSON
|
|
46
64
|
check "estado válido con campos nuevos" pass "$TMP/valid.json"
|
|
47
65
|
check "layer inválido rechazado" fail "$TMP/bad-layer.json"
|
|
66
|
+
check "foundation + project_kind válidos" pass "$TMP/valid-foundation.json"
|
|
67
|
+
check "project_kind inválido rechazado" fail "$TMP/bad-project-kind.json"
|
|
68
|
+
check "checklist item sin 'item' rechazado" fail "$TMP/bad-foundation-item.json"
|
|
48
69
|
|
|
49
70
|
CFG="$ROOT/config/build-config.template.json"
|
|
50
71
|
if [ -f "$CFG" ] && python3 -c "import json,sys; d=json.load(open('$CFG'))['context']; assert d['warning_pct']==35 and d['critical_pct']==25 and d['auto_checkpoint'] is False" 2>/dev/null; then
|
|
@@ -10,6 +10,16 @@ la épica; lo de abajo aplica a la épica y a **cada HU** que cubre (`hus[]`). L
|
|
|
10
10
|
- [ ] **INVEST** por HU: pasa los 6 criterios (Independent, Negotiable, Valuable, Estimable, Small, Testable).
|
|
11
11
|
- [ ] **Dependencias resueltas**: las épicas/HU de las que depende están archivadas (`history[]`) o explícitamente no bloquean. **Excepción dura — cimiento:** si la dependencia es **infraestructura fundacional** (autenticación, acceso a datos, arquitectura base, design-system/componentes base), la cláusula "explícitamente no bloquean" **NO aplica**: debe estar **construida y archivada** antes (ver criterio "Cimiento construido").
|
|
12
12
|
- [ ] **Cimiento construido (épicas de negocio)**: si esta épica es `layer: business`, todo el cimiento que arrastra (auth, acceso a datos, arquitectura base, design-system/componentes base) ya existe como épica(s) `layer: foundational` **archivada(s)** en `history[]`. Si arrastra cimiento no construido → **STOP**: extráelo a una épica fundacional previa y constrúyela primero. Las épicas fundacionales se priorizan **antes** que las de negocio.
|
|
13
|
+
- [ ] **Caparazón construido (solo greenfield — gate PROACTIVO)**: si `project_kind === "greenfield"`
|
|
14
|
+
y `foundation.required === true` en `build-state.json`, ninguna épica `layer: business` entra a
|
|
15
|
+
construcción mientras la épica caparazón (`foundation.epic`) no esté **archivada con su checklist
|
|
16
|
+
evidenciada** (`foundation.completed_at` estampado). Las épicas `layer: foundational` (la
|
|
17
|
+
caparazón `foundation.epic` u otras fundacionales) sí pueden abrir. A diferencia de "Cimiento
|
|
18
|
+
construido" (reactivo: bloquea si *detecta* arrastre), este criterio bloquea **siempre** en
|
|
19
|
+
greenfield hasta que el cimiento exista archivado — no depende de detectar nada. Si
|
|
20
|
+
`foundation.epic` es `null` (no existe la épica caparazón aún) → **STOP**: se define en
|
|
21
|
+
`/build:onboard` Fase 2c o en discovery. Brownfield / `foundation.required: false` → **N/A** (no
|
|
22
|
+
bloquea). Contrato: `references/foundation-contract.md`.
|
|
13
23
|
- [ ] **Tamaño acotado (gate de descomposición)**: si la épica supera el umbral —heurística por defecto **> 3 HU** ó **≥ 3 capas tocadas** (configurable por proyecto)— **no entra como slice único**: se descompone en `sub_slices[]` verificables construidos de a uno, con `journey_smoke` verde entre cada uno. El umbral es proporcional (no cuota rígida): una épica de 1 capa y pocas HU entra directa.
|
|
14
24
|
- [ ] **Cabe en el stack** del PRD §7 (no requiere tecnología fuera de `stack-allowlist.json`).
|
|
15
25
|
- [ ] **Cobertura arquitectónica (ADR)** *(opt-in, retrocompatible)*: si el proyecto adoptó la capa de
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Contrato del caparazón (épica fundacional de app shell) — solo greenfield
|
|
2
|
+
|
|
3
|
+
El **caparazón** es el shell con contenido donde aterrizan las features: navegación, layout,
|
|
4
|
+
homepage, login y redirecciones. No es el *scaffold* (shell vacío que arranca — gate
|
|
5
|
+
`scaffold.confirmed`) ni el *walking skeleton* (primer journey de negocio más delgado): es la
|
|
6
|
+
infraestructura de UI/entrada fundacional que, si no existe primero, cada épica de negocio
|
|
7
|
+
improvisa a pedazos y acumula deuda estructural.
|
|
8
|
+
|
|
9
|
+
**Aplica si y solo si `project_kind === "greenfield"`.** En brownfield el mecanismo entero es
|
|
10
|
+
N/A y el arnés no pregunta ni exige nada (`foundation.required: false`).
|
|
11
|
+
|
|
12
|
+
## Checklist base (ids canónicos)
|
|
13
|
+
|
|
14
|
+
| id | Qué cubre | Poda típica |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `navegacion-menus` | Menús / estructura de navegación principal | API sin UI |
|
|
17
|
+
| `layout-panel-central` | Layout base + panel/área central de contenido | API sin UI |
|
|
18
|
+
| `homepage` | Página de inicio real (no placeholder del scaffold) | API sin UI |
|
|
19
|
+
| `login-authn` | Login + autenticación cableada end-to-end | app pública sin cuentas |
|
|
20
|
+
| `redirecciones-guards` | Redirecciones y guards de ruta (authed/anon, 404, deep-links) | API sin UI |
|
|
21
|
+
|
|
22
|
+
- La **poda** se decide una vez, en `/build:onboard` Fase 2c, con el humano (ítem podado =
|
|
23
|
+
`applies: false`; queda en la checklist como decisión trazable, no se borra).
|
|
24
|
+
- El proyecto puede **añadir** ítems propios (id kebab-case) si su caparazón exige más.
|
|
25
|
+
|
|
26
|
+
## Ciclo de vida
|
|
27
|
+
|
|
28
|
+
1. **Onboard (Fase 2c)** — se poda la checklist, se persiste en `foundation.checklist[]` y se
|
|
29
|
+
identifica (o redacta en borrador híbrido) la épica caparazón → `foundation.epic`.
|
|
30
|
+
2. **DoR (gate proactivo)** — mientras la épica caparazón no esté **archivada**, ninguna épica
|
|
31
|
+
`layer: business` abre slice (solo fundacionales). Ver `dor.md`.
|
|
32
|
+
3. **DoD de la épica caparazón** — cada ítem `applies: true` exige `evidence` de ejecución real
|
|
33
|
+
(test, comando, screenshot MCP), patrón `wiring_checklist`. Sin evidencia completa no se
|
|
34
|
+
archiva.
|
|
35
|
+
4. **Cierre** — al archivar con la checklist evidenciada se estampa `foundation.completed_at` y
|
|
36
|
+
las épicas de negocio quedan desbloqueadas.
|
|
37
|
+
|
|
38
|
+
## Reglas duras
|
|
39
|
+
|
|
40
|
+
- El arnés **propone** el borrador de la épica caparazón; **solo** lo escribe en
|
|
41
|
+
`docs/03-backlog/epicas.md` con aprobación humana explícita (carve-out acotado, ver
|
|
42
|
+
METODOLOGIA §9.2). Si el humano rechaza, STOP: la épica se crea en discovery (`/trycore:*`).
|
|
43
|
+
- La evidencia es de **ejecución**, nunca de inspección.
|
|
44
|
+
- Si contradice `METODOLOGIA.md`, gana la metodología.
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
## Reglas duras (todas las plantillas las cumplen)
|
|
8
8
|
1. **Solo para épicas grandes.** Los workflows del inner loop son **OPT-IN** y solo para épicas troceadas por
|
|
9
9
|
el gate de tamaño (`sub_slices[]` no vacío). **Nunca** en el camino caliente ≤ ~20 min de una épica
|
|
10
|
-
atómica: inflaría el inner loop barato.
|
|
10
|
+
atómica: inflaría el inner loop barato. *Excepción de guard:* `dor-fanout` corre **antes** de abrir el
|
|
11
|
+
slice (aún no existe `sub_slices[]`), así que su guard es por **nº de HUs** (≥ 3; bajo eso se auto-salta
|
|
12
|
+
y la validación queda secuencial en sesión).
|
|
11
13
|
2. **Read-only sobre el estado.** Ninguna plantilla escribe `build-state.json`. El único escritor de los
|
|
12
14
|
gates del slice sigue siendo `build-orchestrator` (y los agentes dueños de cada gate). Las plantillas
|
|
13
15
|
**devuelven un diagnóstico**; la sesión/orquestador aplica el mapeo respetando el protocolo: **una
|
|
@@ -23,3 +25,7 @@
|
|
|
23
25
|
gated por `sub_slices[]`. Contrato detallado en `../references/exploration-fanout.md`.
|
|
24
26
|
- **`wiring-verify.workflow.js`** — conducción adversarial del gate `wiring_verified` (envuelve, read-only, al
|
|
25
27
|
agente `wiring-adversarial-verifier`); devuelve el veredicto, no escribe el gate.
|
|
28
|
+
- **`dor-fanout.workflow.js`** — fan-out **per-HU** de los chequeos lentos del DoR (frontmatter, AC G/W/T
|
|
29
|
+
proporcional, INVEST) con consolidación **fail-closed** y cobertura completa. Los criterios de **nivel
|
|
30
|
+
épica** (dependencias, cimiento, tamaño, stack, diseño, ADR) NO van aquí: los valida `dor-dod-gatekeeper`
|
|
31
|
+
en sesión, que combina ambos y es el único que emite el veredicto DoR. Guard: ≥ 3 HUs.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// PLANTILLA — referencia, NO un script a correr verbatim.
|
|
3
|
+
// dor-fanout.workflow.js — Fan-out de los chequeos PER-HU del DoR (inner loop).
|
|
4
|
+
//
|
|
5
|
+
// QUÉ PARALELIZA (y qué NO): SOLO los criterios del DoR que son por-HU e independientes
|
|
6
|
+
// entre sí — frontmatter completo + estado:lista, AC en Given/When/Then proporcional a
|
|
7
|
+
// `complejidad`, e INVEST. Los criterios de NIVEL ÉPICA (trazabilidad de la épica,
|
|
8
|
+
// dependencias en history[], cimiento construido, gate de tamaño, stack/allowlist,
|
|
9
|
+
// fuente de diseño, cobertura de ADR) NO van aquí: los valida dor-dod-gatekeeper en
|
|
10
|
+
// sesión — necesitan estado y son baratos. Este workflow ataca lo LENTO: O(HUs) → O(1).
|
|
11
|
+
//
|
|
12
|
+
// READ-ONLY sobre el estado: NO escribe gates.dor ni abre active_slice. El ÚNICO que
|
|
13
|
+
// abre el slice sigue siendo dor-dod-gatekeeper, que COMBINA este diagnóstico per-HU
|
|
14
|
+
// con sus criterios de épica y emite el veredicto DoR completo.
|
|
15
|
+
//
|
|
16
|
+
// CUÁNDO: SOLO con ≥ 3 HUs (bajo eso el fan-out no paga su overhead y el inner loop
|
|
17
|
+
// debe seguir barato). Consolidación FAIL-CLOSED con cobertura COMPLETA: una HU
|
|
18
|
+
// fallida o un shard ausente = DoR per-HU false, nunca muestreo silencioso.
|
|
19
|
+
//
|
|
20
|
+
// PLANTILLA AGNÓSTICA: sin vocabulario de dominio/cliente (scripts/check-agnostic.sh
|
|
21
|
+
// escanea *.js). Si METODOLOGIA.md (§3.1) contradice algo aquí, gana la metodología.
|
|
22
|
+
//
|
|
23
|
+
// RUNTIME: corre en el runtime de Workflow de Claude Code, que provee los globals
|
|
24
|
+
// agent()/parallel()/pipeline()/phase()/log()/args y envuelve el cuerpo en un contexto
|
|
25
|
+
// async (por eso usa `await` y `return` a nivel superior). NO es un módulo node standalone.
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
export const meta = {
|
|
29
|
+
name: 'dor-fanout',
|
|
30
|
+
description: 'Fan-out per-HU de los chequeos del DoR (frontmatter, AC G/W/T proporcional, INVEST) con consolidación fail-closed. Read-only; dor-dod-gatekeeper combina el diagnóstico y emite el veredicto.',
|
|
31
|
+
phases: [{ title: 'HUs', detail: 'un validador solo-lectura por HU, en paralelo' }],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const HU_CHECK_SCHEMA = {
|
|
35
|
+
type: 'object', additionalProperties: false,
|
|
36
|
+
required: ['hu', 'pass', 'faltantes'],
|
|
37
|
+
properties: {
|
|
38
|
+
hu: { type: 'string' },
|
|
39
|
+
pass: { type: 'boolean', description: 'true SOLO si los 3 bloques (frontmatter, AC, INVEST) están completos y verificados' },
|
|
40
|
+
faltantes: { type: 'array', items: { type: 'string' }, description: 'cada criterio incumplido, con detalle accionable' },
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Insumos por `args` (dor-dod-gatekeeper los pasa READ-ONLY; el workflow no decide alcance):
|
|
45
|
+
// args.epica : string — ID de la épica (solo para el reporte).
|
|
46
|
+
// args.hus : string[] — IDs de las HU en alcance (hus[] del slice candidato).
|
|
47
|
+
// args.husDir : string — directorio de las HU (default 'docs/04-historias').
|
|
48
|
+
const epica = (args && args.epica) || '<EP-XXX>'
|
|
49
|
+
const hus = (args && Array.isArray(args.hus)) ? args.hus.filter(Boolean) : []
|
|
50
|
+
const husDir = (args && args.husDir) || 'docs/04-historias'
|
|
51
|
+
|
|
52
|
+
// Guard de tamaño: bajo 3 HUs el fan-out no paga su overhead → el gatekeeper valida
|
|
53
|
+
// secuencial en sesión (comportamiento previo). Espeja el guard de explore-fanout.
|
|
54
|
+
if (hus.length < 3) {
|
|
55
|
+
log(`Épica ${epica} con ${hus.length} HU(s): fan-out no amortiza — validación secuencial en sesión.`)
|
|
56
|
+
return { skipped: true, reason: 'pocas-hus', hus_count: hus.length }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
phase('HUs')
|
|
60
|
+
log(`DoR per-HU: fan-out de ${hus.length} HUs de ${epica} (cobertura completa, sin muestreo)`)
|
|
61
|
+
const checks = await parallel(hus.map((hu) => async () => {
|
|
62
|
+
try {
|
|
63
|
+
const v = await agent(
|
|
64
|
+
`Eres un validador SOLO-LECTURA del DoR para UNA SOLA historia de usuario: ${hu} (épica ${epica}).
|
|
65
|
+
Lee ${husDir}/${hu}.md y verifica EXACTAMENTE estos 3 bloques (nada de nivel épica):
|
|
66
|
+
1. FRONTMATTER completo: id, titulo, epica, prioridad, complejidad, estado — y estado: lista.
|
|
67
|
+
2. AC en Given/When/Then PROPORCIONAL a complejidad: trivial/baja → 1-2 (happy + error/edge crítico si
|
|
68
|
+
existe); media → 3 (happy + error + edge); alta → 3-5 (cobertura completa). Regla dura: toda rama de
|
|
69
|
+
error/edge que exista DEBE tener su escenario; NO exijas cuota fija a una HU trivial.
|
|
70
|
+
3. INVEST: evalúa tú mismo los 6 criterios (Independent, Negotiable, Valuable, Estimable, Small, Testable).
|
|
71
|
+
pass:true SOLO si los 3 bloques cumplen. Cada incumplimiento va en faltantes[] con detalle accionable
|
|
72
|
+
(qué campo/escenario/criterio y por qué). Si el archivo no existe o es ilegible → pass:false con el motivo.
|
|
73
|
+
NO edites nada; NO valides otras HU ni criterios de épica.`,
|
|
74
|
+
{ label: `dor:${hu}`, phase: 'HUs', agentType: 'Explore', schema: HU_CHECK_SCHEMA },
|
|
75
|
+
)
|
|
76
|
+
if (!v) return { hu, pass: false, faltantes: [`${hu}: sin veredicto`] }
|
|
77
|
+
return { hu, pass: v.pass === true, faltantes: v.faltantes || [] }
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return { hu, pass: false, faltantes: [`${hu}: el validador falló — revalidar en sesión`] }
|
|
80
|
+
}
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
// Consolidación FAIL-CLOSED + cobertura completa: shard ausente = hueco, nunca verde.
|
|
84
|
+
const done = checks.filter(Boolean)
|
|
85
|
+
const missing = hus.length - done.length
|
|
86
|
+
const bad = done.filter((c) => !c.pass)
|
|
87
|
+
const all_pass = bad.length === 0 && missing === 0
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
epica,
|
|
91
|
+
all_pass, // pass per-HU; NO es el DoR completo (falta el nivel épica)
|
|
92
|
+
hus: done, // diagnóstico por HU para el reporte ✓/✗ del gatekeeper
|
|
93
|
+
faltantes: [
|
|
94
|
+
...bad.flatMap((c) => c.faltantes),
|
|
95
|
+
...(missing > 0 ? [`${missing} HU(s) sin resultado — cobertura incompleta, revalidar en sesión`] : []),
|
|
96
|
+
],
|
|
97
|
+
note: 'Read-only. dor-dod-gatekeeper combina esto con los criterios de NIVEL ÉPICA (dependencias, cimiento, tamaño, stack, diseño, ADR) y es el único que emite el veredicto DoR y abre active_slice.',
|
|
98
|
+
}
|
|
@@ -66,7 +66,10 @@ Checklist de cierre: `references/release-dod.md`.
|
|
|
66
66
|
> **Opcional — conducir con workflow (releases grandes).** El fan-out del paso 3 puede conducirse con la
|
|
67
67
|
> plantilla `workflows/release-gate.workflow.js` (referencia, no obligatoria): SOLO paraleliza los 5 reviewers
|
|
68
68
|
> pesados; el gate `integration` (paso 4) sigue siendo **secuencial**, vía `verify`/`run` con **deps reales**,
|
|
69
|
-
> **fuera** del `parallel()`.
|
|
69
|
+
> **fuera** del `parallel()`. Pasa por `args` lo que computes **read-only**: `diffRange`, `hasUI` y **`hus[]`**
|
|
70
|
+
> (los IDs de todas las HU de las épicas de la release) — con ≥ 3 HUs el carril `coherence` se shardea por HU
|
|
71
|
+
> (lossless, fail-closed) en vez de recorrerlas en un solo agente; con menos, corre monolítico como siempre.
|
|
72
|
+
> El resultado se escribe igual en `releases[]` respetando **una escritura por
|
|
70
73
|
> entrada** y **validando contra el schema**; esta skill sigue siendo la única escritora. Parciales NO
|
|
71
74
|
> promueven a `passed`.
|
|
72
75
|
|
|
@@ -16,4 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
## Plantillas
|
|
18
18
|
- **`release-gate.workflow.js`** — `parallel(5 reviewers)` → `integration` secuencial → síntesis a
|
|
19
|
-
`releases[].gates.{security, smell, ux, coherence, stack_arch, integration}`.
|
|
19
|
+
`releases[].gates.{security, smell, ux, coherence, stack_arch, integration}`. El carril **`coherence`**
|
|
20
|
+
se shardea **por HU** cuando la release tiene ≥ 3 HUs (`args.hus[]`, computadas read-only por la skill):
|
|
21
|
+
la trazabilidad triple de cada HU es independiente → el sharding es *lossless* y el carril más lento pasa
|
|
22
|
+
de O(HUs) a O(1) + consolidación **fail-closed** con cobertura completa (shard fallido/ausente = gate
|
|
23
|
+
false, nunca muestreo). `security`/`smell`/`ux`/`stack_arch` quedan **monolíticos a propósito**:
|
|
24
|
+
shardearlos por archivos puede perder hallazgos cross-cutting.
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
export const meta = {
|
|
21
21
|
name: 'release-gate',
|
|
22
|
-
description: 'Reviewers pesados en paralelo + integración secuencial + síntesis para el Release Gate (outer loop). Read-only; devuelve veredictos, no escribe estado.',
|
|
22
|
+
description: 'Reviewers pesados en paralelo (coherence shardeado por HU cuando la release es grande) + integración secuencial + síntesis para el Release Gate (outer loop). Read-only; devuelve veredictos, no escribe estado.',
|
|
23
23
|
phases: [
|
|
24
|
-
{ title: 'Reviewers', detail: '5 reviewers
|
|
24
|
+
{ title: 'Reviewers', detail: '5 reviewers en paralelo; el carril coherence fan-out por HU (≥3 HUs) con consolidación fail-closed' },
|
|
25
25
|
{ title: 'Integration', detail: 'journey completo con deps reales (SECUENCIAL, fuera del parallel)' },
|
|
26
26
|
],
|
|
27
27
|
}
|
|
@@ -35,24 +35,79 @@ const RELEASE_REVIEW_SCHEMA = {
|
|
|
35
35
|
},
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// diffRange
|
|
38
|
+
// diffRange, si la release tiene UI y la lista de HUs llegan por `args` (la skill los computa READ-ONLY).
|
|
39
|
+
// args.hus : string[] — IDs de TODAS las HU de las épicas de la release (para shardear coherence).
|
|
40
|
+
// Ausente o < 3 → coherence corre monolítico (comportamiento previo, sin inflar releases chicas).
|
|
39
41
|
const diffRange = (args && args.diffRange) || '<merge-anterior>..main'
|
|
40
42
|
const hasUI = !!(args && args.hasUI)
|
|
43
|
+
const hus = (args && Array.isArray(args.hus)) ? args.hus.filter(Boolean) : []
|
|
44
|
+
|
|
45
|
+
// --- Carril coherence: SHARDING por HU (lossless) ----------------------------
|
|
46
|
+
// La trazabilidad triple AC↔change↔código de cada HU es INDEPENDIENTE de las demás: shardearla
|
|
47
|
+
// no pierde hallazgos cruzados (a diferencia de security/smell, que quedan monolíticos a propósito).
|
|
48
|
+
// Consolidación FAIL-CLOSED y cobertura COMPLETA: un shard fallido/ausente = gate false, nunca
|
|
49
|
+
// muestreo silencioso. Con < 3 HUs el fan-out no paga su overhead → monolítico como siempre.
|
|
50
|
+
async function coherenceLane() {
|
|
51
|
+
if (hus.length < 3) {
|
|
52
|
+
try {
|
|
53
|
+
const v = await agent(
|
|
54
|
+
`Eres el reviewer pesado del Release Gate para el gate "coherence". Verifica READ-ONLY la trazabilidad
|
|
55
|
+
triple AC↔change↔código de TODAS las HU de la release sobre el diff acumulado (${diffRange}). Si NO puedes
|
|
56
|
+
verificar, devuelve pass:false con el motivo — nunca PASS por defecto.`,
|
|
57
|
+
{ label: 'release:coherence', agentType: 'coherence-three-way', phase: 'Reviewers', schema: RELEASE_REVIEW_SCHEMA },
|
|
58
|
+
)
|
|
59
|
+
if (!v) return { gate: 'coherence', value: false, error: 'sin veredicto' }
|
|
60
|
+
return { gate: 'coherence', value: v.pass === true, findings: v.findings || [] }
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return { gate: 'coherence', value: false, error: 'el reviewer falló' }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
log(`coherence: fan-out por HU (${hus.length} shards — cobertura completa, sin muestreo)`)
|
|
66
|
+
const shards = await parallel(hus.map((hu) => async () => {
|
|
67
|
+
try {
|
|
68
|
+
const v = await agent(
|
|
69
|
+
`Eres un shard del reviewer "coherence" del Release Gate. Verifica READ-ONLY la trazabilidad triple
|
|
70
|
+
AC↔change↔código de UNA SOLA HU: ${hu}. Lee sus AC (Given/When/Then), el change OpenSpec que la cubre y el
|
|
71
|
+
código/tests del diff acumulado (${diffRange}) que la implementan. pass:true SOLO si cada AC de ${hu} tiene
|
|
72
|
+
test real y código cableado, sin huérfanos. Si NO puedes verificar → pass:false con el motivo.`,
|
|
73
|
+
{ label: `release:coherence:${hu}`, agentType: 'coherence-three-way', phase: 'Reviewers', schema: RELEASE_REVIEW_SCHEMA },
|
|
74
|
+
)
|
|
75
|
+
if (!v) return { hu, pass: false, findings: [`${hu}: sin veredicto`] }
|
|
76
|
+
return { hu, pass: v.pass === true, findings: (v.findings || []).map((f) => `${hu}: ${f}`) }
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { hu, pass: false, findings: [`${hu}: el shard falló`] }
|
|
79
|
+
}
|
|
80
|
+
}))
|
|
81
|
+
const done = shards.filter(Boolean)
|
|
82
|
+
const missing = hus.length - done.length // shard ausente (skip/kill) = hueco de cobertura
|
|
83
|
+
const bad = done.filter((s) => !s.pass)
|
|
84
|
+
const value = bad.length === 0 && missing === 0
|
|
85
|
+
return {
|
|
86
|
+
gate: 'coherence', value,
|
|
87
|
+
findings: [
|
|
88
|
+
...bad.flatMap((s) => s.findings),
|
|
89
|
+
...(missing > 0 ? [`coherence: ${missing} shard(s) sin resultado — cobertura incompleta, gate false`] : []),
|
|
90
|
+
],
|
|
91
|
+
}
|
|
92
|
+
}
|
|
41
93
|
|
|
42
94
|
// --- PASO A · Reviewers pesados EN PARALELO (exactamente estos 5) ------------
|
|
43
95
|
// Cada uno delega en su subagente sobre el MISMO diff acumulado y devuelve síntesis.
|
|
44
96
|
// Barrera deliberada: la síntesis de release necesita los 5 veredictos juntos.
|
|
97
|
+
// security/smell/ux/stack_arch quedan MONOLÍTICOS a propósito: shardearlos por archivos
|
|
98
|
+
// puede perder hallazgos cross-cutting (p.ej. un bypass de auth visible solo entre módulos).
|
|
45
99
|
phase('Reviewers')
|
|
46
100
|
const REVIEWERS = [
|
|
47
101
|
{ gate: 'security', agentType: 'security-reviewer' },
|
|
48
102
|
{ gate: 'smell', agentType: 'simple-design-reviewer' },
|
|
49
103
|
{ gate: 'ux', agentType: 'ux-krug-reviewer' }, // null SOLO si la release no tiene UI
|
|
50
|
-
{ gate: 'coherence', agentType: 'coherence-three-way' },
|
|
104
|
+
{ gate: 'coherence', agentType: 'coherence-three-way' }, // carril con sharding interno por HU (ver coherenceLane)
|
|
51
105
|
{ gate: 'stack_arch', agentType: 'stack-guardian' },
|
|
52
106
|
]
|
|
53
107
|
const reviews = await parallel(REVIEWERS.map((r) => async () => {
|
|
54
108
|
// N/A legítimo: ux sin UI → null (NO es fallo). El resto SIEMPRE corre.
|
|
55
109
|
if (r.gate === 'ux' && !hasUI) return { gate: r.gate, value: null, na: true }
|
|
110
|
+
if (r.gate === 'coherence') return coherenceLane()
|
|
56
111
|
try {
|
|
57
112
|
const v = await agent(
|
|
58
113
|
`Eres el reviewer pesado del Release Gate para el gate "${r.gate}". Revisa READ-ONLY el diff acumulado de la
|
package/state/README.md
CHANGED
|
@@ -59,6 +59,24 @@ Espejo de `scaffold` para la UI: `applies` (¿el proyecto tiene UI?), `confirmed
|
|
|
59
59
|
existe una fuente de diseño declarada), `source` (puntero al prototipo/export). El arnés NO genera el
|
|
60
60
|
prototipo. Lo respalda `design-source-guard.sh`.
|
|
61
61
|
|
|
62
|
+
### `foundation` (gate de proyecto, solo greenfield) + `project_kind`
|
|
63
|
+
|
|
64
|
+
`project_kind` (`greenfield` | `brownfield` | `null`) lo detecta `trycore-build init` con
|
|
65
|
+
heurística conservadora (`project_kind_source: "auto"`); ante ambigüedad queda `null` y
|
|
66
|
+
`/build:onboard` pregunta una vez (`"human"`). En **brownfield el mecanismo entero es N/A**: no
|
|
67
|
+
se pregunta ni se exige nada.
|
|
68
|
+
|
|
69
|
+
`foundation` es el contrato de la **épica caparazón** (app shell: navegación, layout, homepage,
|
|
70
|
+
login, redirecciones — ids canónicos en
|
|
71
|
+
`skills/building-a-slice/references/foundation-contract.md`): `{ required, epic, completed_at,
|
|
72
|
+
checklist[] }`. En greenfield, el DoR (criterio 7-bis, **proactivo**) bloquea abrir épicas
|
|
73
|
+
`layer: business` hasta que la épica caparazón (`foundation.epic`) esté **archivada con su
|
|
74
|
+
checklist evidenciada** (`foundation.completed_at` estampado); otras épicas fundacionales abren
|
|
75
|
+
libremente pero no satisfacen este gate. El DoD de la épica caparazón exige `evidence` de
|
|
76
|
+
ejecución por cada ítem `applies: true` y al archivar se estampa `completed_at`. El arnés
|
|
77
|
+
**propone** el borrador de la épica y solo lo escribe con aprobación humana explícita (carve-out
|
|
78
|
+
METODOLOGIA §9.2).
|
|
79
|
+
|
|
62
80
|
### gate `fidelity` (por-slice, inner loop) — ESTRICTO para UI
|
|
63
81
|
|
|
64
82
|
`true` = FIEL (o desviaciones justificadas, **vía verificación visual real**); `false` = desviaciones
|
|
@@ -102,3 +120,7 @@ El razonamiento vive en el modelo; el hook solo es un recordatorio determinista.
|
|
|
102
120
|
| `releases[]` (`security`, `smell`, `ux`, `coherence`, `stack_arch`, `integration`, `status`) | `releasing-a-version` (delega en `security-reviewer`, `simple-design-reviewer`, `ux-krug-reviewer`, `coherence-three-way`, `stack-guardian`) | release |
|
|
103
121
|
| `history[].reflected` · `history[].reflected_at` | `/build:reflect` | post-slice (tras archivar) |
|
|
104
122
|
| `harness_phase` | `load-build-state.sh` (SessionStart) | — |
|
|
123
|
+
| `project_kind` · `project_kind_source` | `trycore-build init` (auto) · `/build:onboard` (human, solo ambiguo) | una vez |
|
|
124
|
+
| `foundation` (`required`, `checklist[]`, `epic`) | `/build:onboard` Fase 2c | una vez (greenfield) |
|
|
125
|
+
| `foundation.checklist[].evidence` | `build-orchestrator` (durante la construcción de la caparazón) | al construir la caparazón |
|
|
126
|
+
| `foundation.completed_at` | `dor-dod-gatekeeper` (al cerrar el DoD de la épica caparazón) | al archivar la caparazón |
|
|
@@ -35,7 +35,18 @@
|
|
|
35
35
|
"parallel_front": {
|
|
36
36
|
"description": "Coordinación outer-loop de worktrees inter-épica (C). null = modo secuencial normal.",
|
|
37
37
|
"oneOf": [ { "type": "null" }, { "$ref": "#/$defs/parallel_front" } ]
|
|
38
|
-
}
|
|
38
|
+
},
|
|
39
|
+
"project_kind": {
|
|
40
|
+
"type": ["string", "null"],
|
|
41
|
+
"enum": ["greenfield", "brownfield", null],
|
|
42
|
+
"description": "Tipo de proyecto. brownfield = ya construido (el requisito de épica caparazón NO aplica y no se pregunta); greenfield = app nueva (activa el gate foundation); null = indeterminado (lo resuelve /build:onboard con el humano). Detección automática en trycore-build init."
|
|
43
|
+
},
|
|
44
|
+
"project_kind_source": {
|
|
45
|
+
"type": ["string", "null"],
|
|
46
|
+
"enum": ["auto", "human", null],
|
|
47
|
+
"description": "Quién fijó project_kind: auto (heurística del CLI, solo con certeza) | human (confirmado en onboard ante ambigüedad)."
|
|
48
|
+
},
|
|
49
|
+
"foundation": { "$ref": "#/$defs/foundation" }
|
|
39
50
|
},
|
|
40
51
|
"$defs": {
|
|
41
52
|
"scaffold": {
|
|
@@ -64,6 +75,34 @@
|
|
|
64
75
|
"notes": { "type": "string", "description": "Evidencia libre (p.ej. 'prototipo en docs/… revisado y vigente')." }
|
|
65
76
|
}
|
|
66
77
|
},
|
|
78
|
+
"foundation": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": false,
|
|
81
|
+
"description": "Gate de PROYECTO (solo greenfield): la épica caparazón — navegación/menús, layout/panel central, homepage, login/authN, redirecciones/guards — debe construirse y archivarse ANTES que cualquier épica business. El contrato es la checklist (podada por el humano en /build:onboard según el tipo de app); cada ítem applies=true exige evidence en el DoD (patrón wiring_checklist, nada de palabra de honor). El arnés propone el borrador de la épica pero solo lo escribe con aprobación humana explícita.",
|
|
82
|
+
"required": ["required"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"required": { "type": "boolean", "description": "true solo en greenfield con contrato confirmado en onboard. false = N/A (brownfield o proyecto sin caparazón exigible)." },
|
|
85
|
+
"epic": {
|
|
86
|
+
"oneOf": [ { "type": "null" }, { "type": "string", "pattern": "^EP-[0-9]{3}$" } ],
|
|
87
|
+
"description": "La épica caparazón del backlog. null mientras no exista (el gate del DoR bloquea business igual)."
|
|
88
|
+
},
|
|
89
|
+
"completed_at": { "type": ["string", "null"], "format": "date-time", "description": "Cuándo se archivó la épica caparazón con toda la checklist evidenciada. null = pendiente." },
|
|
90
|
+
"checklist": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"description": "Contrato del caparazón. Ids canónicos: navegacion-menus, layout-panel-central, homepage, login-authn, redirecciones-guards (podables; extensible por proyecto).",
|
|
93
|
+
"items": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"additionalProperties": false,
|
|
96
|
+
"required": ["item", "applies"],
|
|
97
|
+
"properties": {
|
|
98
|
+
"item": { "type": "string", "description": "Id kebab-case del ítem del contrato." },
|
|
99
|
+
"applies": { "type": "boolean", "description": "false = podado en onboard (p.ej. homepage en una API sin UI)." },
|
|
100
|
+
"evidence": { "type": "string", "description": "Evidencia de ejecución que demuestra el ítem construido (test/comando/screenshot). Vacío mientras pendiente." }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
67
106
|
"slice": {
|
|
68
107
|
"type": "object",
|
|
69
108
|
"additionalProperties": false,
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"source": "",
|
|
16
16
|
"notes": ""
|
|
17
17
|
},
|
|
18
|
+
"project_kind": null,
|
|
19
|
+
"project_kind_source": null,
|
|
20
|
+
"foundation": {
|
|
21
|
+
"required": false,
|
|
22
|
+
"epic": null,
|
|
23
|
+
"completed_at": null,
|
|
24
|
+
"checklist": []
|
|
25
|
+
},
|
|
18
26
|
"active_slice": null,
|
|
19
27
|
"history": [],
|
|
20
28
|
"releases": []
|
|
@@ -37,7 +37,7 @@ Outer loop (por release): Release Gate (seguridad · diseño · UX · cohe
|
|
|
37
37
|
5. **Producto completo, no MVP.** El alcance acordado se construye **entero**. **Recortar o diferir es bloqueante explícito** que requiere acuerdo del equipo — **nunca** una decisión del modelo. No se "deja para después" ni se deriva en lo complejo. La verificación es **ejecutada, no por inspección** (correr la suite, cargar la página, leer la consola).
|
|
38
38
|
6. **Cierre verificado, no declarado.** `dod` exige el gate `wiring_verified`: un subagente **adversarial independiente** (`wiring-adversarial-verifier`, contexto virgen) intenta refutar el slice (stubs, rutas sin cablear, AC sin test) antes de cerrar. El estado del cableado vive en disco (`wiring_checklist[]` + `progress_log[]`) para que una sesión fresca retome sin "creer que ya está".
|
|
39
39
|
7. **Fidelidad por verificación visual real.** Para slices con UI, el gate `fidelity` solo cierra observando la salida real vía MCP de devtools de navegador (screenshot app vs prototipo); sin verificación visual queda `false` (no "INCONCLUSO pasa").
|
|
40
|
-
8. **Cimiento antes que negocio y unidades pequeñas.** Las épicas de cimiento (auth, datos, arquitectura base, design-system) se construyen antes que las de negocio; una épica grande (>3 HU ó ≥3 capas) se descompone en sub-slices construidos de a uno.
|
|
40
|
+
8. **Cimiento antes que negocio y unidades pequeñas.** Las épicas de cimiento (auth, datos, arquitectura base, design-system) se construyen antes que las de negocio; una épica grande (>3 HU ó ≥3 capas) se descompone en sub-slices construidos de a uno. En proyectos **nuevos** (`project_kind: greenfield`), la **épica caparazón** (app shell: navegación, layout, homepage, login, redirecciones — gate de proyecto `foundation`) se construye y archiva **con evidencia** antes que cualquier épica de negocio; en brownfield el mecanismo es N/A.
|
|
41
41
|
9. Si una regla del arnés contradice la metodología Trycore (`METODOLOGIA.md`), **gana la metodología**.
|
|
42
42
|
|
|
43
43
|
### Bloque de dominio (lo resuelve `/build:onboard`)
|