@trycore/spec-build-harness 0.14.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.14.2",
5
+ "version": "0.16.0",
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
@@ -8,7 +8,7 @@ evoluciona con el modelo y el proyecto. No es "instalar y olvidar".
8
8
  |---|---|---|
9
9
  | Contexto | sección Construcción de CLAUDE.md, `openspec/project.md` | raíz / `openspec/` |
10
10
  | Estado (modo `legacy`, default) | `build-state.json` (+schema, README) | `.claude/state/` |
11
- | Cliente runtime (modo `dual`/`runtime`, opt-in — beta) | `runtime.credentials` (0600), `context.lock`, `runtime-projection.json`, `outbox/`; `slice-ops.sh`/`release-ops.sh` (13 subcomandos) | `.claude/state/`, `.claude/hooks/build/` |
11
+ | Cliente runtime (modo `dual`/`runtime`, opt-in — beta) | `runtime.credentials` (0600), `context.lock`, `runtime-projection.json`, `outbox/`; `slice-ops.sh`/`release-ops.sh` (20 + 3 subcomandos) | `.claude/state/`, `.claude/hooks/build/` |
12
12
  | Agentes | 14 agentes de build | `.claude/agents/build/` |
13
13
  | Hooks | settings.json + 19 scripts (15 registrados + 4 invocados: `reconcile-build-state.py`, `context-sync.sh`, `heartbeat.sh` como daemon, `statusline-bridge.sh` como comando `statusLine`) | `.claude/settings.json`, `.claude/hooks/build/` |
14
14
  | 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) · `prototyping-screens` (prototipo HTML de referencia) | `.claude/skills/` |
package/INSTALL.md CHANGED
@@ -107,7 +107,7 @@ Qué hace `init`:
107
107
  1. **Verifica requisitos duros** (a menos que uses `--skip-doctor`).
108
108
  2. **Siembra los assets** en rutas nativas de Claude Code:
109
109
  - `.claude/agents/build/` — 14 agentes.
110
- - `.claude/commands/opsx/` (10 comandos `/opsx:*`) y `.claude/commands/build/` (13 comandos: `/build:onboard`, `/build:reflect`, `/build:architect`, `/build:prototype`, `/build:slice`, `/build:release`, `/build:work`, `/build:resume`, `/build:front`, `/build:claim`, `/build:status`, `/build:escalate`, `/build:epic`). Los cuatro últimos son la **superficie de agente del modo runtime** (§9): en `legacy`, `claim`, `escalate` y `epic` devuelven `rc 3` remitiendo al flujo del fichero, y `status` informa de que la fuente de verdad es `build-state.json`.
110
+ - `.claude/commands/opsx/` (10 comandos `/opsx:*`) y `.claude/commands/build/` (14 comandos: `/build:onboard`, `/build:reflect`, `/build:architect`, `/build:prototype`, `/build:slice`, `/build:release`, `/build:work`, `/build:resume`, `/build:front`, `/build:claim`, `/build:status`, `/build:escalate`, `/build:epic`, `/build:graph-sync`). Los cinco últimos son la **superficie de agente del modo runtime** (§9): en `legacy`, `claim`, `escalate`, `epic` y `graph-sync` devuelven `rc 3` remitiendo al flujo del fichero, y `status` informa de que la fuente de verdad es `build-state.json`.
111
111
  - `.claude/skills/` — 16 skills (`building-a-slice`, `building-a-micro-change`, `releasing-a-version`, `managing-parallel-front`, `setup-architecture`, `prototyping-screens`, `openspec-*`).
112
112
  - `.claude/hooks/build/` — 19 hooks (bash + python; 6 son del cliente runtime opt-in — §9).
113
113
  3. **Siembra el estado**: `state/build-state.schema.json` y `state/README.md` se versionan;
package/METODOLOGIA.md CHANGED
@@ -332,15 +332,26 @@ worktree— y coordina la selección, la construcción y el merge desde el estad
332
332
  (`parallel_front`). Es un concepto de **outer-loop**: no es una fase nueva del pipeline por-épica, es
333
333
  orquestación **entre** épicas.
334
334
 
