@trycore/spec-build-harness 0.8.5 → 0.10.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.
Files changed (106) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/GOVERNANCE.md +27 -4
  3. package/INSTALL.md +27 -5
  4. package/METODOLOGIA.md +55 -5
  5. package/README.md +39 -6
  6. package/VERSION +1 -1
  7. package/agents/build/build-orchestrator.md +33 -7
  8. package/agents/build/dor-dod-gatekeeper.md +13 -5
  9. package/agents/build/wiring-adversarial-verifier.md +52 -5
  10. package/commands/build/architect.md +1 -1
  11. package/commands/build/claim.md +46 -0
  12. package/commands/build/escalate.md +36 -0
  13. package/commands/build/front.md +9 -3
  14. package/commands/build/onboard.md +59 -14
  15. package/commands/build/prototype.md +3 -2
  16. package/commands/build/reflect.md +60 -40
  17. package/commands/build/release.md +10 -7
  18. package/commands/build/resume.md +33 -13
  19. package/commands/build/slice.md +32 -27
  20. package/commands/build/status.md +35 -0
  21. package/commands/build/work.md +11 -8
  22. package/config/build-config.template.json +4 -0
  23. package/dist/cli.js +22 -0
  24. package/dist/commands/doctor.js +42 -0
  25. package/dist/commands/init.js +84 -1
  26. package/dist/commands/migrate.js +48 -0
  27. package/dist/commands/status.js +34 -0
  28. package/dist/lib/normalize.js +276 -0
  29. package/dist/lib/paths.js +6 -0
  30. package/dist/lib/runtime-client.js +196 -0
  31. package/dist/lib/settings-merge.js +3 -3
  32. package/dist/lib/state-bundle.js +46 -0
  33. package/docs/commands.md +25 -8
  34. package/docs/getting-started.md +1 -0
  35. package/docs/hooks.md +114 -27
  36. package/docs/runtime/guia-modo-dual-y-migracion.md +136 -0
  37. package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
  38. package/docs/runtime/protocolo-cliente-runtime.md +109 -34
  39. package/hooks/build/build-gate-check.sh +21 -0
  40. package/hooks/build/context-monitor.sh +82 -15
  41. package/hooks/build/context-sync.sh +192 -0
  42. package/hooks/build/design-source-guard.sh +30 -2
  43. package/hooks/build/dual-compare.sh +92 -0
  44. package/hooks/build/event-emitter.sh +75 -0
  45. package/hooks/build/gitflow-guard.sh +164 -14
  46. package/hooks/build/heartbeat.sh +259 -0
  47. package/hooks/build/lib/agent-context.sh +139 -0
  48. package/hooks/build/lib/config.sh +27 -0
  49. package/hooks/build/lib/projection.sh +71 -0
  50. package/hooks/build/lib/runtime-client.sh +465 -0
  51. package/hooks/build/lib/runtime-ops.sh +221 -0
  52. package/hooks/build/lib/state-io.sh +5 -18
  53. package/hooks/build/load-build-state.sh +64 -2
  54. package/hooks/build/reflect-nudge.sh +15 -0
  55. package/hooks/build/release-gate-nudge.sh +15 -0
  56. package/hooks/build/release-ops.sh +164 -0
  57. package/hooks/build/scaffold-guard.sh +29 -2
  58. package/hooks/build/session-start.sh +103 -0
  59. package/hooks/build/session-stop.sh +22 -0
  60. package/hooks/build/slice-ops.sh +877 -0
  61. package/hooks/build/stack-guard.sh +8 -0
  62. package/hooks/build/statusline-bridge.sh +24 -3
  63. package/hooks/build-harness.json +16 -0
  64. package/package.json +3 -3
  65. package/scripts/check-agnostic.sh +3 -1
  66. package/scripts/check-pack-clean.sh +31 -0
  67. package/scripts/check-runtime-purity.sh +43 -0
  68. package/scripts/lib/front-plan.py +4 -0
  69. package/scripts/lib/graph-bundle.py +133 -0
  70. package/scripts/runtime-purity-allow.txt +5 -0
  71. package/scripts/smoke-test.sh +1 -1
  72. package/scripts/tests/lib/http-stub.py +46 -0
  73. package/scripts/tests/test-baseline-verdict.sh +92 -0
  74. package/scripts/tests/test-config.sh +25 -0
  75. package/scripts/tests/test-hooks-runtime.sh +853 -0
  76. package/scripts/tests/test-install.sh +57 -0
  77. package/scripts/tests/test-runtime-client.sh +298 -0
  78. package/scripts/tests/test-schema.sh +29 -1
  79. package/scripts/tests/test-skill-ops.sh +847 -0
  80. package/skills/building-a-micro-change/SKILL.md +22 -4
  81. package/skills/building-a-slice/SKILL.md +55 -21
  82. package/skills/building-a-slice/assets/baseline-verdict.sh +172 -0
  83. package/skills/building-a-slice/references/dod.md +12 -3
  84. package/skills/building-a-slice/references/dor.md +3 -2
  85. package/skills/building-a-slice/references/evidence-budget.md +51 -0
  86. package/skills/building-a-slice/references/exploration-fanout.md +1 -1
  87. package/skills/building-a-slice/references/gitflow.md +1 -1
  88. package/skills/building-a-slice/references/regression-baseline.md +67 -0
  89. package/skills/building-a-slice/references/runtime-protocol.md +75 -0
  90. package/skills/building-a-slice/references/state-protocol.md +12 -1
  91. package/skills/building-a-slice/workflows/README.md +7 -3
  92. package/skills/building-a-slice/workflows/explore-fanout.workflow.js +3 -3
  93. package/skills/building-a-slice/workflows/wiring-verify.workflow.js +26 -4
  94. package/skills/managing-parallel-front/SKILL.md +32 -16
  95. package/skills/openspec-archive-change/SKILL.md +15 -0
  96. package/skills/prototyping-screens/SKILL.md +9 -5
  97. package/skills/releasing-a-version/SKILL.md +25 -16
  98. package/skills/releasing-a-version/references/release-dod.md +7 -5
  99. package/skills/releasing-a-version/workflows/README.md +2 -1
  100. package/skills/releasing-a-version/workflows/release-gate.workflow.js +6 -5
  101. package/skills/setup-architecture/SKILL.md +4 -2
  102. package/state/README.md +16 -1
  103. package/state/build-state.schema.json +2 -1
  104. package/templates/CLAUDE.md.template +16 -0
  105. package/templates/settings-hooks.template.json +8 -4
  106. package/internal/skills/auditar-arnes/SKILL.md +0 -29
package/docs/hooks.md CHANGED
@@ -1,17 +1,25 @@
1
1
  # Hooks del arnés de construcción
2
2
 
3
- Este documento describe los **13 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 el helper compartido `lib/state-io.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`) 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
 
