@trycore/spec-build-harness 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/GOVERNANCE.md +3 -3
  3. package/METODOLOGIA.md +44 -0
  4. package/README.md +60 -5
  5. package/VERSION +1 -1
  6. package/agents/build/build-orchestrator.md +6 -0
  7. package/commands/build/front.md +15 -0
  8. package/commands/build/resume.md +29 -0
  9. package/config/build-config.template.json +8 -0
  10. package/dist/commands/init.js +15 -5
  11. package/dist/commands/status.js +1 -0
  12. package/dist/commands/uninstall.js +2 -1
  13. package/dist/lib/paths.js +7 -0
  14. package/dist/lib/settings-merge.js +29 -2
  15. package/dist/lib/state-seed.js +14 -0
  16. package/docs/commands.md +15 -3
  17. package/docs/decisiones/2026-07-03-gsd-vs-openspec-fork-vs-rama.md +157 -0
  18. package/docs/hooks.md +22 -5
  19. package/hooks/build/build-gate-check.sh +1 -1
  20. package/hooks/build/context-monitor.sh +80 -0
  21. package/hooks/build/design-source-guard.sh +1 -1
  22. package/hooks/build/lib/state-io.sh +55 -0
  23. package/hooks/build/lint-typecheck.sh +1 -1
  24. package/hooks/build/load-build-state.sh +46 -2
  25. package/hooks/build/reconcile-build-state.py +70 -0
  26. package/hooks/build/reflect-nudge.sh +1 -1
  27. package/hooks/build/release-gate-nudge.sh +1 -1
  28. package/hooks/build/scaffold-guard.sh +1 -1
  29. package/hooks/build/stack-guard.sh +1 -1
  30. package/hooks/build/statusline-bridge.sh +32 -0
  31. package/hooks/build-harness.json +24 -0
  32. package/package.json +1 -1
  33. package/scripts/lib/front-plan.py +47 -0
  34. package/scripts/smoke-test.sh +12 -0
  35. package/scripts/tests/test-context-monitor.sh +106 -0
  36. package/scripts/tests/test-front-plan.sh +38 -0
  37. package/scripts/tests/test-install.sh +89 -0
  38. package/scripts/tests/test-reconciler.sh +54 -0
  39. package/scripts/tests/test-schema.sh +58 -0
  40. package/skills/building-a-slice/references/dor.md +2 -0
  41. package/skills/managing-parallel-front/SKILL.md +36 -0
  42. package/state/build-state.schema.json +50 -0
@@ -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.7.1",
5
+ "version": "0.8.1",
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",
package/GOVERNANCE.md CHANGED
@@ -9,9 +9,9 @@ evoluciona con el modelo y el proyecto. No es "instalar y olvidar".
9
9
  | Contexto | sección Construcción de CLAUDE.md, `openspec/project.md` | raíz / `openspec/` |
10
10
  | Estado | `build-state.json` (+schema, README) | `.claude/state/` |
11
11
  | Agentes | 12 agentes de build | `.claude/agents/build/` |
12
- | Hooks | settings.json + 10 scripts | `.claude/settings.json`, `.claude/hooks/build/` |
13
- | Skill | `building-a-slice` (+11 refs · `workflows/`) · `releasing-a-version` (`workflows/`) · `building-a-micro-change` (carril ligero de mantenimiento) | `.claude/skills/` |
14
- | Comandos | `/opsx:*` · `/build:onboard` · `/build:reflect` · `/build:slice` · `/build:release` · `/build:work` | `.claude/commands/` |
12
+ | Hooks | settings.json + 13 scripts | `.claude/settings.json`, `.claude/hooks/build/` |
13
+ | Skill | `building-a-slice` (+11 refs · `workflows/`) · `releasing-a-version` (`workflows/`) · `building-a-micro-change` (carril ligero de mantenimiento) · `managing-parallel-front` (front paralelo inter-épica) | `.claude/skills/` |
14
+ | Comandos | `/opsx:*` · `/build:onboard` · `/build:reflect` · `/build:slice` · `/build:release` · `/build:work` · `/build:resume` · `/build:front` | `.claude/commands/` |
15
15
  | Config | allowlist de stack | `.claude/config/stack-allowlist.json` |
16
16
 
17
17
  ## Fases de activación (`harness_phase`)