335
+ **Compuerta cero: un agente por worktree, un token por agente.** En modo `runtime`, cada worktree
336
+ se da de alta como **agente propio** con un token que el ADMIN emitió **para él**
337
+ (`slice-ops.sh register`, que lo pide por stdin). No es un trámite: el hub deriva el `agent_key`
338
+ del `agent_id` grabado en el token, así que dos worktrees con el mismo token son **un solo agente**
339
+ y el segundo `claim` devuelve el slice del primero. Y un worktree **sin** credenciales propias no
340
+ queda huérfano — hereda las del clon principal y **suplanta** a ese agente. Por eso toda escritura
341
+ desde un árbol sin identidad propia aborta con `rc 9` (issue #74).
342
+
335
343
  **Compuertas del front:**
336
344
 
337
345
  1. **Foundational-first (G1).** Ninguna épica `layer: foundational` entra jamás al front: el
338
346
  cimiento se construye secuencial, antes que el negocio (§1-bis.2, §3.1). Solo épicas
339
347
  `layer: business` son candidatas a paralelizarse.
340
- 2. **Disjunción por `files_scope` (G2).** `scripts/lib/front-plan.py` calcula, a partir de los globs
341
- declarados en `files_scope` de cada épica candidata, qué subconjunto es mutuamente disjunto
342
- (`selected`, va al front) y cuál se solapa (`serialized`, espera y se construye después,
343
- secuencial). Sin `files_scope` declarado, una épica no es candidata al front.
348
+ 2. **Disjunción por `files_scope` y por el grafo (G2).** `scripts/lib/front-plan.py` calcula, a
349
+ partir de los globs declarados en `files_scope` de cada épica candidata, qué subconjunto es
350
+ mutuamente disjunto (`selected`, va al front) y cuál se solapa (`serialized`, espera y se
351
+ construye después, secuencial). Sin `files_scope` declarado, una épica **no es candidata** al
352
+ front: el script la serializa (fail-closed). Tampoco son paralelizables dos épicas unidas por
353
+ un camino en el DAG de `depends_on`, aunque sus ficheros sean disjuntos — la de abajo
354
+ construiría sobre lo que la de arriba todavía no ha mergeado.
344
355
  3. **Merge en orden + re-smoke (G3).** El merge de los worktrees sigue un `merge_order`
345
356
  determinista; tras **cada** merge se re-corre el `journey_smoke` completo sobre el árbol
346
357
  principal (no basta con el smoke local del worktree). Un solape no capturado por la disjunción
package/README.md CHANGED
@@ -80,6 +80,7 @@ Gate de release que corre **una sola vez por versión** sobre el conjunto de sli
80
80
  | `/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. |
81
81
  | `/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`. |
82
82
  | `/build:epic` | Propone una **épica incremental** al hub (modo `runtime`): el agente redacta y propone, el hub asigna el `EP-XXX` al aprobar y un humano aprueba; después `epicas.md` se escribe como **proyección** del grafo. En `legacy`/`dual` no aplica. |
83
+ | `/build:graph-sync` | Propone el **re-sync del grafo** del hub desde los docs de discovery (modo `runtime`): el hub calcula el delta y un humano lo aprueba — nada se aplica sin esa aprobación. Con `--check` lee el veredicto. Sin esto, el grafo del hub se quedaba en la foto del import inicial y los eventos de las épicas posteriores rebotaban. |
83
84
  | `/opsx:*` (10) | Ciclo OpenSpec: `explore` · `new` · `continue` · `apply` · `verify` · `archive` · `bulk-archive` · `ff` · `onboard` · `sync`. Detalle → [`docs/commands.md`](docs/commands.md). |
84
85
 
85
86
  ## Gestión de contexto
@@ -120,14 +121,21 @@ Cuando hay **≥ 2 épicas de negocio (`layer: business`)** listas (DoR pasado)
120
121
  archivos**, el comando **`/build:front`** (skill `managing-parallel-front`) las construye en
121
122
  paralelo, cada una en su propio `git worktree`/rama/PR — el inner loop no cambia: cada worktree
122
123
  mantiene su `active_slice` singular y su propio `build-state.json`. Tres compuertas gobiernan el
123
- front:
124
+ front (más una compuerta cero, de identidad, cuando se corre contra el hub):
124
125
 
126
+ 0. **Un agente por worktree, un token por agente** (modo `runtime`). Cada árbol se da de alta con
127
+ `slice-ops.sh register` y un token que el ADMIN emitió **para él**: el hub deriva el `agent_key`
128
+ del token, así que dos worktrees con el mismo token son un solo agente. Un árbol sin dar de alta
129
+ hereda las credenciales del clon principal y lo suplantaría — por eso toda escritura desde él
130
+ aborta con `rc 9`.
125
131
  1. **Foundational-first.** Ninguna épica `layer: foundational` entra jamás al front; si una aparece
126
132
  mientras el front está activo, el front pasa a `status: "draining"` (termina lo que ya empezó, no
127
133
  admite épicas nuevas) hasta vaciarse.
128
- 2. **Disjunción por `files_scope`.** `scripts/lib/front-plan.py` calcula, por conjuntos de globs
129
- declarados en cada épica, qué candidatas son mutuamente disjuntas (`selected`) y cuáles deben
130
- esperar (`serialized`) por solaparse.
134
+ 2. **Disjunción por `files_scope` y por el grafo.** `scripts/lib/front-plan.py` calcula, por
135
+ conjuntos de globs declarados en cada épica, qué candidatas son mutuamente disjuntas
136
+ (`selected`) y cuáles deben esperar (`serialized`) por solaparse. Sin `files_scope` declarado una
137
+ épica **no es candidata** (se serializa), y dos épicas unidas por un camino en el DAG de
138
+ `depends_on` tampoco van juntas aunque sus ficheros sean disjuntos.
131
139
  3. **Merge en orden + re-smoke.** El merge de los worktrees sigue un `merge_order` determinista; tras
132
140
  cada merge se re-corre el `journey_smoke` completo en el árbol principal. Un conflicto no
133
141
  detectado por la disjunción declarada lo caza el re-smoke: la épica perdedora se serializa
@@ -143,7 +151,7 @@ trycore-spec-build-harness/
143
151
  ├── agents/build/ ← 14 agentes revisores (segunda opinión, contexto limpio)
144
152
  ├── commands/
145
153
  │ ├── opsx/ ← 10 comandos /opsx:* (ciclo OpenSpec)
146
- │ └── build/ ← 13 comandos /build:* (onboard, reflect, architect, prototype, slice, release, work, resume, front, claim, status, escalate, epic)
154
+ │ └── build/ ← 14 comandos /build:* (onboard, reflect, architect, prototype, slice, release, work, resume, front, claim, status, escalate, epic, graph-sync)
147
155
  ├── skills/ ← 16 skills (building-a-slice, building-a-micro-change, releasing-a-version, managing-parallel-front, setup-architecture, prototyping-screens, 10 openspec-*) + 3 plantillas *.workflow.js (opt-in, read-only)
148
156
  ├── hooks/build/ ← 19 hooks (gate-check, reflect-nudge, release-gate-nudge, scaffold-guard, gitflow-guard, stack-guard, statusline-bridge, context-monitor, reconcile-build-state, …) + 6 opt-in del cliente runtime (session-start, event-emitter, context-sync, heartbeat, dual-compare, session-stop — ver docs/hooks.md)
149
157
  ├── state/ ← máquina de estado legacy: build-state.json + schema + README
@@ -183,7 +191,36 @@ El core no menciona ningún dominio de cliente. Toda parametrización entra por
183
191
 
184
192
  ## Roadmap
185
193
 
186
- - ✅ **v0.14.0 (actual) — contexto vivo, épicas propuestas y versión de grafo** (#61, #62, #63)
194
+ - ✅ **v0.16.0 (actual) — identidad por árbol de trabajo** (#74) construir dos épicas en paralelo
195
+ en la misma máquina, una por `git worktree`, ya es una operación segura. El reparto nunca fue el
196
+ problema del hub —ya entrega dos épicas a dos agentes sin objeción—: el bloqueo estaba en el
197
+ cliente. El estado del arnés está en `.gitignore` y no viaja a un worktree, así que el árbol nuevo
198
+ **heredaba en silencio** las credenciales del clon principal y lo suplantaba: reclamaba con su
199
+ `agent_key`, reportaba los sha del árbol equivocado (cegando la detección de drift justo en el
200
+ escenario paralelo) y compartía su heartbeat. Ahora toda **escritura** desde un árbol sin dar de
201
+ alta aborta con **`rc 9`** antes de abrir un socket (las lecturas siguen, y `status` avisa con
202
+ `identidad: ⚠ HEREDADA`), y el alta es explícita: **`slice-ops.sh register`**, con el token por
203
+ **stdin** —nunca por argumento—, siembra local antes de tocar la red, verificación de que el
204
+ `agent_key` **difiera** del principal y rollback total ante cualquier fallo. Dato que corrige la
205
+ documentación anterior: `POST /agents/register` **no crea identidad** —el `agent_key` es el
206
+ `agent_id` grabado en el token por el ADMIN—, así que es **un token por agente**, no uno por
207
+ proyecto. Además, `front-plan.py` cierra tres fallos silenciosos: `layer` normalizado (el bundle
208
+ del grafo emite `FOUNDATIONAL` y la exclusión foundational-first podía no disparar),
209
+ **fail-closed** sin `files_scope` declarado, y serialización de los pares con camino en el DAG de
210
+ `depends_on`.
211
+ - ✅ **v0.15.0 — el arnés propone el re-sync de su grafo** (EP-OR-17) — el grafo del hub se sembraba
212
+ una vez, con el import de admin de `/build:onboard`, y ahí se quedaba: las épicas que discovery
213
+ escribía después no tenían por dónde entrar y **todos** los eventos de un slice cuya épica falta
214
+ rebotan. Comando **`/build:graph-sync`** (+ `--check`) y subcomandos `graph-sync`/
215
+ `graph-sync-status`: el arnés construye el bundle desde los docs, el hub calcula el delta y un
216
+ **humano aprueba** en la consola — nada se aplica sin esa aprobación; tras ella, las historias
217
+ nuevas entran solas al reparto del claim. `client_event_id` determinista para que el reintento
218
+ deduplique en vez de dejar dos propuestas del mismo grafo esperando. En el camino, dos defectos
219
+ vivos desde antes: el `409` se leía en la raíz y con un nombre que no existe en el backend (el
220
+ claim no reintentaba ante grafo rancio ni ante drift), y el **espejo del modo `dual` era un 422 el
221
+ 100 % de las veces** (mandaba un cuerpo que el contrato no tiene). Total: **14 comandos
222
+ `/build:*`**.
223
+ - ✅ **v0.14.0 — contexto vivo, épicas propuestas y versión de grafo** (#61, #62, #63) —
187
224
  tres capacidades del cliente del runtime que componen entre sí. **(1)** El daemon de heartbeat
188
225
  refresca `GET /agent/context` con **cadencia propia** (`runtime.context_refresh_s`, default 45 s,
189
226
  `0` la desactiva): antes la caché se hidrataba al arrancar la sesión y en cada `claim` —y el
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.2
1
+ 0.16.0
@@ -23,6 +23,9 @@ Eres el único que reporta gates y transiciona fases; los reviewers solo emiten
23
23
  ## Coexistencia con el front paralelo (outer-loop)
24
24
  Si hay un front paralelo abierto, el paralelismo lo gobierna la skill
25
25
  `managing-parallel-front`; cada worktree corre su propio inner loop con `active_slice` singular.
26
+ En modo `runtime`, cada worktree es además un **agente propio** con su token del ADMIN (alta con
27
+ `slice-ops.sh register`): si un árbol no está dado de alta, sus escrituras salen con `rc 9` porque
28
+ estaría operando con la identidad del clon principal — eso se arregla registrando, no reintentando.
26
29
  **Regla de drenado:** si se necesita abrir una épica `layer=foundational`, primero pon
27
30
  `parallel_front.status="draining"` (termina las en curso, no admite nuevas) y espera a cerrarlo.
28
31
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "BUILD: Front"
3
- description: Prepara y coordina un front paralelo de épicas NO fundacionales y disjuntas en archivos, cada una en su worktree/rama/PR con su propio agente registrado. Delega en la skill managing-parallel-front (selección disjunta local vía scripts/lib/front-plan.py, worktrees, merge en orden con re-smoke reportado por cada worktree). Planificar, abrir, drenar y cerrar el front son actos humanos en la consola.
3
+ description: Prepara y coordina un front paralelo de épicas NO fundacionales y disjuntas en archivos, cada una en su worktree/rama/PR con su propio agente registrado (un token por agente, alta con slice-ops.sh register). Delega en la skill managing-parallel-front (selección disjunta local vía scripts/lib/front-plan.py, worktrees, merge en orden con re-smoke reportado por cada worktree). Planificar, abrir, drenar y cerrar el front son actos humanos en la consola.
4
4
  category: Workflow
5
5
  tags: [build-harness, outer-loop, front-paralelo, trycore]
6
6
  ---
@@ -10,11 +10,15 @@ tags: [build-harness, outer-loop, front-paralelo, trycore]
10
10
  Delega en la skill **managing-parallel-front**. Resumen:
11
11
  1. Verifica precondiciones (scaffold confirmado; sin épica foundational abierta) con
12
12
  `bash .claude/hooks/build/slice-ops.sh status`.
13
- 2. Reúne candidatas no fundacionales listas (DoR pasado) con `layer` y `files_scope`.
13
+ 2. Reúne candidatas no fundacionales listas (DoR pasado) con `layer`, `files_scope` y
14
+ `depends_on`. Sin `files_scope` declarado la épica **no es candidata** (se serializa).
14
15
  3. Propón el conjunto disjunto y el `merge_order` (`scripts/lib/front-plan.py`, local) y
15
16
  **preséntaselo a una persona**: abrir el front es acto de gobierno, en la consola del hub.
16
- 4. Un worktree por épica aprobada, cada uno registrado como **agente propio**
17
- (`trycore-build init` con el mismo token de proyecto); construye con `/build:slice`.
17
+ 4. Un worktree por épica aprobada, cada uno dado de alta como **agente propio** con **su
18
+ propio token** emitido por el ADMIN: `trycore-build init` (sin `--runtime-token`) y luego
19
+ `bash .claude/hooks/build/slice-ops.sh register` (pide el token por stdin). Sin ese alta,
20
+ toda escritura desde el worktree sale con **rc 9**: estaría suplantando al clon principal.
21
+ Después, construye con `/build:slice`.
18
22
  5. Tras cada merge, re-smoke y reporte desde el worktree:
19
23
  `bash .claude/hooks/build/release-ops.sh front-integration <front_id> --merge-status … --resmoke …`.
20
24
 
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: "BUILD: Graph Sync"
3
+ description: Propone al hub el re-sync del grafo del proyecto desde los docs de discovery (epicas.md + HU-*.md). El hub calcula el delta y un humano lo aprueba en la consola; nada se aplica sin esa aprobación. Con --check no propone nada: solo consulta el veredicto de las propuestas ya enviadas. Adaptador delgado sobre slice-ops.sh graph-sync/graph-sync-status.
4
+ category: Workflow
5
+ tags: [build-harness, runtime, backlog, grafo, trycore]
6
+ ---
7
+
8
+ # /build:graph-sync — re-sincronizar el grafo del hub con los docs (la aprobación es humana)
9
+
10
+ El grafo del hub se siembra una vez, con el **import de admin** de `/build:onboard`. A partir de
11
+ ahí discovery sigue escribiendo: épicas nuevas en `docs/03-backlog/epicas.md`, historias nuevas
12
+ en las `HU-*.md`. Sin este comando, nada de eso llega al hub — y el síntoma es concreto: **todos
13
+ los eventos de un slice cuya épica no está en el grafo rebotan** con «la épica no existe en el
14
+ grafo del proyecto».
15
+
16
+ **Regla dura**: el arnés **propone**, un humano **aprueba**. Este comando no muta el grafo; deja
17
+ una propuesta con su delta calculado, y hasta que alguien la apruebe en la consola del hub no
18
+ cambia ni una arista.
19
+
20
+ **Entrada (opcional):** `--check`. Sin argumento, el comando **propone** el re-sync.
21
+
22
+ ## Despacho por argumento (léelo antes que nada)
23
+
24
+ - **Con `--check`** → ve directo al **§0** y luego **salta al §4**. `--check` es una consulta de
25
+ **solo lectura**: no construyas ningún bundle y no propongas nada (§1–§3 no se ejecutan, ni
26
+ siquiera "por si acaso"). Proponer aquí crearía una segunda propuesta del mismo grafo cada vez
27
+ que alguien comprueba el estado.
28
+ - **Sin argumento** → §0 → §1 → §2 → §3. El §4 se recorre después, cuando el humano haya
29
+ resuelto la propuesta en la consola (normalmente en otra invocación, con `--check`).
30
+
31
+ ## 0. Precondición
32
+
33
+ ```bash
34
+ bash .claude/hooks/build/slice-ops.sh mode
35
+ ```
36
+
37
+ - `legacy` → **STOP**: el grafo vive en los documentos y no hay hub con el que sincronizarlo.
38
+ - `dual` → **STOP** para proponer, pero aquí **sí hay hub**: el fichero local es el primario y el
39
+ grafo entra por el import de admin. No lo diagnostiques como un problema de conexión. El
40
+ `--dry-run` del §2 sí funciona: úsalo para revisar el grafo antes del corte a `runtime`.
41
+ - `runtime` → sigue.
42
+
43
+ ## 1. Construir el JSON de épicas desde los docs
44
+
45
+ Lee `docs/03-backlog/epicas.md` y las `HU-*.md` y escribe un fichero temporal con **todo** el
46
+ backlog vigente, no solo lo nuevo: el hub compara el bundle contra su grafo y calcula el delta,
47
+ así que mandar un subconjunto no borra nada pero tampoco describe el proyecto.
48
+
49
+ ```json
50
+ {"project_ref": "<nombre del proyecto>",
51
+ "epics": [{"code": "EP-001", "title": "…", "layer": "foundational",
52
+ "files_scope": ["src/core/**"], "depends_on": [],
53
+ "stories": [{"id": "HU-001", "title": "…"}]},
54
+ {"code": "EP-002", "title": "…", "layer": "business",
55
+ "files_scope": ["src/pagos/**"], "depends_on": ["EP-001"],
56
+ "stories": [{"id": "HU-011", "title": "…"}]}],
57
+ "release_lines": [{"id": "R1-mvp", "epics": ["EP-001", "EP-002"]}]}
58
+ ```
59
+
60
+ Es el **mismo formato** que la fase de grafo de `/build:onboard`: si ya lo generaste allí,
61
+ reutilízalo. A diferencia de las propuestas de épica, aquí el `code` **sí** viaja: el re-sync es
62
+ la puerta de lo que ya tiene identidad en los documentos.
63
+
64
+ **No inventes ninguna épica ni ninguna capa.** Si un dato falta en los docs, falta en el grafo:
65
+ se corrige en discovery, no aquí.
66
+
67
+ ## 2. Revisar antes de proponer (`--dry-run`)
68
+
69
+ ```bash
70
+ bash .claude/hooks/build/slice-ops.sh graph-sync \
71
+ --file /tmp/epics.json --from-docs docs/03-backlog/epicas.md --dry-run
72
+ ```
73
+
74
+ **Pasa siempre `--from-docs`**: relee el campo `**Depende de**` de cada sección de épica y une
75
+ ese grafo al del JSON. Es la única lectura fiable de las dependencias, que la metodología escribe
76
+ como prosa; sin él, un backlog lleno de dependencias sale con `depends_on: []` y el Lienzo del
77
+ hub queda sin una sola arista.
78
+
79
+ `--dry-run` imprime el bundle ya proyectado al contrato del hub y **no envía nada**. Revísalo con
80
+ el usuario: número de épicas, capas, aristas.
81
+
82
+ Un `rc 2` aquí no es un fallo del comando: es el bundle incumpliendo el contrato del hub, con un
83
+ motivo por línea que nombra la épica y el campo (`layer` ausente, historia sin `code`, techos
84
+ excedidos). Corrígelo **en los docs de discovery** y repite. El hub rechaza el grafo entero ante
85
+ una sola épica mal formada, así que estas comprobaciones se hacen aquí para no gastar un viaje.
86
+
87
+ ## 3. Proponer
88
+
89
+ ```bash
90
+ bash .claude/hooks/build/slice-ops.sh graph-sync \
91
+ --file /tmp/epics.json --from-docs docs/03-backlog/epicas.md
92
+ ```
93
+
94
+ | rc | Significado | Qué haces |
95
+ |---|---|---|
96
+ | `0` | Propuesta creada (el comando enseña el `id` y el delta), **o** el grafo ya estaba al día | Si hay `id`, dile al usuario que queda **pendiente de aprobación humana** en la consola. Si dice «ya está al día», no hay nada que hacer |
97
+ | `5` | Encolada, aún sin entregar (sin conexión, o el daemon estaba drenando la cola) | Avisa de que se despachará sola y de que `--check` la sigue |
98
+ | `6` | El hub la rechazó (422 con el motivo, 404, o falta `project_id`) | Muestra la razón; **no** reintentes en bucle |
99
+ | `2` | El bundle no cumple el contrato, o falta `--file` | Corrige lo que indique el mensaje, en los docs |
100
+ | `3` | `legacy`/`dual` | Vuelve al paso 0 |
101
+
102
+ Un **409 de grafo rancio** no lo verás como error: el comando refresca el contexto y reintenta
103
+ una vez con el mismo identificador, para que el hub deduplique en vez de crear una segunda
104
+ propuesta del mismo grafo.
105
+
106
+ ## 4. Después de la decisión — `/build:graph-sync --check`
107
+
108
+ **Aquí aterriza `--check`.** Es mecánico y de solo lectura:
109
+
110
+ ```bash
111
+ bash .claude/hooks/build/slice-ops.sh graph-sync-status
112
+ ```
113
+
114
+ - `APPROVED` (con la versión de grafo aplicada) → **no hay nada más que hacer**. Las historias
115
+ nuevas entran solas al reparto del claim: no hay que reclamar de otra forma ni tocar el estado.
116
+ - `REJECTED` (con el motivo que escribió el ADMIN) → **léeselo al usuario**. El motivo existe
117
+ precisamente para que esta terminal lo lea. Corrige en discovery y vuelve a proponer.
118
+ - `PROPOSED` → sigue esperando decisión humana. No propongas otra vez.
119
+ - `QUEUED` → aún sin despachar; el daemon de heartbeat la lleva.
120
+
121
+ ## Guardrails
122
+
123
+ - El arnés **propone**; la aprobación es humana (gobierno, METODOLOGIA §10 regla 8). Ni el
124
+ modelo ni el arnés aprueban un cambio de grafo.
125
+ - **No edites `docs/03-backlog/epicas.md` para «arreglar» el bundle.** El fichero es la fuente;
126
+ si el bundle no cumple, lo que está mal es la fuente, y se corrige con el usuario delante.
127
+ - **No propongas dos veces sin mirar `--check`.** Dos propuestas del mismo grafo esperando
128
+ aprobación es exactamente el ruido que este carril evita.
129
+ - Un `REJECTED` **no** se resuelve reintentando: se resuelve leyendo el motivo.
@@ -230,6 +230,9 @@ function syncGitignoreBlock(targetDir, mode) {
230
230
  // sin esto, un consumidor los commitea por descuido.
231
231
  '.claude/state/epic-proposals.json',
232
232
  '.claude/state/graph-status.json',
233
+ // [EP-OR-17] El ledger del re-sync del grafo es lo mismo: el rastro local de una
234
+ // conversación con el hub, no estado del proyecto.
235
+ '.claude/state/graph-sync-proposals.json',
233
236
  // [Ronda final · C5] Estado del daemon de heartbeat (EP-OR-08-B): un pid y unos ppids
234
237
  // solo significan algo en LA máquina que los escribió. Commiteados, otro clon recibe un
235
238
  // pidfile ajeno que el relanzamiento da por bueno hasta el `kill -0`, y un
package/docs/commands.md CHANGED
@@ -121,7 +121,7 @@ El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y
121
121
 
122
122
  | Slash command | Propósito |
123
123
  |---|---|
124
- | `/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`. |
124
+ | `/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`, que serializa también las candidatas sin `files_scope` declarado y los pares con camino en el DAG de `depends_on`) y mergea en orden con re-smoke. En modo `runtime`, **cada worktree se da de alta como agente propio** con su token del ADMIN (`slice-ops.sh register`): sin ese alta, toda escritura desde el árbol sale con `rc 9` porque estaría firmando con la identidad del clon principal. Úsalo solo con ≥2 épicas no fundacionales disjuntas listas; para una sola épica, usa `/build:slice`. |
125
125
 
126
126
  ### `/build:prototype` — prototipo HTML de referencia (fuente de diseño)
127
127
 
@@ -129,17 +129,26 @@ El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y
129
129
  |---|---|
130
130
  | `/build:prototype` | Genera o amplía el **prototipo HTML de referencia** (el `DESIGN_SOURCE`) en `docs/05-prototipo/` (`DESIGN.md` + `tokens.css` + `manifest.json` + un HTML autocontenido por pantalla). Adaptador delgado: **delega** en la skill `prototyping-screens`. Dos modos — **greenfield** (`/build:prototype`): inventario de pantallas desde PRD/mapa/historias → confirmación humana → dirección estética (manual de marca o 2-3 variantes a elección humana) → generación por lotes con auto-verificación visual; **feature** (`/build:prototype <épica>`): pantallas nuevas coherentes con el UX/UI **ya implementado**, con **precondición dura** (app corriendo + MCP de inspección de UI: extrae CSS computado real, screenshots en 3 viewports y estructura; sin degradación estática). Es **outer-loop** (antes de abrir slices). **La skill genera; el humano aprueba**: las pantallas nacen `borrador`, solo las `aprobada` son fuente de verdad (las lee `ux-fidelity-reviewer` vía `manifest.json`) y `design_source.confirmed` sigue siendo humano. |
131
131
 
132
- ### `/build:claim`, `/build:status`, `/build:escalate`, `/build:epic` — superficie de agente del runtime (beta, opt-in)
132
+ ### `/build:claim`, `/build:status`, `/build:escalate`, `/build:epic`, `/build:graph-sync` — superficie de agente del runtime (beta, opt-in)
133
133
 
134
134
  Adaptadores delgados sobre `slice-ops.sh`; en modo `legacy` (default) devuelven `rc 3` y remiten al
135
135
  protocolo del fichero (`/build:slice`) — no rompen el flujo normal si el proyecto no está migrado.
136
136
 
137
+ > **Un árbol de trabajo, un agente.** Estos comandos operan con la identidad de
138
+ > `.claude/state/runtime.credentials`. Ese fichero está en `.gitignore` y **no viaja** a un
139
+ > `git worktree`, así que un árbol nuevo hereda la del clon principal y lo suplantaría: por eso
140
+ > toda **escritura** desde un árbol sin dar de alta aborta con **`rc 9`**. El alta no es un slash
141
+ > command —pide el token por stdin y necesita un terminal humano—: se corre una vez por worktree
142
+ > con `bash .claude/hooks/build/slice-ops.sh register`. Ver
143
+ > [guía runtime → «Varios agentes en la misma máquina»](runtime/guia-modo-dual-y-migracion.md).
144
+
137
145
  | Slash command | Propósito |
138
146
  |---|---|
139
147
  | `/build:claim` | Pide la siguiente tarea al runtime (`POST /tasks/next`): sincroniza contexto, reporta hashes locales (detección de drift) y reclama por lease. Si trae `CHECKPOINT`, continúa desde ahí — **nunca reinicia** un slice de otro agente. **No acepta épica dirigida**: el hub reparte por orden de cola y `claim --epic` falla explícito con `rc 2` (issue #39 — el servidor ignoraba el `epic_code` y entregaba otra épica como si fuera la pedida, dejando un lease huérfano). |
140
148
  | `/build:status` | Informe de solo lectura: modo, slice activo, gates, wiring failing, versión de contexto, lease y cola de eventos pendientes. Nunca transiciona nada. |
141
149
  | `/build:escalate <razón>` | Registra un bloqueo en el runtime (`escalation_raised`) y devuelve la decisión a un humano — recortar, diferir o desbloquear **nunca** lo decide el modelo (mismo principio que la regla 8 de METODOLOGIA §10). |
142
150
  | `/build:epic` | Propone una épica **incremental de construcción** al hub y, tras la aprobación humana, la escribe en `docs/03-backlog/epicas.md` con el `EP-XXX` que asignó el hub (`/build:epic --check`). El agente **no elige** el código: elegirlo mirando el fichero es lo que hace colisionar a dos terminales. La descomposición inicial del backlog sigue siendo de discovery. |
151
+ | `/build:graph-sync` | Propone al hub el **re-sync del grafo** desde los docs de discovery (`epicas.md` + `HU-*.md`): el hub calcula el delta y un humano lo aprueba en la consola — **nada se aplica** sin esa aprobación. Existe porque el grafo del hub se sembraba una vez, con el import de admin, y todo lo que discovery escribía después no tenía por dónde entrar: el síntoma es que **cada evento de un slice cuya épica falta en el grafo rebota**. Con `--check` solo consulta el veredicto (`APPROVED` con la versión aplicada, o `REJECTED` con el motivo que escribió el ADMIN). Tras la aprobación no hay nada que hacer: las historias nuevas entran solas al reparto del claim. |
143
152
 
144
153
  ---
145
154
 
@@ -56,7 +56,7 @@ npm i -g @fission-ai/openspec @trycore/spec-build-harness
56
56
 
57
57
  ## 2 · `trycore-build init` (terminal)
58
58
 
59
- Desde la raíz de tu proyecto. Es **idempotente** (re-correrlo es seguro) y siembra: **14 agentes**, **16 skills** (`building-a-slice`, `building-a-micro-change`, `releasing-a-version`, `managing-parallel-front`, `setup-architecture`, `prototyping-screens` + 10 `openspec-*`), **10 comandos `/opsx:*`** + **13 comandos `/build:*`** (`onboard`, `reflect`, `architect`, `prototype`, `slice`, `release`, `work`, `resume`, `front` + `claim`, `status`, `escalate`, `epic`, que son la superficie del modo runtime), **19 hooks**, el estado vacío `.claude/state/build-state.json` (gitignored, nunca se sobreescribe) y el bloque `<!-- BEGIN trycore-build-harness -->` en tu `CLAUDE.md` con `{{placeholders}}` sin resolver.
59
+ Desde la raíz de tu proyecto. Es **idempotente** (re-correrlo es seguro) y siembra: **14 agentes**, **16 skills** (`building-a-slice`, `building-a-micro-change`, `releasing-a-version`, `managing-parallel-front`, `setup-architecture`, `prototyping-screens` + 10 `openspec-*`), **10 comandos `/opsx:*`** + **14 comandos `/build:*`** (`onboard`, `reflect`, `architect`, `prototype`, `slice`, `release`, `work`, `resume`, `front` + `claim`, `status`, `escalate`, `epic`, `graph-sync`, que son la superficie del modo runtime), **19 hooks**, el estado vacío `.claude/state/build-state.json` (gitignored, nunca se sobreescribe) y el bloque `<!-- BEGIN trycore-build-harness -->` en tu `CLAUDE.md` con `{{placeholders}}` sin resolver.
60
60
 
61
61
  > **¿Ya tenías el arnés instalado?** Entonces esto es un `update`, no un `init` — el CLI lo detecta solo por `.claude/.build-harness-version`. Corre `npm i -g @trycore/spec-build-harness@latest` y luego `trycore-build update`; **no** repitas `/build:onboard`, tu dominio ya está parametrizado. La tabla «¿Qué camino me toca?» de [`INSTALL.md`](../INSTALL.md) cubre los cuatro casos.
62
62
 
package/docs/hooks.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Hooks del arnés de construcción
2
2
 
3
- Este documento describe los **19 hooks** que `@trycore/spec-build-harness` instala en el proyecto del consumidor. Todos viven en `hooks/build/` (un script bash o python por hook, más los helpers compartidos `lib/state-io.sh`, `lib/config.sh`, `lib/runtime-client.sh`, `lib/runtime-ops.sh`, `lib/agent-context.sh`, `lib/projection.sh`) y se cablean a los eventos de Claude Code mediante una **cadena de comando única**, idéntica en ambos canales de instalación.
3
+ Este documento describe los **19 hooks** que `@trycore/spec-build-harness` instala en el proyecto del consumidor. Todos viven en `hooks/build/` (un script bash o python por hook, más los helpers compartidos `lib/state-io.sh`, `lib/config.sh`, `lib/runtime-client.sh`, `lib/runtime-ops.sh`, `lib/agent-context.sh`, `lib/projection.sh`, `lib/harness-meta.sh`) y se cablean a los eventos de Claude Code mediante una **cadena de comando única**, idéntica en ambos canales de instalación.
4
4
 
5
5
  Los hooks son el sistema nervioso del arnés: vigilan GitFlow, el stack declarado, el scaffold y la fuente de diseño (bloqueantes), inyectan y re-anclan el estado del build al iniciar la sesión, delegan estilo/typecheck a herramientas, vigilan la presión de contexto y escriben handoff automático, y recuerdan validar trazabilidad, gates abiertos, reflexionar al cerrar un slice y correr el Release Gate cuando se acumulan épicas sin auditar. No reemplazan a los agentes ni a las skills; los **complementan** liberando capacidad de razonamiento del modelo y poniendo barandillas mecánicas donde un olvido cuesta caro.
6
6
 
@@ -41,7 +41,9 @@ Los hooks son el sistema nervioso del arnés: vigilan GitFlow, el stack declarad
41
41
  >
42
42
  > `lib/state-io.sh` (`legacy`), `lib/config.sh` (`runtime.mode`, umbrales), `lib/runtime-client.sh`
43
43
  > (cliente HTTP + cola offline), `lib/runtime-ops.sh` (helpers de `slice-ops.sh`/`release-ops.sh`),
44
- > `lib/agent-context.sh` (normaliza `GET /agent/context`) y `lib/projection.sh` (lee esa caché,
44
+ > `lib/agent-context.sh` (normaliza `GET /agent/context`), `lib/harness-meta.sh` (versión del arnés
45
+ > y catálogo de asset types, compartidos por el registro de sesión y el alta de un worktree) y
46
+ > `lib/projection.sh` (lee esa caché,
45
47
  > sin red) **no son hooks**: son los helpers compartidos que los scripts de arriba `source`an.
46
48
 
47
49
  ---
@@ -120,12 +120,13 @@ arman peticiones a mano. En `dual`, cada transición también se **espeja** al s
120
120
  identidad del fichero (que sigue mandando). Si el espejo falla (red caída, servidor rechaza), el
121
121
  trabajo local **nunca se bloquea** — se avisa y queda como discrepancia para el comparador.
122
122
 
123
- `slice-ops.sh` tiene **17 subcomandos** (los conducen las skills; los listamos para que puedas
124
- leer un log o depurar, no para que los teclees):
123
+ `slice-ops.sh` tiene **20 subcomandos** (los conducen las skills; los listamos para que puedas
124
+ leer un log o depurar, no para que los teclees — `register` es la excepción: ese lo tecleas tú):
125
125
 
126
126
  | Subcomando | Para qué |
127
127
  |---|---|
128
128
  | `mode` | imprime `legacy`\|`dual`\|`runtime` |
129
+ | `register` | da de alta **este árbol de trabajo** como agente propio, con su token (lo pide por stdin). Solo hace falta en un `git worktree` — ver «Varios agentes en la misma máquina» |
129
130
  | `claim` | reclama trabajo (el hub reparte por orden de cola; sin épica dirigida) |
130
131
  | `next-step` | deriva la siguiente acción desde la caché de proyección |
131
132
  | `phase` | reporta la fase del pipeline (emite la cadena que falte; idempotente) |
@@ -136,12 +137,87 @@ leer un log o depurar, no para que los teclees):
136
137
  | `fact` | reporta un hecho de proyecto confirmado |
137
138
  | `propose-asset` | propone un documento al plano de contexto (lo publica un ADMIN) |
138
139
  | `propose-epic` · `epic-status` · `epic-writeback` | proponen una épica, consultan su estado y proyectan la aprobada a `epicas.md` |
140
+ | `graph-sync` · `graph-sync-status` | proponen el re-sync del grafo desde los docs de discovery y leen el veredicto humano (solo en `runtime`) |
139
141
  | `status` · `escalate` | informe local del agente y registro de un bloqueo |
140
142
 
141
143
  Del lado release, `release-ops.sh` añade `verdict` y `front-integration`.
142
144
 
143
145
  **Códigos de salida** (sobre estos ramifica la prosa de las skills): `0` ok · `2` uso · `3` legacy ·
144
- `4` sin slice · `5` offline · `6` rechazado · `7` sin trabajo.
146
+ `4` sin slice · `5` offline · `6` rechazado · `7` sin trabajo · `8` tipo no espejable ·
147
+ `9` identidad heredada (este árbol no está dado de alta — ver abajo).
148
+
149
+ ### Varios agentes en la misma máquina (0.16.0)
150
+
151
+ Si abres un `git worktree` por épica para construir en paralelo (`/build:front`), **cada árbol
152
+ necesita su propio token de agente**, emitido por el ADMIN para él. No es burocracia:
153
+
154
+ - El hub **no crea identidad** al registrar. El `agent_key` es el `agent_id` que va grabado en el
155
+ token, así que dos árboles con el mismo token son **un solo agente** — y el segundo `claim` te
156
+ devuelve el slice que ya tiene reclamado el primero.
157
+ - El estado del arnés está en `.gitignore` y **no viaja** al worktree. Sin credenciales propias, el
158
+ árbol nuevo hereda las del clon principal y **suplanta** a ese agente: reclamaría con su
159
+ `agent_key`, reportaría los hashes del árbol equivocado y compartiría su heartbeat.
160
+
161
+ Por eso, en un árbol sin dar de alta, **toda escritura** (`claim`, `phase`, `gate`, `submit`,
162
+ `archive`, `wiring`, `progress`, `checkpoint`, `fact`, `propose-*`, `graph-sync`, `escalate` y
163
+ `release-ops.sh front-integration`) aborta con `rc 9` **antes de tocar la red**. Las lecturas siguen
164
+ funcionando —son el diagnóstico de ese árbol— y `status` lo dice en su primera línea:
165
+
166
+ ```
167
+ identidad: ⚠ HEREDADA del clon principal (/ruta/al/clon) — solo lectura
168
+ ```
169
+
170
+ El alta es un comando, en el terminal, una vez por worktree:
171
+
172
+ ```bash
173
+ cd .wt/ep-003
174
+ trycore-build init # tiende .claude/ (sin --runtime-token)
175
+ bash .claude/hooks/build/slice-ops.sh register # pide el token por stdin
176
+ ```
177
+
178
+ `register` **no acepta el token por argumento** (`--token` sale con `rc 2`): en argv quedaría en el
179
+ historial del shell y en `ps`. Por lo mismo lo corres tú y no un hook — las tool calls no tienen
180
+ TTY. Antes de tocar la red siembra el fichero local (si no, el registro habría escrito sobre las
181
+ credenciales del clon principal), y después verifica contra el hub que el `agent_key` obtenido
182
+ **difiere** del suyo; si coincide, lo rechaza y te dice que pidas al ADMIN un token con `agent_id`
183
+ distinto. Ante cualquier fallo revierte: el árbol queda con identidad propia y verificada, o
184
+ exactamente como estaba. El fichero del clon principal no se toca nunca.
185
+
186
+ Para rotar el token de un árbol ya registrado: `register --force`.
187
+
188
+ ### Qué espeja el modo `dual` — y qué no (0.15.0)
189
+
190
+ El espejo cubre la **enumeración normativa** del hub, ni más ni menos: `slice_opened`,
191
+ `phase_advanced`, `phase_reverted`, `gate_verdict`, `wiring_item_updated`,
192
+ `wiring_checklist_seeded`, `checkpoint_recorded`, `progress_noted` y `handoff_recorded`. Todo eso
193
+ viaja como `{"transitions": [{client_event_id, epic_code, event_type, payload}]}`, con la épica
194
+ del `build-state.json` local en el **sobre**, y el hub responde un reporte **por elemento** cuyo
195
+ motivo de rechazo el cliente te enseña tal cual.
196
+
197
+ Lo que **no** se espeja, y por qué — son huecos del **hub**, no del arnés, y el cliente los corta
198
+ en local (`rc 8`) en vez de gastar una llamada para recibir un rechazo:
199
+
200
+ | No espejable | Motivo |
201
+ |---|---|
202
+ | `slice_escalated` · `slice_archived` · `slice_submitted` | Existen en el catálogo del hub pero no heredan de `_TransicionDeEspejo`: la ingesta espejo no los acepta. Una discrepancia dual sigue siendo **visible** donde la lees —el aviso del hook `Stop`—, pero no queda registrada en el hub. |
203
+ | `release_verdict_reported` · `front_integration_reported` | No existen en el catálogo del hub, y la superficie síncrona de los veredictos de release es de **gobierno humano**. En `dual` el veredicto se anota en local y el comando te lo dice con esas palabras; quedará reflejado al pasar a `runtime`. |
204
+
205
+ Cerrar estos huecos requiere un tipo nuevo en el catálogo del hub con su rama de reducer: es
206
+ trabajo hub-side. Mientras tanto el arnés lo **nombra** en vez de fallar en silencio.
207
+
208
+ ### Cuando el hub no conoce tu épica (0.15.0)
209
+
210
+ Si el espejo rechaza con «la épica no existe en el grafo del proyecto», el grafo del hub se quedó
211
+ en la foto del import inicial. La salida es proponer el re-sync:
212
+
213
+ ```bash
214
+ /build:graph-sync # construye el bundle desde los docs y lo propone
215
+ /build:graph-sync --check # lee el veredicto: APPROVED, o REJECTED con su motivo
216
+ ```
217
+
218
+ En `dual` el carril de propuesta no opera (el fichero es primario y el grafo entra por el import
219
+ de admin), pero `slice-ops.sh graph-sync --file epics.json --dry-run` **sí** funciona: úsalo para
220
+ revisar el grafo antes del corte a `runtime`.
145
221
 
146
222
  ### Lo que aportó la 0.14.0
147
223