7
+ > **Modo runtime (beta, opt-in — EP-OR-08).** Seis de los 19 hooks (`session-start.sh`,
8
+ > `event-emitter.sh`, `context-sync.sh`, `heartbeat.sh`, `dual-compare.sh`, `session-stop.sh`) son
9
+ > los que hacen del arnés un **cliente del Agent Orchestrator Runtime** cuando el proyecto corre en
10
+ > modo `dual`/`runtime` (`config/build-config.json#runtime.mode`, default `legacy`). En `legacy`
11
+ > son no-op o casi (fail-open: sin URL/token, no tocan red). Guía de uso →
12
+ > `docs/runtime/guia-modo-dual-y-migracion.md`. Contrato de red → `docs/runtime/protocolo-cliente-runtime.md`.
13
+
7
14
  ---
8
15
 
9
- ## Resumen de los 13 hooks
16
+ ## Resumen de los 19 hooks
10
17
 
11
18
  | Hook | Evento | Matcher | Qué hace | ¿Bloqueante? |
12
19
  |---|---|---|---|---|
13
- | `load-build-state.sh` | `SessionStart` | `startup\|clear\|compact` | Inyecta al contexto la rama, la fase del arnés, el slice activo y los gates abiertos; sincroniza `harness_phase`. Invoca a `reconcile-build-state.py` antes de leer el estado. | No |
14
- | `reconcile-build-state.py` | `SessionStart` (invocado por `load-build-state.sh`) | | Ancla `build-state.json` a la realidad de git + tests: degrada a `failing` los items de `wiring_checklist` marcados `passing` sin `evidence`, y anota (sin corregir) el *drift* entre la rama real y `active_slice.branch`. Nunca revierte gates `true→false`. | No |
20
+ | `session-start.sh` | `SessionStart` | `startup\|clear\|compact` | (Beta.) Solo si el modo no es `legacy`: registra el agente (`POST /agents/register`), refresca `GET /agent/context`, dispara `context-sync.sh` y asegura el daemon `heartbeat.sh`. Escribe la caché de proyección; no inyecta contexto (eso lo hace `load-build-state.sh`, que corre después). En `legacy`, no-op. | No |
21
+ | `load-build-state.sh` | `SessionStart` | `startup\|clear\|compact` | Inyecta al contexto la rama, la fase del arnés, el slice activo y los gates abiertos. En `legacy`, sincroniza `harness_phase` e invoca a `reconcile-build-state.py`; en `dual`/`runtime`, renderiza desde la caché de proyección que escribió `session-start.sh`. | No |
22
+ | `reconcile-build-state.py` | `SessionStart` (invocado por `load-build-state.sh`, solo `legacy`) | — | Ancla `build-state.json` a la realidad de git + tests: degrada a `failing` los items de `wiring_checklist` marcados `passing` sin `evidence`, y anota (sin corregir) el *drift* entre la rama real y `active_slice.branch`. Nunca revierte gates `true→false`. | No |
15
23
  | `statusline-bridge.sh` | `statusLine` (comando, no evento de hooks; solo canal CLI) | — | Imprime la línea de estado (`🏗️ build · ctx N%`) y escribe el archivo-puente de contexto (`claude-ctx-<session>.json`) que `context-monitor.sh` consume para calcular la presión de contexto. | No |
16
24
  | `gitflow-guard.sh` | `PreToolUse` | `Bash` | Enforce GitHub Flow estricto sobre `git commit` / `git push`. | **Sí (exit 2)** |
17
25
  | `stack-guard.sh` | `PreToolUse` | `Write\|Edit\|MultiEdit` | Bloquea dependencias en `package.json` fuera de la allowlist del stack del PRD. | **Sí (exit 2)** |
@@ -19,20 +27,39 @@ Los hooks son el sistema nervioso del arnés: vigilan GitFlow, el stack declarad
19
27
  | `design-source-guard.sh` | `PreToolUse` | `Write\|Edit\|MultiEdit` | Bloquea escribir código de un slice **con UI** (`gates.fidelity===false`, fases `red…data`) si la fuente de diseño del proyecto no está confirmada (`design_source.confirmed`). | **Sí (exit 2)** |
20
28
  | `lint-typecheck.sh` | `PostToolUse` | `Write\|Edit\|MultiEdit` | Corre prettier/eslint/tsc sobre el archivo `.ts`/`.tsx` editado. | No |
21
29
  | `coherence-flag.sh` | `PostToolUse` | `Write\|Edit\|MultiEdit` | Recuerda validar la trazabilidad de un `proposal.md` de OpenSpec recién tocado. | No |
22
- | `context-monitor.sh` | `PostToolUse` \| `PreCompact` \| `Stop` | `Bash\|Edit\|Write\|MultiEdit\|Task` (`PostToolUse`) · `.*` (`PreCompact`/`Stop`) | Lee el puente de contexto de `statusline-bridge.sh`, aplica los umbrales `context.warning_pct`/`context.critical_pct` y, si está por debajo, inyecta `additionalContext` de advertencia; en `critical` escribe una sola vez por sesión el handoff (`active_slice.session_continuity`) en `build-state.json`. | No |
30
+ | `event-emitter.sh` | `PostToolUse` | `Bash\|Edit\|Write\|MultiEdit\|Task` | (Beta.) Solo si el modo no es `legacy`: encola telemetría (`tool_use_recorded`) en la cola offline nunca la red en el hilo del hook. Metadatos únicamente (herramienta, ruta relativa, `argv0`); nunca el comando completo ni el fuente. | No |
31
+ | `context-monitor.sh` | `PostToolUse` \| `PreCompact` \| `Stop` | `Bash\|Edit\|Write\|MultiEdit\|Task` (`PostToolUse`) · `.*` (`PreCompact`/`Stop`) | Lee el puente de contexto de `statusline-bridge.sh`, aplica los umbrales `context.warning_pct`/`context.critical_pct` y, si está por debajo, inyecta `additionalContext` de advertencia; en `critical` escribe el handoff una sola vez por sesión — en `build-state.json` (`legacy`) o como evento `handoff_recorded` (`dual`/`runtime`). | No |
23
32
  | `build-gate-check.sh` | `Stop` | `.*` | Al cerrar el turno, avisa si el slice activo tiene gates abiertos. | No |
24
33
  | `reflect-nudge.sh` | `Stop` | `.*` | Al cerrar el turno, sugiere `/build:reflect` si hay slice(s) archivado(s) sin reflexionar (`reflected != true`). | No |
25
34
  | `release-gate-nudge.sh` | `Stop` | `.*` | Al cerrar el turno, sugiere correr el Release Gate (`/build:release`) cuando hay ≥2 épicas archivadas sin auditar desde el último release. Determinista; solo sugiere. | No |