package/METODOLOGIA.md CHANGED
@@ -282,6 +282,43 @@ paralelo** y devuelven síntesis (protegen el contexto). Resultado en `releases[
282
282
 
283
283
  ---
284
284
 
285
+ ## 5-bis. Front paralelo inter-épica (outer-loop, opcional)
286
+
287
+ El inner loop (§3) construye **una épica a la vez, secuencial**. Cuando hay **≥ 2 épicas
288
+ `layer: business` listas** (DoR pasado) y **disjuntas en archivos**, el arnés permite construirlas
289
+ **en paralelo** sin romper la regla de un slice activo por árbol: el comando `/build:front`
290
+ (skill `managing-parallel-front`) abre un **`git worktree` por épica** —cada worktree con su propio
291
+ `active_slice` singular y su propio `build-state.json`; el inner loop **no cambia** dentro de cada
292
+ worktree— y coordina la selección, la construcción y el merge desde el estado raíz
293
+ (`parallel_front`). Es un concepto de **outer-loop**: no es una fase nueva del pipeline por-épica, es
294
+ orquestación **entre** épicas.
295
+
296
+ **Compuertas del front:**
297
+
298
+ 1. **Foundational-first (G1).** Ninguna épica `layer: foundational` entra jamás al front: el
299
+ cimiento se construye secuencial, antes que el negocio (§1-bis.2, §3.1). Solo épicas
300
+ `layer: business` son candidatas a paralelizarse.
301
+ 2. **Disjunción por `files_scope` (G2).** `scripts/lib/front-plan.py` calcula, a partir de los globs
302
+ declarados en `files_scope` de cada épica candidata, qué subconjunto es mutuamente disjunto
303
+ (`selected`, va al front) y cuál se solapa (`serialized`, espera y se construye después,
304
+ secuencial). Sin `files_scope` declarado, una épica no es candidata al front.
305
+ 3. **Merge en orden + re-smoke (G3).** El merge de los worktrees sigue un `merge_order`
306
+ determinista; tras **cada** merge se re-corre el `journey_smoke` completo sobre el árbol
307
+ principal (no basta con el smoke local del worktree). Un solape no capturado por la disjunción
308
+ declarada que produzca conflicto de merge lo caza este re-smoke: la épica perdedora se
309
+ **serializa** (rebase + re-correr sus gates), nunca se fuerza el merge ni se aborta el front
310
+ entero.
311
+
312
+ **Regla de drenado (drain).** Si mientras el front está activo (`parallel_front.status: "open"`)
313
+ aparece o se vuelve elegible una épica `layer: foundational`, el front **deja de admitir épicas
314
+ nuevas** y pasa a `parallel_front.status: "draining"`: los worktrees en curso terminan y mergean
315
+ normalmente, pero no se abren worktrees adicionales. Solo cuando todos los miembros llegan a
316
+ `merge_status: "merged"` (`parallel_front` se limpia a `null`) puede abrirse la épica fundacional
317
+ pendiente. El drenado existe porque **fundacional siempre precede a negocio** (§1-bis.2): el front
318
+ nunca puede ser la razón por la que el cimiento se retrasa.
319
+
320
+ ---
321
+
285
322
  ## 6. Contrato de trazabilidad change ↔ épica
286
323
 
287
324
  Cada OpenSpec change corresponde a **exactamente una épica** (la unidad de construcción) y declara
@@ -349,6 +386,9 @@ agente lo lee antes de actuar. Estructura: `version`, `harness_phase`, `active_s
349
386
  | `gates.data` | `data-consistency-checker` | slice |
350
387
  | `gates.wiring_verified` | `wiring-adversarial-verifier` (independiente, contexto virgen) | slice (antes de `dod`) |
351
388
  | `releases[]` (security, smell, ux, coherence, stack_arch, integration, status) | `releasing-a-version` (delega en los reviewers) | release |
389
+ | `parallel_front` (status, members[], merge_order) | skill `managing-parallel-front` (vía `/build:front`) | front (outer-loop, opcional) |
390
+ | `active_slice.session_continuity` | `context-monitor.sh` (auto-handoff) / `/build:resume` | por sesión (critical/PreCompact) |
391
+ | `active_slice.branch_drift`, degradación de `wiring_checklist` sin evidencia | `reconcile-build-state.py` (SessionStart / `/build:resume`) | por sesión |
352
392
  | `harness_phase` | `load-build-state.sh` (SessionStart) | — |
353
393
 
354
394
  **Régimen de fases (`harness_phase`):** arranca en `authoring`; `load-build-state.sh` lo cambia a
@@ -457,3 +497,7 @@ El arnés **no escribe** en `docs/`; cuando una HU no cumple DoR, devuelve el tr
457
497
  10. El core es **agnóstico**: lo específico del dominio se inyecta vía `/build:onboard` y
458
498
  `stack-allowlist.json`; el ejemplo de referencia vive en `docs/examples/reference/`.
459
499
  11. Si una skill, agente o reference contradice este documento, **gana la metodología**.
500
+ 12. **Front paralelo es outer-loop y drena ante fundacional (§5-bis)**: solo épicas `layer: business`
501
+ disjuntas en `files_scope` se paralelizan; una épica `layer: foundational` nunca entra al front y
502
+ lo pone en `draining` hasta que se vacía; el merge exige re-smoke del journey completo tras cada
503
+ integración.
package/README.md CHANGED
@@ -76,8 +76,61 @@ Gate de release que corre **una sola vez por versión** sobre el conjunto de sli
76
76
  | `/build:slice` | Entrada del **inner loop**: abre o continúa un slice (épica `EP-XXX`) y conduce el pipeline DoR → change → TDD → smoke → api/data → DoD → PR+archive. Adaptador delgado que delega en la skill `building-a-slice`. |
77
77
  | `/build:release` | Entrada del **outer loop**: corre el Release Gate **una sola vez** sobre el diff acumulado (los 5 reviewers pesados en paralelo + integración secuencial). Delega en la skill `releasing-a-version`. |
78
78
  | `/build:work` | Router *classify-and-act*: clasifica el trabajo entrante y enruta al carril correcto (`building-a-micro-change` · `building-a-slice` · `releasing-a-version`). Es ruteo, no política: no ejecuta el pipeline ni toca el estado. |
79
+ | `/build:resume` | Rehidrata el slice activo **desde disco** (no desde la conversación) tras un reinicio de contexto: reconcilia el estado, lee `session_continuity`/`wiring_checklist`/`parallel_front` y determina la siguiente acción por prioridad. |
80
+ | `/build:front` | Abre y coordina un **front paralelo** de épicas no fundacionales y disjuntas en archivos (`parallel_front`), cada una en su worktree/rama/PR. Delega en la skill `managing-parallel-front`. |
79
81
  | `/opsx:*` (10) | Ciclo OpenSpec: `explore` · `new` · `continue` · `apply` · `verify` · `archive` · `bulk-archive` · `ff` · `onboard` · `sync`. Detalle → [`docs/commands.md`](docs/commands.md). |
80
82
 
83
+ ## Gestión de contexto
84
+
85
+ Las sesiones largas (una épica multicapa, un slice que se alarga) pueden agotar la ventana de
86
+ contexto antes de terminar el cableado. El arnés lo mitiga con un **motor de contexto** que corre
87
+ solo, sin que el humano lo invoque:
88
+
89
+ - **Aviso por umbrales.** El hook `context-monitor.sh` lee el contexto restante en cada
90
+ `PostToolUse`/`PreCompact`/`Stop` y lo compara contra dos umbrales **configurables**
91
+ (`context.warning_pct: 35`, `context.critical_pct: 25` en `build-config.json`): por debajo del
92
+ umbral de aviso, inyecta un recordatorio de acercarse a un punto de corte natural; por debajo del
93
+ crítico (o en `PreCompact`), dispara un **auto-handoff** —una sola vez por sesión— que escribe en
94
+ `active_slice.session_continuity` un `resume_hint` derivado de los items `wiring_checklist` aún
95
+ `failing`, sin esperar a que el humano lo pida.
96
+ - **`/build:resume`.** Para retomar sin pérdida: reconcilia el estado contra disco y determina la
97
+ siguiente acción por prioridad (`resume_hint` → primer item `failing` → siguiente `sub_slice` →
98
+ fase del pipeline). Reconstruye el contexto **desde el estado**, nunca desde la conversación viva.
99
+ - **`config context.auto_checkpoint`** (opt-in, default `false`): si se activa, el handoff marca
100
+ `auto_continue: true` para que la siguiente sesión retome automáticamente el `resume_hint` sin
101
+ preguntar. Apagado por defecto porque el checkpoint automático es una decisión que el equipo debe
102
+ optar explícitamente a tomar.
103
+ - **Caveat de canal.** El aviso proactivo depende de `statusLine`, un ajuste de `settings.json` que
104
+ **solo existe en el canal CLI** (`trycore-build init`/`update`). En una instalación **plugin-only**
105
+ no hay puente de contexto que leer y el motor **degrada fail-open**: no rompe nada, simplemente no
106
+ avisa; `/build:resume` y el reconciliador de disco (ver abajo) siguen funcionando igual porque no
107
+ dependen del puente.
108
+
109
+ El **estado sigue anclado a disco** entre sesiones: `reconcile-build-state.py` corre en cada
110
+ `SessionStart` (y en `/build:resume`) y deriva la verdad desde git y la evidencia de tests —degrada a
111
+ `failing` cualquier item `wiring_checklist` marcado `passing` sin `evidence`, anota *branch drift* si
112
+ la rama real difiere de la del slice, y nunca revierte un gate booleano por sí mismo (ratchet)—.
113
+ Fail-open siempre: un estado ilegible o `git` ausente no bloquean la sesión.
114
+
115
+ ## Front paralelo
116
+
117
+ Cuando hay **≥ 2 épicas de negocio (`layer: business`)** listas (DoR pasado) y **disjuntas en
118
+ archivos**, el comando **`/build:front`** (skill `managing-parallel-front`) las construye en
119
+ paralelo, cada una en su propio `git worktree`/rama/PR — el inner loop no cambia: cada worktree
120
+ mantiene su `active_slice` singular y su propio `build-state.json`. Tres compuertas gobiernan el
121
+ front:
122
+
123
+ 1. **Foundational-first.** Ninguna épica `layer: foundational` entra jamás al front; si una aparece
124
+ mientras el front está activo, el front pasa a `status: "draining"` (termina lo que ya empezó, no
125
+ admite épicas nuevas) hasta vaciarse.
126
+ 2. **Disjunción por `files_scope`.** `scripts/lib/front-plan.py` calcula, por conjuntos de globs
127
+ declarados en cada épica, qué candidatas son mutuamente disjuntas (`selected`) y cuáles deben
128
+ esperar (`serialized`) por solaparse.
129
+ 3. **Merge en orden + re-smoke.** El merge de los worktrees sigue un `merge_order` determinista; tras
130
+ cada merge se re-corre el `journey_smoke` completo en el árbol principal. Un conflicto no
131
+ detectado por la disjunción declarada lo caza el re-smoke: la épica perdedora se serializa
132
+ (rebase + re-correr sus gates) en vez de abortar el front.
133
+
81
134
  ## Arquitectura
82
135
 