35
+ | `dual-compare.sh` | `Stop` | `.*` | (Beta, solo modo `dual`.) Detecta cuando la proyección del runtime diverge del fichero local (épica/fase/gates ya resueltos) y escala vía el mismo canal que `/build:escalate`. Es el instrumento de medición del piloto que condiciona el corte a `runtime` — nunca bloquea el cierre de sesión. | No |
36
+ | `session-stop.sh` | `Stop` | `.*` | (Beta.) Deja `outbox/.flush-request` y asegura el daemon `heartbeat.sh` — el flush de la cola offline lo ejecuta el daemon, no el hook. No-op en `legacy`. | No |
37
+ | `context-sync.sh` | invocado por `session-start.sh` y, pre-claim, por las skills (no registrado como hook) | — | (Beta.) Sincronización de contexto *content-addressed*: compara el `manifest_hash` esperado contra el lock local; si difiere, descarga solo los archivos gobernados (`config/`, `rules/`, `docs-cache/`) que cambiaron, por hash. Nunca escribe fuera de esos prefijos. Fail-open: sin runtime, sin `python3` o con manifiesto ilegible, deja el lock anterior intacto. | No |
38
+ | `heartbeat.sh` | daemon singleton por repo, lanzado por `session-start.sh` (no registrado como hook) | — | (Beta.) Renueva el lease (`PUT /leases/renew`) y despacha la cola offline mientras viva al menos una sesión interesada (`.claude/state/heartbeat-sessions.json`). No existe evento Timer en Claude Code; por eso el latido es un daemon, no un hook. |
26
39
 
27
- > Cuatro bloqueantes (`gitflow-guard`, `stack-guard`, `scaffold-guard`, `design-source-guard`) y nueve informativos. Los bloqueantes usan `exit 2`: Claude Code devuelve el `stderr` al modelo y aborta la herramienta; el resto siempre sale `0`.
40
+ > Cuatro bloqueantes (`gitflow-guard`, `stack-guard`, `scaffold-guard`, `design-source-guard`) y quince informativos. Los bloqueantes usan `exit 2`: Claude Code devuelve el `stderr` al modelo y aborta la herramienta; el resto siempre sale `0`. Los 4 bloqueantes **nunca** tocan la red (ni en `legacy` ni en `dual`/`runtime`) — ratchet estático en `scripts/tests/test-hooks-runtime.sh`.
28
41
  >
29
- > `lib/state-io.sh` no es un hook: es el **helper compartido** (`state_path`, `config_get`, `state_atomic_patch`) que usan `context-monitor.sh` y otros scripts para resolver la ruta del estado, leer `config/build-config.json` y escribir patches atómicos.
42
+ > `lib/state-io.sh` (`legacy`), `lib/config.sh` (`runtime.mode`, umbrales), `lib/runtime-client.sh`
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é,
45
+ > sin red) **no son hooks**: son los helpers compartidos que los scripts de arriba `source`an.
30
46
 
31
47
  ---
32
48
 
33
49
  ## Detalle por hook
34
50
 
35
- ### 1. `load-build-state.sh` — `SessionStart` · no bloqueante
51
+ ### 1. `session-start.sh` — `SessionStart` · no bloqueante (beta — EP-OR-08)
52
+
53
+ Primer hook del `SessionStart` (antes de `load-build-state.sh`). Si el modo (`runtime_mode`) es
54
+ `legacy`, no hace nada. Si es `dual`/`runtime`: registra el agente (`POST /agents/register`, con
55
+ `asset_types` leído de `.claude/asset-types.json` si existe), refresca `GET /agent/context` y
56
+ **normaliza** esa respuesta a la caché de proyección local (`lib/agent-context.sh` — la única pieza
57
+ que conoce la forma cruda de la respuesta del servidor), dispara `context-sync.sh` y asegura que el
58
+ daemon `heartbeat.sh` esté vivo. Nunca inyecta `additionalContext` directamente — eso lo hace
59
+ `load-build-state.sh`, que corre después en el mismo evento. Fail-open: un registro fallido no
60
+ aborta la sesión, solo deja el modo degradado a lo último sincronizado.
61
+
62
+ ### 2. `load-build-state.sh` — `SessionStart` · no bloqueante
36
63
 
37
64
  Se dispara al arrancar, limpiar o compactar la sesión (`matcher: startup|clear|compact`). Su trabajo:
38
65
 