83
136
  ```
@@ -88,11 +141,11 @@ trycore-spec-build-harness/
88
141
  ├── agents/build/ ← 12 agentes revisores (segunda opinión, contexto limpio)
89
142
  ├── commands/
90
143
  │ ├── opsx/ ← 10 comandos /opsx:* (ciclo OpenSpec)
91
- │ └── build/ ← 5 comandos /build:* (onboard, reflect, slice, release, work)
92
- ├── skills/ ← 13 skills (building-a-slice, building-a-micro-change, releasing-a-version, 10 openspec-*) + 3 plantillas *.workflow.js (opt-in, read-only)
93
- ├── hooks/build/ ← 10 hooks bash (gate-check, reflect-nudge, release-gate-nudge, scaffold-guard, gitflow-guard, stack-guard, …)
144
+ │ └── build/ ← 7 comandos /build:* (onboard, reflect, slice, release, work, resume, front)
145
+ ├── skills/ ← 14 skills (building-a-slice, building-a-micro-change, releasing-a-version, managing-parallel-front, 10 openspec-*) + 3 plantillas *.workflow.js (opt-in, read-only)
146
+ ├── hooks/build/ ← 13 hooks (gate-check, reflect-nudge, release-gate-nudge, scaffold-guard, gitflow-guard, stack-guard, statusline-bridge, context-monitor, reconcile-build-state, …)
94
147
  ├── state/ ← máquina de estado: build-state.json + schema + README
95
- ├── config/ ← stack-allowlist.template.json (artefacto del consumidor)
148
+ ├── config/ ← build-config.template.json (umbrales de contexto) + stack-allowlist.template.json (artefacto del consumidor)
96
149
  ├── src/ + dist/ ← CLI trycore-build (init/update/status/uninstall/doctor)
97
150
  ├── scripts/ ← installer + guardias (check-version-sync/agnostic/state-clean)
98
151
  └── docs/examples/reference/ ← ejemplo de referencia (fuera del core, excluido de check-agnostic)
@@ -126,7 +179,9 @@ El core no menciona ningún dominio de cliente. Toda parametrización entra por
126
179
  - ✅ **v0.4.0** — carril `building-a-micro-change` (mantenimiento ligero sin slice) + DoR proporcional a la complejidad.
127
180
  - ✅ **v0.5.0** — seguro de fuente de diseño (`design_source` + `design-source-guard.sh`) + agente `ux-fidelity-reviewer` (gate `fidelity`, inner loop). Total: **11 agentes**, **9 hooks**.
128
181
  - ✅ **v0.6.0** — **calidad de cierre contra horizonte largo** (feedback exodocs): handoff fino en disco (`wiring_checklist[]` + `progress_log[]` + `sub_slices[]`), gate `wiring_verified` por nuevo agente **`wiring-adversarial-verifier`** (verificación adversarial independiente, contexto virgen), cimiento pre-construido + tag `layer` y gate de tamaño en el DoR, runner fuera-de-chat `integration-check`, y **fidelidad estricta por verificación visual real** (MCP requerido para UI). Convenciones anti-deriva (producto completo, no MVP) upstreadas al bloque del arnés. Total: **12 agentes**, **9 hooks**.
129
- - ✅ **v0.7.0 (actual)** — **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:*`**.
182
+ - ✅ **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:*`**.
183
+ - ✅ **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**.
184
+ - ✅ **v0.8.1 (actual)** — **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`.
130
185
 
131
186
  ## Licencia
132
187
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.1
1
+ 0.8.1
@@ -14,6 +14,12 @@ Protocolo en `.claude/state/README.md`. **Sólo un slice activo a la vez** (mode
14
14
  La **unidad de construcción es la épica** (`active_slice.epica`); las HU que cubre el change van en
15
15
  `active_slice.hus[]`. Un slice = una épica = un change = una rama = un PR.
16
16
 
17
+ ## Coexistencia con el front paralelo (outer-loop)
18
+ Si `parallel_front` existe en el estado principal, el paralelismo lo gobierna la skill
19
+ `managing-parallel-front`; cada worktree corre su propio inner loop con `active_slice` singular.
20
+ **Regla de drenado:** si se necesita abrir una épica `layer=foundational`, primero pon
21
+ `parallel_front.status="draining"` (termina las en curso, no admite nuevas) y espera a cerrarlo.
22
+
17
23
  ## Trabajo por fases encadenadas (NO one-shot)
18
24
  Una épica multicapa es demasiado para una pasada. Trabaja por **fases encadenadas** —**mapear →
19
25
  generar → revisar → fix-loop → optimizar**— nunca todo de golpe. Reparte la **exploración** "ancho
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: "BUILD: Front"
3
+ description: Abre y coordina un front paralelo de épicas NO fundacionales y disjuntas en archivos, cada una en su worktree/rama/PR. Delega en la skill managing-parallel-front (selección disjunta vía scripts/lib/front-plan.py, worktrees, merge en orden con re-smoke).
4
+ category: Workflow
5
+ tags: [build-harness, outer-loop, front-paralelo, trycore]
6
+ ---
7
+
8
+ # /build:front — Front paralelo inter-épica
9
+
10
+ Delega en la skill **managing-parallel-front**. Resumen:
11
+ 1. Verifica precondiciones (scaffold confirmado; sin épica foundational abierta).
12
+ 2. Reúne candidatas no fundacionales listas (DoR pasado) con `layer` y `files_scope`.
13
+ 3. Selecciona el conjunto disjunto (`scripts/lib/front-plan.py`), abre worktrees, construye y mergea en orden con re-smoke.
14
+
15
+ Úsalo solo cuando haya ≥2 épicas no fundacionales disjuntas listas. Para una sola épica, usa `/build:slice`.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: "BUILD: Resume"
3
+ description: Rehidrata el slice activo desde disco tras un reinicio de contexto (reconcilia estado, muestra continuidad y la siguiente acción).
4
+ category: Workflow
5
+ tags: [build-harness, resume, rehydrate, trycore]
6
+ ---
7
+
8
+ # /build:resume — Retomar sin pérdida
9
+
10
+ Objetivo: reconstruir el contexto de construcción **desde disco**, no desde la conversación.
11
+
12
+ ## Pasos
13
+
14
+ 1. **Reconciliar**: `python3 .claude/hooks/build/reconcile-build-state.py .claude/state/build-state.json`
15
+ (degrada wiring sin evidencia; anota branch drift; fail-open).
16
+
17
+ 2. **Leer estado**: `.claude/state/build-state.json`. Extraer `active_slice`, sus `gates`, `wiring_checklist`, `progress_log`, `session_continuity`, y `parallel_front` si existe.
18
+
19
+ 3. **Determinar la siguiente acción por prioridad** (la primera que aplique):
20
+ 1. `session_continuity.resume_hint` presente → ejecutarla.
21
+ 2. Items de `wiring_checklist` en `failing` → cablear el primero (con prueba real; no marcar passing sin evidencia).
22
+ 3. `sub_slices` con `status!=done` → construir el siguiente.
23
+ 4. Según `active_slice.phase` → continuar el pipeline (delegar en la skill `building-a-slice`).
24
+
25
+ 4. **Si hay `parallel_front`**: delegar en la skill `managing-parallel-front` (Task 12+).
26
+
27
+ 5. Registrar un hito en `progress_log[]` (`by: /build:resume`).
28
+
29
+ **Regla dura:** mientras quede un item `failing`, el slice NO está terminado. Nunca marques `passing` sin evidencia de ejecución real.
@@ -0,0 +1,8 @@
1
+ {
2
+ "context": {
3
+ "warning_pct": 35,
4
+ "critical_pct": 25,
5
+ "auto_checkpoint": false,
6
+ "stale_seconds": 60
7
+ }
8
+ }
@@ -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 } from '../lib/state-seed.js';
12
+ import { seedState, seedConfig, seedBuildConfig } 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';
@@ -45,9 +45,16 @@ export async function init(opts) {
45
45
  linkChildren(ASSETS.commandsOpsx, t.commandsOpsx, linkMode, (n) => n.endsWith('.md'));
46
46
  linkChildren(ASSETS.commandsBuild, t.commandsBuild, linkMode, (n) => n.endsWith('.md'));
47
47
  const nSkills = linkChildren(ASSETS.skills, t.skillsDir, linkMode);
48
- linkChildren(ASSETS.hooksBuild, t.hooksBuild, linkMode, (n) => n.endsWith('.sh'));
49
- if (linkMode === 'copy')
50
- chmodExec(t.hooksBuild); // preserva +x [H1]
48
+ // hooks/build: acepta .sh Y .py en el nivel raíz (reconcile-build-state.py es un hook v0.8).
49
+ linkChildren(ASSETS.hooksBuild, t.hooksBuild, linkMode, (n) => n.endsWith('.sh') || n.endsWith('.py'));
50
+ // hooks/build/lib: subcarpeta NO recorrida por el linkChildren de arriba — hay que recursar a mano.
51
+ linkChildren(ASSETS.hooksBuildLib, t.hooksBuildLib, linkMode, (n) => n.endsWith('.sh'));
52
+ // scripts/lib: asset nuevo consumido por /build:front (front-plan.py).
53
+ linkChildren(ASSETS.scriptsLib, t.scriptsLib, linkMode, (n) => n.endsWith('.py'));
54
+ if (linkMode === 'copy') {
55
+ chmodExec(t.hooksBuild); // preserva +x [H1] (recorre hooks/build/ incl. lib/)
56
+ chmodExec(t.scriptsLib);
57
+ }
51
58
  // 2) Estado (schema/README versionados; build-state.json vacío solo si falta) [C1]
52
59
  const { stateSeeded } = seedState(targetDir);
53
60
  // 3) stack-allowlist.json del consumidor (artefacto del consumidor)
@@ -60,6 +67,7 @@ export async function init(opts) {
60
67
  }
61
68
  }
62
69
  const { configSeeded } = seedConfig(targetDir, allowlist);
70
+ const { buildConfigSeeded } = seedBuildConfig(targetDir);
63
71
  // 4) Hooks + permisos mínimos en settings.json (canal CLI) [H3,H11]
64
72
  const { addedHooks, addedPerms } = mergeHarnessSettings(t.settingsFile);
65
73
  // 5) CLAUDE.md: bloque marcado con {{placeholders}} SIN resolver (los resuelve /build:onboard)