@@ -43,7 +70,7 @@ Se dispara al arrancar, limpiar o compactar la sesión (`matcher: startup|clear|
43
70
 
44
71
  Es el hook que le da a Claude "memoria de obra" al empezar: sabe en qué historia/épica está, qué change de OpenSpec le corresponde y qué le falta cerrar.
45
72
 
46
- ### 2. `gitflow-guard.sh` — `PreToolUse` · Bash · **bloqueante**
73
+ ### 3. `gitflow-guard.sh` — `PreToolUse` · Bash · **bloqueante**
47
74
 
48
75
  Enforce **GitHub Flow estricto** antes de ejecutar cualquier comando Bash. Parsea el `tool_input.command` del JSON del hook y solo actúa sobre comandos `git` de escritura. Bloquea con `exit 2` en tres casos:
49
76
 
@@ -53,7 +80,7 @@ Enforce **GitHub Flow estricto** antes de ejecutar cualquier comando Bash. Parse
53
80
 
54
81
  Cualquier otro comando (incluido cualquier `git` que no sea commit/push) sale `0`. La política está alineada con el inner loop (la skill `building-a-slice` cierra cada slice con PR + archive).
55
82
 
56
- ### 3. `stack-guard.sh` — `PreToolUse` · Write/Edit/MultiEdit · **bloqueante**
83
+ ### 4. `stack-guard.sh` — `PreToolUse` · Write/Edit/MultiEdit · **bloqueante**
57
84
 
58
85
  Vigila el **contrato de stack** declarado en el PRD. Solo le importan las ediciones que tocan `package.json`. Compara las dependencias del contenido entrante (`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`) contra `config/stack-allowlist.json` —usando *globs* (`fnmatch`)— y **bloquea con `exit 2`** si aparece alguna dependencia fuera de la allowlist.
59
86
 
@@ -61,7 +88,7 @@ Vigila el **contrato de stack** declarado en el PRD. Solo le importan las edicio
61
88
  - El mensaje guía a actualizar `config/stack-allowlist.json` y dejar nota en `GOVERNANCE.md`, o a consultar al agente `stack-guardian`.
62
89
  - La allowlist es **artefacto del consumidor**: el CLI la siembra y `/build:onboard` la puebla a partir del PRD.
63
90
 
64
- ### 4. `lint-typecheck.sh` — `PostToolUse` · Write/Edit/MultiEdit · no bloqueante
91
+ ### 5. `lint-typecheck.sh` — `PostToolUse` · Write/Edit/MultiEdit · no bloqueante
65
92
 
66
93
  Tras editar un archivo `.ts`/`.tsx`, delega el estilo a las herramientas para no gastar tokens del modelo en formateo. Si los binarios existen en `node_modules/.bin/`, corre:
67
94
 
@@ -75,42 +102,97 @@ Tras editar un archivo `.ts`/`.tsx`, delega el estilo a las herramientas para no
75
102
 
76
103
  Nunca bloquea (siempre `exit 0`); reporta a `stderr` como información. Inerte si no hay `package.json` o si el archivo no es `.ts`/`.tsx`.
77
104
 
78
- ### 5. `coherence-flag.sh` — `PostToolUse` · Write/Edit/MultiEdit · no bloqueante
105
+ ### 6. `coherence-flag.sh` — `PostToolUse` · Write/Edit/MultiEdit · no bloqueante
79
106
 
80
107
  Cuando se edita un `openspec/changes/**/proposal.md`, imprime un recordatorio: correr el agente `change-epic-coherence` para validar el bloque `## Trazabilidad` (Épica `EP-XXX` / Historias `HU-XXX`) y ejecutar `openspec validate`. Es un *nudge* de coherencia change↔épica, no un control de bloqueo.
81
108
 
82
- ### 6. `build-gate-check.sh` — `Stop` · no bloqueante
109
+ ### 7. `event-emitter.sh` — `PostToolUse` · no bloqueante (beta — EP-OR-08)
110
+
111
+ Solo si el modo no es `legacy`: telemetría **como propiedad del harness**, imposible de omitir por
112
+ el modelo. Tras cada `Bash|Edit|Write|MultiEdit|Task`, encola un evento `tool_use_recorded` en
113
+ `.claude/state/outbox/` (nunca llama a la red en el hilo del hook) y hace la comprobación barata de
114
+ que el daemon `heartbeat.sh` esté vivo (`stat` del pidfile, sin red). Solo guarda metadatos —
115
+ herramienta, ruta relativa, `argv0` — **nunca** el comando completo ni el contenido editado. En
116
+ `legacy`, no-op.
117
+
118
+ ### 8. `build-gate-check.sh` — `Stop` · no bloqueante
83
119
 
84
120
  Al cerrar el turno, si hay un slice activo con gates en `false`, avisa por `stderr` qué gates quedan abiertos y recuerda **no archivar ni abrir PR** hasta cerrarlos (ver skill `building-a-slice` / `dod.md`). Inerte si no hay `package.json`, ni estado, ni `python3`.
85
121
 
86
- ### 7. `scaffold-guard.sh` — `PreToolUse` · Write/Edit/MultiEdit · **bloqueante** (desde v0.2.0)
122
+ ### 9. `scaffold-guard.sh` — `PreToolUse` · Write/Edit/MultiEdit · **bloqueante** (desde v0.2.0)
87
123
 
88
- Refuerza el **scaffold como "Paso 1 fundamental"**. Bloquea con `exit 2` la escritura de **código de slice** —cuando `active_slice.phase` ∈ `red`/`green`/`refactor`/`smoke`/`api`/`data`— mientras `scaffold.confirmed` no sea `true` en `build-state.json`. **Permite** crear el scaffold (sin slice activo, o en fases `dor`/`change`). El arnés **exige** el scaffold pero **no lo genera**; la confirmación es **explícita** (vía `building-a-slice` Fase 0 / `dor-dod-gatekeeper`), nunca auto-detectada. Guarda `python3` fail-closed dirigido (solo bloquea si la entrada parece código de slice).
124
+ Refuerza el **scaffold como "Paso 1 fundamental"**. Bloquea con `exit 2` la escritura de **código de slice** —cuando `active_slice.phase` ∈ `red`/`green`/`refactor`/`smoke`/`api`/`data`— mientras `scaffold.confirmed` no sea `true`. En `legacy` lo lee de `build-state.json`; en `dual`/`runtime`, de la caché de proyección local (`lib/projection.sh`, sin red — el guard sigue con latencia local). **Permite** crear el scaffold (sin slice activo, o en fases `dor`/`change`). El arnés **exige** el scaffold pero **no lo genera**; la confirmación es **explícita** (vía `building-a-slice` Fase 0 / `dor-dod-gatekeeper`), nunca auto-detectada. Guarda `python3` fail-closed dirigido (solo bloquea si la entrada parece código de slice).
89
125
 
90
- ### 8. `reflect-nudge.sh` — `Stop` · no bloqueante (desde v0.3.0)
126
+ ### 10. `reflect-nudge.sh` — `Stop` · no bloqueante (desde v0.3.0)
91
127
 
92
128
  Cierra el **ciclo autocorrectivo**. Al terminar el turno, si en `history[]` hay slice(s) archivado(s) con `reflected != true`, imprime un *nudge* sugiriendo ejecutar `/build:reflect` para capturar las convenciones aprendidas (y errores recurrentes) en el bloque `trycore-build-learnings` de `CLAUDE.md`. **Nunca bloquea** el cierre de sesión: si falta `python3` o el estado, sale `0` en silencio (**fail-open**). El razonamiento —qué se aprendió— vive en el comando `/build:reflect`, no en el hook; este solo recuerda. Tras reflexionar y estampar `reflected: true`, el nudge calla.
93
129
 
94
- ### 9. `design-source-guard.sh` — `PreToolUse` · Write/Edit/MultiEdit · **bloqueante** (desde v0.5.0)
130
+ ### 11. `design-source-guard.sh` — `PreToolUse` · Write/Edit/MultiEdit · **bloqueante** (desde v0.5.0)
95
131
 
96
- Refuerza el **seguro de fuente de diseño** (espejo de `scaffold-guard`, para slices con UI). Bloquea con `exit 2` la escritura de **código de un slice con UI** —cuando `active_slice.phase` ∈ `red`/`green`/`refactor`/`smoke`/`api`/`data` **y** `active_slice.gates.fidelity === false` (marcado UI-pendiente por la DoR)— mientras el proyecto tenga UI (`design_source.applies === true`) y `design_source.confirmed` no sea `true`. **Permite** todo lo demás: slices sin UI (`gates.fidelity === null` o ausente), proyectos sin UI (`applies !== true`), fases de planificación (`dor`/`change`), o fuente ya confirmada. El arnés **exige** la fuente de diseño y puede **generarla** vía `/build:prototype` (skill `prototyping-screens`; el mensaje de bloqueo lo sugiere); la confirmación sigue siendo **explícita y humana** (vía `building-a-slice` Fase 0-bis / `dor-dod-gatekeeper`), nunca auto-detectada. Guarda `python3` fail-closed dirigido. En la práctica, como `design_source` es gate de proyecto, solo muerde la **primera** construcción de UI sin fuente declarada.
132
+ Refuerza el **seguro de fuente de diseño** (espejo de `scaffold-guard`, para slices con UI). Bloquea con `exit 2` la escritura de **código de un slice con UI** —cuando `active_slice.phase` ∈ `red`/`green`/`refactor`/`smoke`/`api`/`data` **y** `active_slice.gates.fidelity === false` (marcado UI-pendiente por la DoR)— mientras el proyecto tenga UI (`design_source.applies === true`) y `design_source.confirmed` no sea `true`. **Permite** todo lo demás: slices sin UI (`gates.fidelity === null` o ausente), proyectos sin UI (`applies !== true`), fases de planificación (`dor`/`change`), o fuente ya confirmada. En `legacy` lo lee de `build-state.json`; en `dual`/`runtime`, de la caché de proyección local (sin red). El arnés **exige** la fuente de diseño y puede **generarla** vía `/build:prototype` (skill `prototyping-screens`; el mensaje de bloqueo lo sugiere); la confirmación sigue siendo **explícita y humana** (vía `building-a-slice` Fase 0-bis / `dor-dod-gatekeeper`), nunca auto-detectada. Guarda `python3` fail-closed dirigido. En la práctica, como `design_source` es gate de proyecto, solo muerde la **primera** construcción de UI sin fuente declarada.
97
133
 
98
- ### 10. `release-gate-nudge.sh` — `Stop` · no bloqueante (desde v0.7.0)
134
+ ### 12. `release-gate-nudge.sh` — `Stop` · no bloqueante (desde v0.7.0)
99
135
 
100
136
  Cierra el lazo del **outer loop**. Al terminar el turno, cuenta las épicas **archivadas** (`history[].epica`) que **ningún** release ha cubierto todavía (`releases[].epicas`); si quedan **≥2 épicas sin auditar**, imprime un *nudge* sugiriendo ejecutar `/build:release` (o la skill `releasing-a-version`) para correr los gates pesados **una sola vez** sobre el diff acumulado. Es puramente determinista: **aritmética de conjuntos** (archivadas − cubiertas) independiente de *timestamps*, sin consultar fechas ni el criterio de "cierre de línea de release" (eso lo computa la skill `building-a-slice` en su fase de cierre). **Nunca bloquea** el cierre, **nunca** ejecuta trabajo pesado, **nunca** llama al modelo ni escribe el estado; si falta `python3` o el estado, sale `0` en silencio (**fail-open**).
101
137
 
102
- ### 11. `statusline-bridge.sh` — comando `statusLine` · no bloqueante (desde v0.8.0)
138
+ ### 13. `dual-compare.sh` — `Stop` · no bloqueante (beta, solo modo `dual` — EP-OR-08)
139
+
140
+ El **comparador dual**: en modo `dual` el fichero es primario y cada transición se espeja al
141
+ servidor (`POST …/mirror/transitions`); este hook detecta cuando el **reducer del servidor**
142
+ llegó a una proyección distinta de lo que dice el fichero local — señal de un espejo rechazado o
143
+ perdido. Refresca la proyección (Stop no es sensible a latencia como PreToolUse) y compara épica,
144
+ fase y gates **ya resueltos** (nunca `null`=pendiente) entre ambos lados. Ante discrepancia, escala
145
+ por el mismo canal que `/build:escalate` — nunca inventa un evento nuevo, nunca bloquea el cierre
146
+ de sesión. Es el instrumento de medición que condiciona el corte a `runtime`: si un piloto real
147
+ corre en `dual` sin que este hook reporte nunca una discrepancia, se justifica apagar `legacy` (ver
148
+ `docs/runtime/plan-migracion-harness-v0.9.md` §2.3). En `legacy`/`runtime`, no-op.
149
+
150
+ ### 14. `session-stop.sh` — `Stop` · no bloqueante (beta — EP-OR-08)
151
+
152
+ Al cerrar el turno, si el modo no es `legacy`: deja el sentinela `outbox/.flush-request` y asegura
153
+ que el daemon `heartbeat.sh` esté vivo. El **flush real de la cola offline lo ejecuta el daemon**,
154
+ no este hook (un `Stop` no puede quedarse esperando una respuesta HTTP). No-op en `legacy`.
155
+
156
+ ### 15. `statusline-bridge.sh` — comando `statusLine` · no bloqueante (desde v0.8.0)
103
157
 
104
158
  Es el motor de contexto visto desde la barra de estado. No es un hook de evento: es el comando que Claude Code invoca para renderizar el `statusLine` (solo canal CLI; el plugin no puede inyectar `statusLine`). Lee el payload por `stdin`, calcula `remaining_pct` desde `context_window.remaining_percentage` y escribe atómicamente el archivo-puente `claude-ctx-<session_id>.json` (nombre de sesión saneado contra *path traversal*) para que `context-monitor.sh` lo consuma. Imprime siempre una línea (`🏗️ build` o `🏗️ build · ctx N%`); si falta `python3` o el payload no trae el dato, degrada sin romper la barra (**fail-open**).
105
159
 
106
- ### 12. `context-monitor.sh` — `PostToolUse` / `PreCompact` / `Stop` · no bloqueante (desde v0.8.0)
160
+ ### 16. `context-monitor.sh` — `PostToolUse` / `PreCompact` / `Stop` · no bloqueante (desde v0.8.0)
107
161
 
108
- Es la vigilancia de **presión de contexto**. Se dispara tras herramientas (`PostToolUse`, matcher `Bash|Edit|Write|MultiEdit|Task`), antes de compactar (`PreCompact`) y al cerrar el turno (`Stop`). Lee el puente que escribió `statusline-bridge.sh` (ignora lecturas más viejas que `context.stale_seconds`) y compara `remaining_pct` contra `context.warning_pct`/`context.critical_pct` (leídos vía `lib/state-io.sh#config_get`, con defaults 35/25). En `warning` inyecta `additionalContext` pidiendo buscar un punto de corte natural. En `critical`, y **una sola vez por sesión** (`active_slice.session_continuity.critical_recorded`), escribe el handoff en `build-state.json`: `stopped_at`, `resume_hint` (a partir de los items `failing` de `wiring_checklist`) y un hito en `progress_log[]`; si `context.auto_checkpoint` está activo, lo señala para continuar en sesión fresca sin pedir confirmación. **Nunca bloquea**; sin `python3`, sin puente, o con el puente desactualizado, sale `0` en silencio (**fail-open**).
162
+ Es la vigilancia de **presión de contexto**. Se dispara tras herramientas (`PostToolUse`, matcher `Bash|Edit|Write|MultiEdit|Task`), antes de compactar (`PreCompact`) y al cerrar el turno (`Stop`). Lee el puente que escribió `statusline-bridge.sh` (ignora lecturas más viejas que `context.stale_seconds`) y compara `remaining_pct` contra `context.warning_pct`/`context.critical_pct` (leídos vía `lib/state-io.sh#config_get`, con defaults 35/25). En `warning` inyecta `additionalContext` pidiendo buscar un punto de corte natural. En `critical`, y **una sola vez por sesión** (`active_slice.session_continuity.critical_recorded`), escribe el handoff: en `build-state.json` (`legacy`), como evento `handoff_recorded` (`runtime`), o ambos (`dual`) — `stopped_at`, `resume_hint` (a partir de los items `failing` de `wiring_checklist`) y un hito en `progress_log[]`; si `context.auto_checkpoint` está activo, lo señala para continuar en sesión fresca sin pedir confirmación. **Nunca bloquea**; sin `python3`, sin puente, o con el puente desactualizado, sale `0` en silencio (**fail-open**).
109
163
 
110
- ### 13. `reconcile-build-state.py` — `SessionStart` (invocado por `load-build-state.sh`) · no bloqueante (desde v0.8.0)
164
+ ### 17. `reconcile-build-state.py` — `SessionStart` (invocado por `load-build-state.sh`, solo `legacy`) · no bloqueante (desde v0.8.0)
111
165
 
112
166
  Es el único hook en Python puro (los demás son bash que delegan fragmentos a `python3`). No se cablea como entrada independiente en `settings.json`/`hooks/build-harness.json`: `load-build-state.sh` lo invoca al arrancar, **antes** de leer el estado, para "anclarlo a la realidad" de git y de los tests. Deriva, nunca lanza: (1) degrada a `failing` cualquier item de `wiring_checklist` marcado `passing` sin `evidence` no vacía (self-heal contra falsos positivos); (2) detecta *drift* entre la rama git real y `active_slice.branch` y lo **anota** en `branch_drift` (no lo corrige); (3) nunca revierte un gate booleano de `true` a `false` (ratchet: solo una señal explícita lo haría). Escribe atómico (`tempfile` + `os.replace`) solo si algo cambió. Fail-open: cualquier error de lectura o parseo devuelve `0` sin tocar el archivo.
113
167
 
168
+ ### 18. `context-sync.sh` — invocado por `session-start.sh` y las skills, no registrado (beta — EP-OR-08)
169
+
170
+ Sincronización de contexto **content-addressed** (`docs/runtime/protocolo-cliente-runtime.md` §4).
171
+ Compara el `manifest_hash` esperado (de `GET /agent/context` o el persistido en las credenciales)
172
+ contra el del lock local (`.claude/state/context.lock`); si coinciden, no hace nada (comparación de
173
+ strings, barata). Si difieren: pide el manifiesto, calcula el diff por `sha256` archivo a archivo y
174
+ descarga **solo** lo cambiado — pero únicamente hacia destinos gobernados (`config/`, `rules/`,
175
+ `docs-cache/`, siempre bajo `.claude/`); cualquier entrada absoluta, con `..` o fuera de esos
176
+ prefijos se rechaza sin escribir. El lock solo sella el hash nuevo si se aplicó el plan **completo**;
177
+ una convergencia parcial deja el lock en el hash anterior para que la próxima sesión reintente lo
178
+ que falta. Escritura atómica (`mkstemp` + `os.replace`). Fail-open total: sin runtime, sin
179
+ `python3` o con manifiesto ilegible, sale `0` dejando el último lock intacto — se sigue trabajando
180
+ con el contexto ya sincronizado.
181
+
182
+ ### 19. `heartbeat.sh` — daemon singleton por repo, lanzado por `session-start.sh`, no registrado (beta — EP-OR-08)
183
+
184
+ No existe evento Timer en Claude Code, los hooks son efímeros y un `PostToolUse` con throttle no
185
+ late durante una tool call larga — por eso el latido es un **daemon**, no un hook de evento.
186
+ **Singleton por repo**: `.claude/state/heartbeat.pid` + `.claude/state/heartbeat-sessions.json`
187
+ (ppids de las sesiones interesadas); el arranque se serializa con un mutex de directorio (`mkdir`,
188
+ atómico) con liberación por edad si queda huérfano. Cada `SessionStart` registra su ppid y relanza
189
+ si el pidfile está muerto; cada `PostToolUse` repite la comprobación barata (`stat` + `kill -0`, sin
190
+ red). **Muere** cuando no queda ningún ppid registrado vivo (dos sesiones sobre el mismo repo
191
+ comparten daemon; cerrar la primera no lo mata) — nunca se cuelga de `Stop` ni de `SessionEnd`. Por
192
+ tick (`TRYCORE_HEARTBEAT_TICK_S`, default 5s): consume `outbox/.flush-request` y despacha la cola;
193
+ cada `lease_ttl_s/3` (mínimo 10s), `PUT /leases/renew`. **Sin lease no se apaga**: los eventos de
194
+ ámbito proyecto (pre-claim) necesitan despachador igual.
195
+
114
196
  ---
115
197
 
116
198
  ## La cadena de comando única (sin doble disparo entre canales)
@@ -135,9 +217,9 @@ Como la cadena es **carácter por carácter idéntica** en ambos canales, si el
135
217
  Los hooks parsean el JSON del evento con `python3`. Qué pasa si **falta** `python3` depende de si el hook es bloqueante:
136
218
 
137
219
  - **Bloqueantes** (`gitflow-guard`, `stack-guard`, `scaffold-guard`, `design-source-guard`) → **fail-closed dirigido**: si no pueden analizar el comando/edición, solo bloquean (`exit 2`) cuando la entrada *cruda* parece relevante (un `git commit`/`push`, una edición que menciona `package.json`, o código de slice sin scaffold/fuente de diseño confirmados); en cualquier otro caso salen `0`. Esto evita falsos negativos peligrosos sin frenar el trabajo no relacionado. El mensaje pide instalar `python3` (verificable con `trycore-build doctor`).
138
- - **No bloqueantes** (`load-build-state`, `lint-typecheck`, `coherence-flag`, `build-gate-check`, `reflect-nudge`, `release-gate-nudge`) → si falta `python3`, simplemente **omiten** su trabajo y salen `0` (fail-open).
220
+ - **No bloqueantes** (`load-build-state`, `lint-typecheck`, `coherence-flag`, `build-gate-check`, `reflect-nudge`, `release-gate-nudge`, y los seis hooks de runtime — `session-start`, `event-emitter`, `context-sync`, `heartbeat`, `dual-compare`, `session-stop`) → si falta `python3`, simplemente **omiten** su trabajo y salen `0` (fail-open).
139
221
 
140
- > `python3` es un **requisito duro**: `trycore-build init` y `trycore-build doctor` **fallan** si no está presente, justo porque toda la cadena de hooks depende de él para leer el JSON del evento.
222
+ > `python3` es un **requisito duro**: `trycore-build init` y `trycore-build doctor` **fallan** si no está presente, justo porque toda la cadena de hooks depende de él para leer el JSON del evento (y, en modo runtime, para parsear las respuestas HTTP).
141
223
 
142
224
  ---
143
225
 
@@ -160,6 +242,11 @@ Los hooks que tocan el código construido se **auto-arman**: permanecen **inerte
160
242
 
161
243
  Así el arnés convive sin fricción con la fase de *discovery* y se "enciende" cuando empieza la construcción real.
162
244
 
245
+ > **Segundo gate, independiente**: los seis hooks de runtime (`session-start`, `event-emitter`,
246
+ > `context-sync`, `heartbeat`, `dual-compare`, `session-stop`) no los gobierna `package.json` sino
247
+ > `config/build-config.json#runtime.mode` — en `legacy` (default) son no-op o casi, sin importar la
248
+ > fase `authoring`/`active`. Los dos gates son ortogonales.
249
+
163
250
  ---
164
251
 
165
252
  ## Cómo se instalan
@@ -170,7 +257,7 @@ Una sola definición de hooks, expresada en dos archivos espejo según el canal:
170
257
 
171
258
  `trycore-build init` hace un **merge idempotente y aditivo** en el `settings.json` del consumidor (lógica en `src/lib/settings-merge.ts`; espejo documental en `templates/settings-hooks.template.json`):
172
259
 
173
- - Agrega las 5 agrupaciones de hooks (10 invocaciones: `PreToolUse·Write` agrupa `stack-guard` + `scaffold-guard` + `design-source-guard`; `PostToolUse·Write` agrupa `lint-typecheck` + `coherence-flag`; `Stop` agrupa `build-gate-check` + `reflect-nudge` + `release-gate-nudge`) sin pisar lo que ya exista. Ambos canales (CLI y plugin) cablean las **mismas 10 invocaciones**.
260
+ - Agrega las **7 agrupaciones** de hooks (**17 invocaciones**: `SessionStart` = `session-start` + `load-build-state`; `PreToolUse·Bash` = `gitflow-guard`; `PreToolUse·Write` = `stack-guard` + `scaffold-guard` + `design-source-guard`; `PostToolUse·Write` = `lint-typecheck` + `coherence-flag`; `PostToolUse·Bash|Edit|Write|MultiEdit|Task` = `context-monitor` + `event-emitter`; `PreCompact` = `context-monitor`; `Stop` = `build-gate-check` + `reflect-nudge` + `release-gate-nudge` + `dual-compare` + `context-monitor` + `session-stop`) sin pisar lo que ya exista. Ambos canales (CLI y plugin) cablean las **mismas 17 invocaciones** — un test de sincronización (`scripts/tests/test-hooks-runtime.sh`) compara los 3 ficheros contra un mapa objetivo y rompe si divergen.
174
261
  - Agrega **permisos mínimos y enumerados** (sin `mcp__*` ni rutas absolutas):
175
262
 
176
263
  ```
@@ -183,6 +270,6 @@ Una sola definición de hooks, expresada en dos archivos espejo según el canal:
183
270
 
184
271
  ### Canal plugin — `hooks/build-harness.json`
185
272
 
186
- El plugin declara los hooks en `hooks/build-harness.json` (referenciado desde `.claude-plugin/plugin.json` con `"hooks": "./hooks/build-harness.json"`) y usa la **misma cadena de comando**. Es el bloque más completo: su grupo `Stop` cablea las **3** invocaciones (`build-gate-check` + `reflect-nudge` + `release-gate-nudge`), por lo que el canal plugin suma las **10** invocaciones. La única diferencia con el merge del CLI es ese `release-gate-nudge.sh`; las otras 4 agrupaciones son idénticas. Si un consumidor instala ambos canales, las cadenas comunes se deduplican (ver arriba) y `release-gate-nudge.sh` lo aporta el plugin.
273
+ El plugin declara los hooks en `hooks/build-harness.json` (referenciado desde `.claude-plugin/plugin.json` con `"hooks": "./hooks/build-harness.json"`) y usa la **misma cadena de comando** y las **mismas 17 invocaciones** que el canal CLI no hay asimetría entre canales. Si un consumidor instala ambos, las cadenas idénticas se deduplican (ver arriba) y cada hook dispara una sola vez.
187
274
 
188
275
  > **Caveat de canales:** el canal CLI es el **canónico** para operar en un proyecto. El plugin namespacea los componentes bajo el nombre del plugin (`/trycore-spec-build-harness:*`) por diseño de Claude Code; las cross-references internas (skills que invocan `/opsx:*`, agentes por nombre) están escritas para el canal CLI. Los **hooks**, en cambio, son idénticos en ambos canales y se deduplican si coexisten.
@@ -0,0 +1,136 @@
1
+ # Guía práctica — modo legacy, dual y runtime (Agent Orchestrator Runtime)
2
+
3
+ > **Estado: beta, opt-in.** Nada de esto cambia el comportamiento de un proyecto que no lo activa
4
+ > explícitamente. `legacy` sigue siendo el default y el único camino con soporte completo hasta que
5
+ > un piloto real confirme el corte (`plan-migracion-harness-v0.9.md` §2). Esta guía es el **cómo**;
6
+ > el contrato de red completo vive en [`protocolo-cliente-runtime.md`](protocolo-cliente-runtime.md)
7
+ > y las reglas de gobierno en [`METODOLOGIA.md`](../../METODOLOGIA.md) §7-bis /
8
+ > [`GOVERNANCE.md`](../../GOVERNANCE.md).
9
+
10
+ ## 1. Los tres modos
11
+
12
+ El arnés opera el **mismo** pipeline de dos loops (DoR → change → TDD → smoke → DoD → PR, Release
13
+ Gate) en cualquiera de los tres. Lo que cambia es **dónde vive el estado** y **quién valida las
14
+ transiciones**:
15
+
16
+ | Modo | Fuente de verdad | Quién valida | Cuándo usarlo |
17
+ |---|---|---|---|
18
+ | **`legacy`** (default) | `.claude/state/build-state.json` (fichero local) | Honor system + hooks/agentes locales | Todo proyecto, hoy. Sin cambios. |
19
+ | **`dual`** | El **fichero** sigue siendo primario | Igual que legacy, **más** un espejo al servidor que se compara | Piloto: probar el runtime sin apostar el proyecto a él. |
20
+ | **`runtime`** | El **servidor** (Agent Orchestrator Runtime) | El servidor (reducer server-side) | Solo tras el corte confirmado (no disponible aún — ver §5). |
21
+
22
+ El modo lo decide `TRYCORE_RUNTIME_MODE` (variable de entorno) o, si no está seteada,
23
+ `config/build-config.json#runtime.mode`. Un valor inválido o mal escrito (typo, mayúscula) **cae
24
+ siempre a `legacy`** — nunca hacia la red por error.
25
+
26
+ ## 2. Requisitos antes de activar `dual`
27
+
28
+ - Un **Agent Orchestrator Runtime** corriendo y alcanzable por red (repo `trycore-ia-hub`) — esto
29
+ lo provee tu equipo de plataforma, no el arnés.
30
+ - Un **token de proyecto**, emitido por un **ADMIN** en la consola del hub (superficie humana; el
31
+ arnés no puede emitirse tokens a sí mismo).
32
+ - `python3` y `git` (ya son requisitos duros del arnés en cualquier modo).
33
+
34
+ ## 3. Activar `dual` en un proyecto nuevo (`init`)
35
+
36
+ ```bash
37
+ trycore-build init \
38
+ --runtime-url "https://tu-runtime.example.com" \
39
+ --runtime-token "<token-del-ADMIN>" \
40
+ --runtime-mode dual
41
+ ```
42
+
43
+ Qué hace, en orden, y **fail-open en cada paso** (un runtime inalcanzable durante `init` nunca
44
+ aborta la instalación; degrada a `legacy` y todo se reintenta en la primera sesión de Claude):
45
+
46
+ 1. Guarda las credenciales en `.claude/state/runtime.credentials` (`0600`, gitignored — nunca en
47
+ `settings.json` ni en el repo).
48
+ 2. Registra el agente (`POST /agents/register`) con la versión del arnés y los `asset_types` que
49
+ el paquete declara (`asset-types.json`, sembrado en `.claude/`).
50
+ 3. Escribe `runtime.mode: "dual"` en `.claude/config/build-config.json`.
51
+ 4. Dispara el primer sync de contexto (`context-sync.sh`) — trae `config/`, `rules/` y
52
+ `docs-cache/` gobernados desde el servidor.
53
+
54
+ Si `--runtime-mode` no se especifica pero sí `--runtime-url`/`--runtime-token`, el default es
55
+ `dual` (nunca `runtime` — el corte directo a `runtime` sin pasar por `dual` no está soportado por
56
+ diseño: es exactamente el "big bang" que el plan de migración evita).
57
+
58
+ ## 4. Activar `dual` en un proyecto ya instalado (`update`)
59
+
60
+ `trycore-build update` no acepta las flags de runtime directamente hoy — vuelve a correr `init` con
61
+ las mismas flags sobre el proyecto existente; es idempotente (no pisa `build-state.json` ni
62
+ `stack-allowlist.json`, solo añade las credenciales y el modo):
63
+
64
+ ```bash
65
+ trycore-build init --runtime-url "..." --runtime-token "..." --runtime-mode dual
66
+ ```
67
+
68
+ ## 5. Verificar que quedó bien
69
+
70
+ ```bash
71
+ trycore-build doctor # requisitos + sección "Runtime": token, conectividad, lock, cola offline
72
+ trycore-build status # resumen + sección "Runtime": conexión, proyección, contexto sincronizado
73
+ ```
74
+
75
+ Dentro de una sesión de Claude, `/build:status` da el mismo informe (más `next-step` derivado) y
76
+ `/build:claim [EP-XXX]` reclama la siguiente tarea directamente contra el runtime.
77
+
78
+ ## 6. Qué cambia en el día a día
79
+
80
+ **Nada que tengas que operar tú a mano.** Las skills (`building-a-slice`, `releasing-a-version`,
81
+ `managing-parallel-front`) conducen exactamente el mismo pipeline; por debajo, en vez de
82
+ leer/escribir `build-state.json` directamente, llaman a `slice-ops.sh`/`release-ops.sh` (13
83
+ subcomandos: `claim`, `gate`, `wiring`, `progress`, `checkpoint`, `submit`, `archive`, `fact`,
84
+ `propose-asset`, `status`, `escalate`, y del lado release `verdict`/`front-integration`) — nunca
85
+ arman peticiones a mano. En `dual`, cada transición también se **espeja** al servidor con la
86
+ identidad del fichero (que sigue mandando). Si el espejo falla (red caída, servidor rechaza), el
87
+ trabajo local **nunca se bloquea** — se avisa y queda como discrepancia para el comparador.
88
+
89
+ **El comparador dual** (`hooks/build/dual-compare.sh`, hook `Stop`) corre después de cada turno:
90
+ compara la proyección del servidor contra el fichero local (épica, fase, gates ya resueltos) y, si
91
+ divergen, lo escala vía `/build:escalate` — nunca bloquea el cierre de sesión. Es el instrumento
92
+ que mide si el piloto va "sin discrepancias" (§7).
93
+
94
+ ## 7. Migrar el estado histórico
95
+
96
+ Si el proyecto ya tiene slices archivados/releases en `build-state.json` y quieres que el runtime
97
+ los conozca (proyecciones consultables, no solo lo que ocurra de aquí en adelante):
98
+
99
+ ```bash
100
+ trycore-build migrate --project-ref "<nombre-del-proyecto-en-el-hub>"
101
+ # escribe .claude/state/migration-bundle.json
102
+ ```
103
+
104
+ Esto **normaliza y valida en local** — nunca sube nada. Entrega el fichero resultante a un ADMIN
105
+ con la instrucción: *«súbelo en la consola del hub, pantalla de import histórico del proyecto»*. El
106
+ import es idempotente y reanuda si falló a medias; las entradas no mapeables se importan igual como
107
+ `legacy_imported` con el payload original — nada se pierde, nada bloquea.
108
+
109
+ ## 8. Volver a `legacy` (rollback)
110
+
111
+ `dual` nunca dejó de escribir el fichero local — es la fuente primaria todo el tiempo. Para volver:
112
+
113
+ ```bash
114
+ # edita .claude/config/build-config.json → "runtime": { "mode": "legacy" }
115
+ # o, para una sesión puntual sin editar el fichero:
116
+ TRYCORE_RUNTIME_MODE=legacy claude
117
+ ```
118
+
119
+ No hay nada que deshacer del lado del fichero: nunca dejó de ser la verdad. Las credenciales
120
+ (`runtime.credentials`) pueden quedarse — en modo `legacy` el arnés no las toca.
121
+
122
+ ## 9. Cuándo se corta a `runtime` (y qué NO hacer todavía)
123
+
124
+ El modo `runtime` puro (servidor como única fuente, sin espejo local) y el **retiro físico** de la
125
+ maquinaria legacy (`build-state.schema.json`, `hooks/build/lib/state-io.sh`, `src/lib/state-seed.ts`)
126
+ están condicionados a que un **piloto real** corra en `dual` durante **1 sprint sin discrepancias**
127
+ reportadas por `dual-compare.sh` (`plan-migracion-harness-v0.9.md` §2.3). Hasta entonces:
128
+
129
+ - No apagues `legacy` en ningún proyecto real.
130
+ - No trates `build-state.json`/`state-io.sh` como deprecados ni los borres.
131
+ - No reescribas METODOLOGIA/CLAUDE.md/`state/README.md` como si el runtime fuera canónico — eso
132
+ es, literalmente, declarar que el piloto ya pasó (la publicación de `0.9.0` en npm con el
133
+ cliente en beta/opt-in **no** es esa declaración; es la que hace falta para que el piloto
134
+ arranque).
135
+
136
+ Este documento se actualizará cuando el piloto confirme el corte.
@@ -3,6 +3,17 @@
3
3
  > Repo: `trycore-spec-build-harness` (v0.8.1 → v0.9.0). Contraparte servidor: módulo `orchestrator` de **trycore-ia-hub** (PRD Agent Orchestrator Runtime v2.1, ADR-0012 del hub, épica EP-OR-08). Contrato de red: [protocolo-cliente-runtime.md](protocolo-cliente-runtime.md).
4
4
  >
5
5
  > **Principio rector:** la metodología no cambia — cambia el medio. Los dos loops, las fases, los gates y los veredictos son los mismos; lo que se moviliza es *dónde vive el estado* (del JSON local al runtime) y *quién valida las transiciones* (del prompt/honor system al servidor). Regla de corte: **hechos y transiciones de dominio → runtime · observación y enforcement del working tree → harness · protocolo de estado → cliente API.**
6
+ >
7
+ > **Estado del código (actualizado):** sub-slices **A** (cimiento de red), **B** (hooks cliente),
8
+ > **C** (skills/comandos sobre el runtime) y **D** (CLI `trycore-build`) — completos, mergeados en
9
+ > `main`. **E** (comparador dual + ratchets de la checklist §3, alcance acotado a lo aditivo/seguro)
10
+ > — completo, mergeado. **Publicado en npm como `0.9.0`** (beta/opt-in; `legacy` sigue siendo el
11
+ > default). **Lo que sigue pendiente y NO se ha hecho**: el piloto real en modo `dual` (§2, ítem
12
+ > 1-3 de abajo), el retiro físico de la maquinaria legacy y las enmiendas normativas de
13
+ > METODOLOGIA/CLAUDE.md/`state/README.md` que declararían el runtime canónico — todo eso depende
14
+ > de que el piloto corra un sprint sin discrepancias (§2.3) y será una release **posterior** a
15
+ > `0.9.0`, no esta. Guía de uso del código ya construido →
16
+ > [`guia-modo-dual-y-migracion.md`](guia-modo-dual-y-migracion.md).
6
17
 
7
18
  ## 1. Matriz de movilización por artefacto
8
19