@@ -75,8 +83,10 @@ export async function init(opts) {
75
83
  console.log(` Comandos: ${countChildren(t.commandsOpsx)} /opsx:* + ${countChildren(t.commandsBuild)} /build:*`);
76
84
  console.log(` Skills: ${nSkills} en .claude/skills/`);
77
85
  console.log(` Hooks: ${countChildren(t.hooksBuild)} en .claude/hooks/build/ (settings.json +${addedHooks} hook(s), +${addedPerms} permiso(s))`);
86
+ console.log(` Scripts: ${countChildren(t.scriptsLib)} en .claude/scripts/lib/`);
78
87
  console.log(` Estado: ${stateSeeded ? 'build-state.json sembrado (vacío)' : 'build-state.json preservado'}`);
79
88
  console.log(` Config: ${configSeeded ? 'stack-allowlist.json sembrado' : 'stack-allowlist.json preservado'}`);
89
+ console.log(` Config: ${buildConfigSeeded ? 'build-config.json sembrado' : 'build-config.json preservado'}`);
80
90
  console.log('');
81
91
  console.log('Siguiente paso — abre Claude Code y ejecuta:');
82
92
  console.log(' /build:onboard # parametriza el dominio (PII, capa IA, capa determinista) y escribe memoria');
@@ -133,7 +143,7 @@ function syncGitignoreBlock(targetDir, mode) {
133
143
  const lines = [GI_BEGIN + ' ───────────────────────', '.claude/state/build-state.json'];
134
144
  if (mode === 'symlink') {
135
145
  // Los symlinks absolutos a node_modules global se rompen en otra máquina si se commitean.
136
- lines.push('.claude/agents/build/', '.claude/commands/opsx/', '.claude/commands/build/', '.claude/hooks/build/');
146
+ lines.push('.claude/agents/build/', '.claude/commands/opsx/', '.claude/commands/build/', '.claude/hooks/build/', '.claude/scripts/lib/');
137
147
  const skillNames = fs.existsSync(ASSETS.skills)
138
148
  ? fs.readdirSync(ASSETS.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
139
149
  : [];
@@ -27,6 +27,7 @@ export async function status(opts) {
27
27
  console.log(` Comandos: ${countChildren(t.commandsOpsx)} /opsx:* · ${countChildren(t.commandsBuild)} /build:*`);
28
28
  console.log(` Skills: ${countChildren(t.skillsDir)} en .claude/skills/`);
29
29
  console.log(` Hooks: ${countChildren(t.hooksBuild)} en .claude/hooks/build/`);
30
+ console.log(` Scripts: ${countChildren(t.scriptsLib)} en .claude/scripts/lib/`);
30
31
  console.log('');
31
32
  console.log('Requisitos externos:');
32
33
  console.log(` openspec: ${hasBinary('openspec') ? '✓' : '✗ falta (npm i -g @fission-ai/openspec)'}`);
@@ -14,7 +14,8 @@ export async function uninstall(opts) {
14
14
  }
15
15
  console.log(`▶ uninstall trycore-build-harness de ${targetDir}`);
16
16
  // 1) Assets exclusivos del arnés
17
- for (const p of [t.agentsBuild, t.commandsOpsx, t.commandsBuild, t.hooksBuild]) {
17
+ // scriptsLib (NO t.scriptsDir completo .claude/scripts/ podría llevar contenido del consumidor).
18
+ for (const p of [t.agentsBuild, t.commandsOpsx, t.commandsBuild, t.hooksBuild, t.scriptsLib]) {
18
19
  if (fs.existsSync(p))
19
20
  fs.rmSync(p, { recursive: true, force: true });
20
21
  }
package/dist/lib/paths.js CHANGED
@@ -32,12 +32,15 @@ export const ASSETS = {
32
32
  commandsBuild: path.join(PACKAGE_ROOT, 'commands', 'build'),
33
33
  skills: path.join(PACKAGE_ROOT, 'skills'),
34
34
  hooksBuild: path.join(PACKAGE_ROOT, 'hooks', 'build'),
35
+ hooksBuildLib: path.join(PACKAGE_ROOT, 'hooks', 'build', 'lib'),
36
+ scriptsLib: path.join(PACKAGE_ROOT, 'scripts', 'lib'),
35
37
  // Declaración de hooks para el canal PLUGIN (no la instala el CLI; la consume el plugin nativo).
36
38
  pluginHooks: path.join(PACKAGE_ROOT, 'hooks', 'build-harness.json'),
37
39
  stateSchema: path.join(PACKAGE_ROOT, 'state', 'build-state.schema.json'),
38
40
  stateReadme: path.join(PACKAGE_ROOT, 'state', 'README.md'),
39
41
  stateTemplate: path.join(PACKAGE_ROOT, 'state', 'build-state.template.json'),
40
42
  configTemplate: path.join(PACKAGE_ROOT, 'config', 'stack-allowlist.template.json'),
43
+ buildConfigTemplate: path.join(PACKAGE_ROOT, 'config', 'build-config.template.json'),
41
44
  templateClaudeMd: path.join(PACKAGE_ROOT, 'templates', 'CLAUDE.md.template'),
42
45
  settingsHooksTemplate: path.join(PACKAGE_ROOT, 'templates', 'settings-hooks.template.json'),
43
46
  };
@@ -51,8 +54,12 @@ export function targetPaths(targetDir) {
51
54
  commandsBuild: path.join(claudeDir, 'commands', 'build'),
52
55
  skillsDir: path.join(claudeDir, 'skills'),
53
56
  hooksBuild: path.join(claudeDir, 'hooks', 'build'),
57
+ hooksBuildLib: path.join(claudeDir, 'hooks', 'build', 'lib'),
58
+ scriptsDir: path.join(claudeDir, 'scripts'),
59
+ scriptsLib: path.join(claudeDir, 'scripts', 'lib'),
54
60
  configDir: path.join(claudeDir, 'config'),
55
61
  configFile: path.join(claudeDir, 'config', 'stack-allowlist.json'),
62
+ buildConfigFile: path.join(claudeDir, 'config', 'build-config.json'),
56
63
  stateDir: path.join(claudeDir, 'state'),
57
64
  stateSchema: path.join(claudeDir, 'state', 'build-state.schema.json'),
58
65
  stateReadme: path.join(claudeDir, 'state', 'README.md'),
@@ -14,13 +14,23 @@ const CHAIN = '${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/';
14
14
  function cmd(script) {
15
15
  return `"${CHAIN}${script}"`;
16
16
  }
17
- /** Las 5 agrupaciones de hooks del arnés (= las de hooks/build-harness.json). */
17
+ // statusLine: SOLO canal CLI los plugins no pueden inyectar statusLine.
18
+ // Misma cadena autorresolutiva que los hooks, así funciona tanto si el
19
+ // arnés se instaló por CLI como si conviven ambos canales (CLAUDE_PLUGIN_ROOT
20
+ // resuelve si el plugin está presente; si no, cae a $CLAUDE_PROJECT_DIR/.claude).
21
+ // En instalaciones plugin-only NO hay statusLine → context-monitor.sh no
22
+ // tiene puente (claude-ctx-*.json) que leer → degrada fail-open (sin aviso),
23
+ // lo cual es aceptable.
24
+ const STATUSLINE_CMD = cmd('statusline-bridge.sh');
25
+ /** Las agrupaciones de hooks del arnés (= las de hooks/build-harness.json). */
18
26
  const HOOK_SPECS = [
19
27
  { event: 'SessionStart', matcher: 'startup|clear|compact', scripts: ['load-build-state.sh'] },
20
28
  { event: 'PreToolUse', matcher: 'Bash', scripts: ['gitflow-guard.sh'] },
21
29
  { event: 'PreToolUse', matcher: 'Write|Edit|MultiEdit', scripts: ['stack-guard.sh', 'scaffold-guard.sh', 'design-source-guard.sh'] },
22
30
  { event: 'PostToolUse', matcher: 'Write|Edit|MultiEdit', scripts: ['lint-typecheck.sh', 'coherence-flag.sh'] },
23
- { event: 'Stop', matcher: '.*', scripts: ['build-gate-check.sh', 'reflect-nudge.sh', 'release-gate-nudge.sh'] },
31
+ { event: 'PostToolUse', matcher: 'Bash|Edit|Write|MultiEdit|Task', scripts: ['context-monitor.sh'] },
32
+ { event: 'PreCompact', matcher: '.*', scripts: ['context-monitor.sh'] },
33
+ { event: 'Stop', matcher: '.*', scripts: ['build-gate-check.sh', 'reflect-nudge.sh', 'release-gate-nudge.sh', 'context-monitor.sh'] },
24
34
  ];
25
35
  /** Permisos MÍNIMOS y enumerados [H11]. Nunca permisos amplios (mcp__*, additionalDirectories…). */
26
36
  const MIN_PERMISSIONS = [
@@ -49,6 +59,10 @@ function readSettings(file) {
49
59
  function writeSettings(file, s) {
50
60
  fs.writeFileSync(file, JSON.stringify(s, null, 2) + '\n', 'utf8');
51
61
  }
62
+ /** True si el statusLine actual (si existe) ya es el nuestro (por script). */
63
+ function isOwnStatusLine(statusLine) {
64
+ return typeof statusLine?.command === 'string' && statusLine.command.includes('statusline-bridge.sh');
65
+ }
52
66
  function existingCommands(settings, event) {
53
67
  const out = new Set();
54
68
  for (const group of settings.hooks?.[event] ?? []) {
@@ -83,6 +97,14 @@ export function mergeHarnessSettings(settingsFile) {
83
97
  addedPerms++;
84
98
  }
85
99
  }
100
+ // statusLine: solo canal CLI (el plugin no inyecta statusLine). Escribe el
101
+ // puente de contexto. Idempotente: si ya apunta a statusline-bridge.sh no
102
+ // lo reescribe (evita diffs innecesarios); si el consumidor no tiene
103
+ // statusLine o tiene otra cosa, este paquete reclama el slot (es único,
104
+ // no se puede "mergear" como los hooks).
105
+ if (!isOwnStatusLine(settings.statusLine)) {
106
+ settings.statusLine = { type: 'command', command: STATUSLINE_CMD };
107
+ }
86
108
  writeSettings(settingsFile, settings);
87
109
  return { addedHooks, addedPerms };
88
110
  }
@@ -119,6 +141,11 @@ export function removeHarnessSettings(settingsFile) {
119
141
  changed = true;
120
142
  }
121
143
  }
144
+ // statusLine: quitar SOLO si es el nuestro (simetría con la instalación).
145
+ if (isOwnStatusLine(settings.statusLine)) {
146
+ delete settings.statusLine;
147
+ changed = true;
148
+ }
122
149
  if (changed)
123
150
  writeSettings(settingsFile, settings);
124
151
  return changed;
@@ -59,3 +59,17 @@ export function seedConfig(targetDir, allowlist) {
59
59
  void path;
60
60
  return { configSeeded: true };
61
61
  }
62
+ /**
63
+ * Siembra el build-config.json del consumidor (umbrales del context engine).
64
+ * Solo si NO existe. Idempotente: nunca pisa la config ya ajustada por el equipo.
65
+ */
66
+ export function seedBuildConfig(targetDir) {
67
+ const t = targetPaths(targetDir);
68
+ ensureDir(t.configDir);
69
+ if (fs.existsSync(t.buildConfigFile))
70
+ return { buildConfigSeeded: false };
71
+ if (!fs.existsSync(ASSETS.buildConfigTemplate))
72
+ return { buildConfigSeeded: false };
73
+ fs.copyFileSync(ASSETS.buildConfigTemplate, t.buildConfigFile);
74
+ return { buildConfigSeeded: true };
75
+ }
package/docs/commands.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Esta referencia cubre los **dos planos de operación** del arnés de construcción:
4
4
 
5
5
  1. El **CLI `trycore-build`** (binario Node, paquete `@trycore/spec-build-harness` v0.7.0) — instala, actualiza, diagnostica y desinstala el arnés en el proyecto consumidor. Captura el **stack mecánico**.
6
- 2. Los **slash commands de Claude Code** (`/opsx:*` + los 5 `/build:*`: `onboard`, `reflect`, `slice`, `release`, `work`) — operan el pipeline de dos loops, resuelven la parametrización **semántica** del dominio y capturan el conocimiento aprendido por slice.
6
+ 2. Los **slash commands de Claude Code** (`/opsx:*` + los 7 `/build:*`: `onboard`, `reflect`, `slice`, `release`, `work`, `resume`, `front`) — operan el pipeline de dos loops, resuelven la parametrización **semántica** del dominio y capturan el conocimiento aprendido por slice.
7
7
 
8
8
  > **División de responsabilidades del onboarding (dos capas).** Un binario Node **no puede** escribir la auto-memory de Claude. Por eso `trycore-build init` siembra archivos y captura el stack mecánico (lenguaje/deps, package manager, runtime, ruta del PRD), y el slash command `/build:onboard` —ejecutado por Claude— lee el PRD, pregunta por PII / capa de servicios externos-IA / capa determinista / secretos / decisiones de alto impacto, resuelve los `{{placeholders}}` del bloque marcado de `CLAUDE.md` y escribe la auto-memory.
9
9
 
@@ -52,7 +52,7 @@ Solo `init` y `update` aceptan flags. `status`, `uninstall` y `doctor` toman ún
52
52
 
53
53
  ## 2. Slash commands de Claude Code
54
54
 
55
- El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y `.claude/commands/build/` → los 5 `/build:*` (`onboard`, `reflect`, `slice`, `release`, `work`).
55
+ El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y `.claude/commands/build/` → los 7 `/build:*` (`onboard`, `reflect`, `slice`, `release`, `work`, `resume`, `front`).
56
56
 
57
57
  ### `/opsx:*` — pipeline OpenSpec
58
58
 
@@ -99,6 +99,18 @@ El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y
99
99
  |---|---|
100
100
  | `/build:reflect` | Tras archivar un slice, detecta **convenciones aprendidas** / errores recurrentes (a partir del change, el diff y la sesión) y **propone** viñetas para el bloque `trycore-build-learnings` de `CLAUDE.md`; las aplica **solo tras tu aprobación** y estampa el slice como reflexionado (`reflected: true`). Lo **sugiere** el hook `reflect-nudge.sh` al cerrar sesión, pero puedes invocarlo cuando quieras. No toca el bloque de dominio ni sus `{{placeholders}}` (eso es de `/build:onboard`). |
101
101
 
102
+ ### `/build:resume` — rehidratación sin pérdida de contexto
103
+
104
+ | Slash command | Propósito |
105
+ |---|---|
106
+ | `/build:resume` | Reconstruye el contexto de construcción **desde disco**, no desde la conversación: corre `reconcile-build-state.py`, lee `active_slice` (`gates`, `wiring_checklist`, `progress_log`, `session_continuity`, `parallel_front`) y determina la siguiente acción por prioridad (`resume_hint` → item `failing` de wiring → `sub_slices` pendiente → fase del pipeline). Es el punto de entrada tras un handoff automático del hook `context-monitor.sh` o tras cualquier reinicio de contexto. |
107
+
108
+ ### `/build:front` — front paralelo inter-épica
109
+
110
+ | Slash command | Propósito |
111
+ |---|---|
112
+ | `/build:front` | Abre y coordina un **front paralelo** de épicas NO fundacionales y disjuntas en archivos, cada una en su propio worktree/rama/PR. Delega en la skill `managing-parallel-front`: verifica precondiciones (scaffold confirmado, sin épica foundational abierta), selecciona el conjunto disjunto (`scripts/lib/front-plan.py`) y mergea en orden con re-smoke. Úsalo solo con ≥2 épicas no fundacionales disjuntas listas; para una sola épica, usa `/build:slice`. |
113
+
102
114
  ---
103
115
 
104
116
  ## 3. Caveat de canales (CLI vs. plugin)
@@ -108,7 +120,7 @@ El arnés se distribuye por **dos canales** que coexisten, pero **namespacean di
108
120
  | Aspecto | Canal **CLI** (canónico) | Canal **Plugin** nativo |
109
121
  |---|---|---|
110
122
  | Instalación | `npm i -g @trycore/spec-build-harness` → `trycore-build init` | `/plugin marketplace add <repo-github>` → `/plugin install trycore-spec-build-harness@trycore-build` |
111
- | Namespace de comandos | Por subcarpeta: `/opsx:*` y los 5 `/build:*` (`onboard`, `reflect`, `slice`, `release`, `work`) | Por nombre del plugin: `/trycore-spec-build-harness:*` (por diseño de Claude Code) |
123
+ | Namespace de comandos | Por subcarpeta: `/opsx:*` y los 7 `/build:*` (`onboard`, `reflect`, `slice`, `release`, `work`, `resume`, `front`) | Por nombre del plugin: `/trycore-spec-build-harness:*` (por diseño de Claude Code) |
112
124
  | Referencia a agentes | Por su nombre (p. ej. `build-orchestrator`) | Bajo el nombre del plugin |
113
125
  | Cross-references internas | ✔ Escritas para este canal (skills invocan `/opsx:*`, agentes por nombre) | Pueden no resolver según están escritas |
114
126
  | Recomendación | **Usar este canal para operar un proyecto** | Conveniencia a nivel usuario |