@trycore/spec-build-harness 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/GOVERNANCE.md +3 -3
- package/METODOLOGIA.md +44 -0
- package/README.md +60 -5
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +6 -0
- package/commands/build/front.md +15 -0
- package/commands/build/resume.md +29 -0
- package/config/build-config.template.json +8 -0
- package/dist/commands/init.js +15 -5
- package/dist/commands/status.js +1 -0
- package/dist/commands/uninstall.js +2 -1
- package/dist/lib/paths.js +7 -0
- package/dist/lib/settings-merge.js +29 -2
- package/dist/lib/state-seed.js +14 -0
- package/docs/commands.md +15 -3
- package/docs/decisiones/2026-07-03-gsd-vs-openspec-fork-vs-rama.md +157 -0
- package/docs/hooks.md +22 -5
- package/hooks/build/build-gate-check.sh +1 -1
- package/hooks/build/context-monitor.sh +80 -0
- package/hooks/build/design-source-guard.sh +1 -1
- package/hooks/build/lib/state-io.sh +55 -0
- package/hooks/build/lint-typecheck.sh +1 -1
- package/hooks/build/load-build-state.sh +46 -2
- package/hooks/build/reconcile-build-state.py +70 -0
- package/hooks/build/reflect-nudge.sh +1 -1
- package/hooks/build/release-gate-nudge.sh +1 -1
- package/hooks/build/scaffold-guard.sh +1 -1
- package/hooks/build/stack-guard.sh +1 -1
- package/hooks/build/statusline-bridge.sh +32 -0
- package/hooks/build-harness.json +24 -0
- package/package.json +1 -1
- package/scripts/lib/front-plan.py +47 -0
- package/scripts/smoke-test.sh +12 -0
- package/scripts/tests/test-context-monitor.sh +106 -0
- package/scripts/tests/test-front-plan.sh +38 -0
- package/scripts/tests/test-install.sh +89 -0
- package/scripts/tests/test-reconciler.sh +54 -0
- package/scripts/tests/test-schema.sh +58 -0
- package/skills/building-a-slice/references/dor.md +2 -0
- package/skills/managing-parallel-front/SKILL.md +36 -0
- package/state/build-state.schema.json +50 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Evaluación: ¿reemplazar el harness OpenSpec por GSD? ¿fork o rama?
|
|
2
|
+
|
|
3
|
+
- **Fecha:** 2026-07-03
|
|
4
|
+
- **Estado:** Decidido — ver **Actualización post-análisis de código** al final. Diseño en [`docs/superpowers/specs/2026-07-03-harness-context-engine-design.md`](../superpowers/specs/2026-07-03-harness-context-engine-design.md)
|
|
5
|
+
- **Autor:** Jhonata.segura (asistido)
|
|
6
|
+
- **Repo evaluado:** `@trycore/spec-build-harness` v0.7.1
|
|
7
|
+
- **Candidato:** `open-gsd/gsd-core` (GSD — "Git. Ship. Done"), MIT
|
|
8
|
+
- **Insumo relacionado:** `gsd-template.md` (PRD "Orquestador Semántico Determinista", análisis de 4 agentes)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Pregunta a resolver
|
|
13
|
+
|
|
14
|
+
Dos preguntas, una depende de la otra:
|
|
15
|
+
|
|
16
|
+
1. **¿Debemos reemplazar OpenSpec por GSD** como base del harness de construcción, motivado por que "los foros dicen que GSD es más nativo con Claude Code, usa mejor el contexto y es más eficiente"?
|
|
17
|
+
2. **Si avanzamos, ¿fork de gsd-core o rama en este repo?**
|
|
18
|
+
|
|
19
|
+
Este documento responde ambas con evidencia y deja una recomendación. No modifica arquitectura.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Punto de partida: qué es hoy el harness y qué papel juega OpenSpec
|
|
24
|
+
|
|
25
|
+
`@trycore/spec-build-harness` no *es* OpenSpec: es un **arnés propio** que orquesta a Claude Code con arquitectura de **dos loops** (slice por épica con TDD + gates → release gate), **12 agentes de build** (coherence three-way, security-reviewer, wiring-adversarial-verifier, ux-krug, dor-dod-gatekeeper, stack-guardian, etc.), estado compartido, hooks y una **metodología documentada en español** (`METODOLOGIA.md`, `GOVERNANCE.md`). Es el compañero de `@trycore/spec-product-flow` (Discovery → Construcción).
|
|
26
|
+
|
|
27
|
+
OpenSpec (`@fission-ai/openspec`) es **una pieza dentro**, no el todo. Aporta el **modelo de spec basado en cambios**: `changes/` (propuesta) → implementación → `archive` hacia `specs/`. Se expresa como:
|
|
28
|
+
|
|
29
|
+
- **55 referencias** en el repo (`grep -ril openspec`).
|
|
30
|
+
- Namespace `/opsx:*` — 10 comandos (`new`, `apply`, `archive`, `bulk-archive`, `continue`, `explore`, `ff`, `onboard`, `sync`, `verify`).
|
|
31
|
+
- 8 skills `openspec-*` (todas con `Requires openspec CLI` y licencia MIT — son wrappers del CLI de OpenSpec).
|
|
32
|
+
- Dependencia externa: `npm install -g @fission-ai/openspec`.
|
|
33
|
+
|
|
34
|
+
**Conclusión de esta sección:** "reemplazar OpenSpec" ≠ "reemplazar el harness". OpenSpec es la **capa de gobernanza de spec** (propuesta de cambio → archivo). El valor diferencial de Trycore (dos loops, gates, agentes, metodología, coexistencia con product-flow) **no vive en OpenSpec**.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 3. Qué es realmente GSD (verificado, no de oídas)
|
|
39
|
+
|
|
40
|
+
GSD es un framework de **context-engineering + spec-driven** mucho **más grande y ambicioso** que nuestra capa OpenSpec:
|
|
41
|
+
|
|
42
|
+
- **Loop de 5 fases:** Discuss → Plan → Execute → Verify → Ship, repetido por *milestones*.
|
|
43
|
+
- **~55+ comandos** (`/gsd-*`): incluye planificación con convergencia cross-AI, ejecución en *waves* paralelas, *workstreams*, knowledge graph (`/gsd-graphify`), memory palace temporal (`/gsd-mempalace-*`), auditorías de milestone/UAT/seguridad, code-review, audit-fix, spikes, forensics, etc.
|
|
44
|
+
- **Estado en archivos planos:** `STATE.md`, `CONTEXT.md`, `ROADMAP.md`, `.planning/` — con comandos de validación (`state validate`, `roadmap validate`).
|
|
45
|
+
- **Multi-runtime:** Claude Code, Codex, Gemini CLI, Kimi, Copilot, Cursor, Windsurf… vía instalador (`npx @opengsd/gsd-core@latest`). **No se copian `agents/`/`commands/` a mano.**
|
|
46
|
+
- **Licencia MIT.** Fork legalmente trivial.
|
|
47
|
+
- **Escala/actividad:** ~48K estrellas; v1.34.2 el 2026-04-06; **1.693 commits en 47 releases** desde dic-2025. Es un proyecto que se mueve rápido.
|
|
48
|
+
|
|
49
|
+
### 3.1 ¿Son ciertas las afirmaciones de los foros?
|
|
50
|
+
|
|
51
|
+
| Afirmación de foro | Veredicto | Matiz |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| "Más nativo con Claude Code" | **Parcialmente cierto** | Nació Claude-Code-first y es lo más maduro ahí, pero hoy se posiciona **multi-runtime**. "Nativo" real = su patrón de *fresh-context subagents* encaja muy bien con el modelo de subagentes de Claude Code. |
|
|
54
|
+
| "Usa mejor el contexto" | **Cierto en el patrón** | El núcleo — ejecutar research/plan/execute en **subagentes de contexto fresco (~200k)** manteniendo la sesión principal ligera — es una respuesta genuinamente buena al *context rot*. Es la idea que la comunidad realmente elogia. |
|
|
55
|
+
| "Más eficiente" | **No demostrado como número** | El README no publica benchmarks de tokens. La eficiencia proviene del patrón, no de una métrica auditada. Ojo: fresh-context tiene su propio **"impuesto de arranque"** (reinyección) — es exactamente lo que critica tu PRD §2.2. |
|
|
56
|
+
|
|
57
|
+
**Lo importante:** lo que los foros elogian de GSD es un **patrón de ejecución** (subagentes de contexto fresco + estado externalizado), *no* un motor de spec que haga a OpenSpec obsoleto. Ese patrón es **adoptable sin reemplazar OpenSpec**.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 4. Solapamiento, brechas y choque de modelos
|
|
62
|
+
|
|
63
|
+
| Capacidad | Harness actual | GSD | Lectura |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| Gobernanza de spec | OpenSpec `changes/ → specs/` (brownfield, delta por cambio) | `ROADMAP.md` + fases/milestones (`.planning/`) | **Modelos distintos.** OpenSpec = delta de cambio archivable; GSD = roadmap de fases. Migrar es re-conceptualizar, no un swap. |
|
|
66
|
+
| Ejecución / context rot | Slices + orquestador propio | **Waves paralelas en subagentes frescos** | GSD es **más fuerte** aquí. Es la joya a mirar. |
|
|
67
|
+
| Gates de calidad | 12 agentes Trycore + release gate | code-review, audit-fix, secure-phase, verify | **Solapan.** Adoptar GSD nos obliga a re-hospedar o botar nuestros agentes. |
|
|
68
|
+
| TDD / Gitflow / release gate | **Explícito** (nuestro valor) | Ship/PR + verify; TDD/gitflow **no formalizados** | Perdemos rigor si migramos crudo. |
|
|
69
|
+
| Metodología en español + product-flow | **Sí** (diferencial Trycore) | No existe | Se re-implementa igual, migremos o no. |
|
|
70
|
+
| Extensibilidad | Nuestra, total | Vía instalador/capabilities/overlays; **no copiar archivos a mano** | Forkear = pelear contra su modelo de instalación. |
|
|
71
|
+
|
|
72
|
+
**Choque clave:** OpenSpec gobierna **el cambio** (delta propuesto y archivado); GSD gobierna **el plan** (fases de un roadmap). No son intercambiables 1:1; "reemplazar" implicaría rehacer cómo pensamos la unidad de trabajo.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 5. Riesgos de adoptar/forkear GSD
|
|
77
|
+
|
|
78
|
+
1. **Gobernanza inestable (riesgo alto).** El proyecto original tuvo un incidente (meme-coin) y **el autor original ya no participa**. Hoy conviven `open-gsd/gsd-core`, `open-gsd/get-shit-done-redux` y otros forks. Atarnos a un upstream en plena reorganización de gobernanza es riesgo de cadena de suministro y de dirección.
|
|
79
|
+
2. **Velocidad de upstream (riesgo de mantenimiento).** 47 releases / 1.693 commits en ~4 meses. Un **fork** nos obliga a un *sync* costoso y perpetuo, o a divergir y perder el motivo de forkear.
|
|
80
|
+
3. **Superficie enorme.** ~55 comandos y subsistemas (knowledge graph, memory palace, cross-AI). Adoptarlo entero es asumir complejidad que **no pediste** (YAGNI) y que solapa lo nuestro.
|
|
81
|
+
4. **Modelo de instalación cerrado.** "No copies `agents/`/`commands/`" → un fork/patch integrado con nuestro CLI (`trycore-build init`) va a contracorriente de su diseño.
|
|
82
|
+
5. **Contradice tu propio PRD.** El consenso de los 4 agentes en `gsd-template.md` es explícito: *"El Orquestador… no reemplaza a GSD ni a Claude Code — se inserta entre ellos"*. Es decir, tu análisis ya descartó "reemplazar".
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 6. Opciones de estrategia de repositorio
|
|
87
|
+
|
|
88
|
+
### Opción A — Rama en este repo (evolución in-place) ✅ recomendada para empezar
|
|
89
|
+
Crear `spike/gsd-eval` (y luego `feature/…`) **en este mismo repo**. Adoptar **el patrón que sí vale de GSD** (ejecución en subagentes de contexto fresco / waves) dentro de nuestra arquitectura, **conservando** OpenSpec como gobernanza de cambio y nuestros gates.
|
|
90
|
+
- **Pro:** control total, mantiene identidad Trycore y coexistencia con product-flow; reversible; publicable como v0.8/v1.0; cero riesgo de gobernanza externa.
|
|
91
|
+
- **Contra:** reimplementamos el patrón (no heredamos el código de GSD).
|
|
92
|
+
|
|
93
|
+
### Opción B — Fork de `gsd-core`
|
|
94
|
+
Partir de su base MIT y montar gates/metodología encima.
|
|
95
|
+
- **Pro:** heredamos madurez (waves, graph, memory palace).
|
|
96
|
+
- **Contra:** **el más caro y arriesgado** — sync perpetuo con upstream velocísimo, gobernanza inestable, superficie que no queremos, choque con nuestro CLI e instalación. Botamos o duplicamos nuestros 12 agentes y los dos loops.
|
|
97
|
+
|
|
98
|
+
### Opción C — GSD como dependencia (wrap, igual que hoy con OpenSpec)
|
|
99
|
+
No forkear: invocar el CLI de GSD desde nuestro harness, como orquestamos OpenSpec hoy.
|
|
100
|
+
- **Pro:** bajo mantenimiento; nos beneficiamos de upstream sin cargarlo.
|
|
101
|
+
- **Contra:** dependemos de su CLI/estado; dos modelos de spec conviviendo (OpenSpec + GSD) puede confundir; seguimos expuestos a su gobernanza en runtime.
|
|
102
|
+
|
|
103
|
+
### Opción D — Greenfield: el "Orquestador" del PRD (visión larga)
|
|
104
|
+
Construir el plano de control determinista + datos semánticos (MCP a nivel símbolo + estado tipado) que describe `gsd-template.md`. GSD queda como capa metodológica, no como reemplazo.
|
|
105
|
+
- **Pro:** ataca la métrica real (tokens/rework/first-time-pass) según tu propio análisis.
|
|
106
|
+
- **Contra:** mayor esfuerzo; fuera del alcance de "una versión nueva del harness" ahora.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 7. Recomendación
|
|
111
|
+
|
|
112
|
+
**No forkear gsd-core y no arrancar OpenSpec a ciegas.** En concreto:
|
|
113
|
+
|
|
114
|
+
1. **Reencuadrar el objetivo.** Lo que la comunidad elogia de GSD y lo que tú buscas ("mejor uso de contexto, más eficiente") es un **patrón de ejecución** (subagentes de contexto fresco + estado externalizado), **no** un motor de spec que reemplace a OpenSpec. Perseguir ese patrón ≠ reemplazar OpenSpec.
|
|
115
|
+
2. **Opción A (rama en este repo) para un spike acotado.** Validar el patrón de contexto fresco dentro de nuestra arquitectura, midiendo contra un baseline. Reversible y publicable.
|
|
116
|
+
3. **Descartar la Opción B (fork)** salvo que decidamos, con datos del spike, comprometer GSD como *motor* y aceptar el costo de mantenimiento/gobernanza. Hoy el riesgo de gobernanza (autor fuera, forks múltiples) lo hace desaconsejable.
|
|
117
|
+
4. **Mantener la Opción D (orquestador del PRD) como norte de largo plazo**, no como esta release. Coincide con el consenso de tu propio PRD: *insertar entre*, no reemplazar.
|
|
118
|
+
|
|
119
|
+
En una frase: **la próxima versión del harness debería robarle a GSD la *idea* (ejecución en contexto fresco), no el *repositorio*.**
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 8. Spike de validación propuesto (si se aprueba avanzar)
|
|
124
|
+
|
|
125
|
+
Rama `spike/gsd-eval`, alcance de días, criterios de éxito medibles:
|
|
126
|
+
|
|
127
|
+
1. **Baseline.** Correr un slice representativo con el harness actual; registrar tokens de entrada, nº de llamadas a herramientas, rework y first-time-pass.
|
|
128
|
+
2. **Variante A (patrón GSD, sin GSD).** Reimplementar la ejecución del slice en **subagentes de contexto fresco** (ya tenemos `Agent`/waves nativos de Claude Code) manteniendo OpenSpec + gates. Medir lo mismo.
|
|
129
|
+
3. **Variante B (GSD como dependencia, opcional).** Instalar `gsd-core` en un proyecto de prueba y comparar su Execute-phase contra nuestro slice, para calibrar cuánto valor real añade su motor vs. reimplementarlo.
|
|
130
|
+
4. **Decisión.** Con las tres mediciones, elegir entre: (a) adoptar solo el patrón (Opción A), (b) wrap de GSD (Opción C), o (c) invertir en el orquestador (Opción D). Sólo entonces se justificaría —o no— un fork.
|
|
131
|
+
|
|
132
|
+
**Criterio de descarte de fork:** si el spike no muestra ≥30% de mejora de tokens/rework atribuible al *código* de GSD (y no solo al patrón), no forkear.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 9. Fuentes
|
|
137
|
+
|
|
138
|
+
- [open-gsd/gsd-core (GitHub)](https://github.com/open-gsd/gsd-core)
|
|
139
|
+
- [GSD hits 48K stars — Augment Code](https://www.augmentcode.com/learn/gsd-stars-spec-driven-dev-claude-code)
|
|
140
|
+
- [The Anatomy of Claude Code Workflows (GSD deep dive) — codecentric](https://www.codecentric.de/en/knowledge-hub/blog/the-anatomy-of-claude-code-workflows-turning-slash-commands-into-an-ai-development-system)
|
|
141
|
+
- [Superpowers, GSD, and gstack: what each constrains — Ewan Mak (Medium)](https://medium.com/@tentenco/superpowers-gsd-and-gstack-what-each-claude-code-framework-actually-constrains-12a1560960ad)
|
|
142
|
+
- [GSD vs Spec Kit vs OpenSpec vs Taskmaster — Rick Hightower (Medium)](https://medium.com/@richardhightower/agentic-coding-gsd-vs-spec-kit-vs-openspec-vs-taskmaster-ai-where-sdd-tools-diverge-0414dcb97e46)
|
|
143
|
+
- Interno: `gsd-template.md` (PRD "Orquestador Semántico Determinista"), `README.md`, `METODOLOGIA.md`, skills `openspec-*`.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 10. Actualización post-análisis de código (2026-07-03)
|
|
148
|
+
|
|
149
|
+
Se clonó `gsd-core` y se leyó su ingeniería real (5 exploradores, evidencia `file:line`). Confirma y **cierra** las decisiones:
|
|
150
|
+
|
|
151
|
+
1. **NO forkear — confirmado con datos.** `git log --since=30d` = **861 commits**; sin campo `exports` (no es librería); artefactos compilados en `.gitignore`; instalador monolítico de **11.740 líneas**; 3 sistemas de toggles entrelazados. No hay costura estable para depender. Los patrones valiosos son portables (MIT) sin el motor.
|
|
152
|
+
2. **El dolor real (reinicio manual) NO lo resuelve GSD tampoco:** GSD deja el `/clear` manual (filosofía #884). Su valor = **aviso temprano (hook a 35%/25%) + handoff sin pérdida**. Eso es lo adoptable, y es pequeño/portable.
|
|
153
|
+
3. **Nuestro estado tipado ≥ el de GSD** (JSON schema vs Markdown+regex); GSD compensa con **derivación desde disco** — esa *idea* sí vale robarla.
|
|
154
|
+
4. **Worktrees:** GSD paraleliza solo lo disjunto en archivos + seguro en el DAG, con serializador de solapes. Adoptamos el patrón a nivel **inter-épica** (no fundacionales, disjuntas).
|
|
155
|
+
5. Lo *hypeado* (memory-palace, knowledge graph) es lo *menos* propio de GSD (MCP/Python externos). Descartado por ahora.
|
|
156
|
+
|
|
157
|
+
**Veredicto final:** rama en este repo (`feature/gsd-context-engine`), robar patrones de contexto + estado-en-disco + worktrees inter-épica. Diseño detallado en el spec enlazado arriba.
|
package/docs/hooks.md
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
# Hooks del arnés de construcción
|
|
2
2
|
|
|
3
|
-
Este documento describe los **
|
|
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.
|
|
4
4
|
|
|
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 el estado del build al iniciar la sesión, delegan estilo/typecheck a herramientas, 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.
|
|
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
7
|
---
|
|
8
8
|
|
|
9
|
-
## Resumen de los
|
|
9
|
+
## Resumen de los 13 hooks
|
|
10
10
|
|
|
11
11
|
| Hook | Evento | Matcher | Qué hace | ¿Bloqueante? |
|
|
12
12
|
|---|---|---|---|---|
|
|
13
|
-
| `load-build-state.sh` | `SessionStart` | `startup\|clear\|compact` | Inyecta al contexto la rama, la fase del arnés, el slice activo y los gates abiertos; sincroniza `harness_phase`. | No |
|
|
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 |
|
|
15
|
+
| `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 |
|
|
14
16
|
| `gitflow-guard.sh` | `PreToolUse` | `Bash` | Enforce GitHub Flow estricto sobre `git commit` / `git push`. | **Sí (exit 2)** |
|
|
15
17
|
| `stack-guard.sh` | `PreToolUse` | `Write\|Edit\|MultiEdit` | Bloquea dependencias en `package.json` fuera de la allowlist del stack del PRD. | **Sí (exit 2)** |
|
|
16
18
|
| `scaffold-guard.sh` | `PreToolUse` | `Write\|Edit\|MultiEdit` | Bloquea escribir código de slice (fases `red…data`) si el scaffold no está confirmado (`scaffold.confirmed`). | **Sí (exit 2)** |
|
|
17
19
|
| `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)** |
|
|
18
20
|
| `lint-typecheck.sh` | `PostToolUse` | `Write\|Edit\|MultiEdit` | Corre prettier/eslint/tsc sobre el archivo `.ts`/`.tsx` editado. | No |
|
|
19
21
|
| `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 |
|
|
20
23
|
| `build-gate-check.sh` | `Stop` | `.*` | Al cerrar el turno, avisa si el slice activo tiene gates abiertos. | No |
|
|
21
24
|
| `reflect-nudge.sh` | `Stop` | `.*` | Al cerrar el turno, sugiere `/build:reflect` si hay slice(s) archivado(s) sin reflexionar (`reflected != true`). | No |
|
|
22
25
|
| `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 |
|
|
23
26
|
|
|
24
|
-
> Cuatro bloqueantes (`gitflow-guard`, `stack-guard`, `scaffold-guard`, `design-source-guard`) y
|
|
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`.
|
|
28
|
+
>
|
|
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.
|
|
25
30
|
|
|
26
31
|
---
|
|
27
32
|
|
|
@@ -94,6 +99,18 @@ Refuerza el **seguro de fuente de diseño** (espejo de `scaffold-guard`, para sl
|
|
|
94
99
|
|
|
95
100
|
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**).
|
|
96
101
|
|
|
102
|
+
### 11. `statusline-bridge.sh` — comando `statusLine` · no bloqueante (desde v0.8.0)
|
|
103
|
+
|
|
104
|
+
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
|
+
|
|
106
|
+
### 12. `context-monitor.sh` — `PostToolUse` / `PreCompact` / `Stop` · no bloqueante (desde v0.8.0)
|
|
107
|
+
|
|
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**).
|
|
109
|
+
|
|
110
|
+
### 13. `reconcile-build-state.py` — `SessionStart` (invocado por `load-build-state.sh`) · no bloqueante (desde v0.8.0)
|
|
111
|
+
|
|
112
|
+
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
|
+
|
|
97
114
|
---
|
|
98
115
|
|
|
99
116
|
## La cadena de comando única (sin doble disparo entre canales)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# No bloquea: al cerrar el turno, avisa si hay un slice activo con gates abiertos.
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
7
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
7
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
8
8
|
[ -f "$ROOT/package.json" ] || exit 0
|
|
9
9
|
STATE="$ROOT/.claude/state/build-state.json"
|
|
10
10
|
[ -f "$STATE" ] || exit 0
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# context-monitor.sh — PostToolUse|PreCompact|Stop. Lee el puente, aplica umbrales,
|
|
3
|
+
# inyecta additionalContext y (en critical) escribe handoff. Fail-open.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
source "$HERE/lib/state-io.sh"
|
|
7
|
+
payload="$(cat)"; command -v python3 >/dev/null 2>&1 || exit 0
|
|
8
|
+
|
|
9
|
+
read -r SID EVT <<<"$(printf '%s' "$payload" | python3 -c '
|
|
10
|
+
import json,sys
|
|
11
|
+
try: p=json.load(sys.stdin)
|
|
12
|
+
except Exception: p={}
|
|
13
|
+
print(p.get("session_id","default"), p.get("hook_event_name","PostToolUse"))
|
|
14
|
+
' 2>/dev/null)"
|
|
15
|
+
SID="${SID:-default}"; EVT="${EVT:-PostToolUse}"
|
|
16
|
+
|
|
17
|
+
sanitized_sid="$(printf '%s' "$SID" | tr -c 'A-Za-z0-9_-' '_')"
|
|
18
|
+
BRIDGE="${TMPDIR:-/tmp}/claude-ctx-$sanitized_sid.json"
|
|
19
|
+
[ -f "$BRIDGE" ] || BRIDGE="$(command ls "${TMPDIR:-/tmp}"/claude-ctx-*.json 2>/dev/null | head -1)"
|
|
20
|
+
[ -n "${BRIDGE:-}" ] && [ -f "$BRIDGE" ] || exit 0
|
|
21
|
+
|
|
22
|
+
WARN="$(config_get context.warning_pct 35)"; CRIT="$(config_get context.critical_pct 25)"
|
|
23
|
+
STALE="$(config_get context.stale_seconds 60)"; AUTO="$(config_get context.auto_checkpoint false)"
|
|
24
|
+
STATE="$(state_path)"
|
|
25
|
+
|
|
26
|
+
python3 - "$BRIDGE" "$WARN" "$CRIT" "$STALE" "$EVT" "$STATE" "$AUTO" <<'PY' 2>/dev/null || true
|
|
27
|
+
import json,sys,os,time,tempfile
|
|
28
|
+
bridge,warn,crit,stale,evt,state,auto=sys.argv[1:8]
|
|
29
|
+
warn,crit,stale=int(warn),int(crit),int(stale)
|
|
30
|
+
try: b=json.load(open(bridge))
|
|
31
|
+
except Exception: sys.exit(0)
|
|
32
|
+
if time.time()-b.get("ts",0) > stale: sys.exit(0) # métrica vieja
|
|
33
|
+
rem=b.get("remaining_pct",100)
|
|
34
|
+
sev = "critical" if rem<=crit else ("warning" if rem<=warn else None)
|
|
35
|
+
if not sev: sys.exit(0)
|
|
36
|
+
|
|
37
|
+
# ¿slice activo? handoff solo en critical y una vez por sesión.
|
|
38
|
+
recorded=False
|
|
39
|
+
if sev=="critical" and os.path.exists(state):
|
|
40
|
+
try:
|
|
41
|
+
d=json.load(open(state)); s=d.get("active_slice")
|
|
42
|
+
if s:
|
|
43
|
+
sc=s.setdefault("session_continuity",{})
|
|
44
|
+
if not sc.get("critical_recorded"):
|
|
45
|
+
iso=time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime())
|
|
46
|
+
sc["stopped_at"]=f"context exhaustion at {rem}% ({iso})"
|
|
47
|
+
failing=[w["id"] for w in (s.get("wiring_checklist") or []) if w.get("status")=="failing"]
|
|
48
|
+
sc["resume_hint"]=("cablear: "+", ".join(failing[:6])) if failing else "revisar progreso y continuar"
|
|
49
|
+
sc["critical_recorded"]=True
|
|
50
|
+
sc["auto_continue"]=(auto=="true")
|
|
51
|
+
s.setdefault("progress_log",[]).append(
|
|
52
|
+
{"at":iso,"by":"context-monitor","note":f"handoff auto a {rem}% de contexto restante"})
|
|
53
|
+
dirn=os.path.dirname(state) or "."
|
|
54
|
+
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".build-state.",suffix=".tmp")
|
|
55
|
+
try:
|
|
56
|
+
with os.fdopen(fd,"w") as o: json.dump(d,o,indent=2,ensure_ascii=False); o.flush(); os.fsync(o.fileno())
|
|
57
|
+
os.replace(tmp,state); recorded=True
|
|
58
|
+
except Exception:
|
|
59
|
+
try: os.unlink(tmp)
|
|
60
|
+
except OSError: pass
|
|
61
|
+
raise
|
|
62
|
+
except Exception: pass
|
|
63
|
+
|
|
64
|
+
if sev=="warning":
|
|
65
|
+
msg=(f"⚠️ Contexto al {rem}% restante. Acércate a un punto natural de corte "
|
|
66
|
+
"(fin de fase/gate). No inicies trabajo complejo nuevo.")
|
|
67
|
+
else:
|
|
68
|
+
tail=(" Handoff escrito en build-state.json (session_continuity)." if recorded else "")
|
|
69
|
+
cont=(" auto_checkpoint=ON: continúa en sesión fresca." if auto=="true" else
|
|
70
|
+
" Avisa al usuario para reiniciar en un punto natural.")
|
|
71
|
+
msg=(f"🛑 Contexto CRÍTICO al {rem}% restante.{tail} El estado ya vive en build-state.json;"
|
|
72
|
+
f" no reescribas handoff manual.{cont}")
|
|
73
|
+
# En 'Stop', inyectar additionalContext RE-LANZA el turno (re-prompt): repetirlo en cada
|
|
74
|
+
# intento de cierre entra en bucle hasta el tope CLAUDE_CODE_STOP_HOOK_BLOCK_CAP (=9→override).
|
|
75
|
+
# Por eso en 'Stop' re-lanzamos como MUCHO una vez por sesión y solo en la TRANSICIÓN a crítico
|
|
76
|
+
# (recorded=True, el instante en que se graba el handoff). 'warning' nunca inyecta en 'Stop'
|
|
77
|
+
# (el nudge solo sirve mientras se trabaja). Fuera de 'Stop' se inyecta con normalidad.
|
|
78
|
+
if evt=="Stop" and not (sev=="critical" and recorded): sys.exit(0)
|
|
79
|
+
print(json.dumps({"hookSpecificOutput":{"hookEventName":evt,"additionalContext":msg}}))
|
|
80
|
+
PY
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# AUTO-ARME: si no existe build-state.json, exit 0.
|
|
9
9
|
set -uo pipefail
|
|
10
10
|
|
|
11
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
11
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
12
12
|
STATE="$ROOT/.claude/state/build-state.json"
|
|
13
13
|
[ -f "$STATE" ] || exit 0
|
|
14
14
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# state-io.sh — helpers compartidos: rutas, lectura de config y patch atómico del estado.
|
|
3
|
+
# Diseño: fail-open. Nunca lanza; ante fallo deja el archivo intacto.
|
|
4
|
+
|
|
5
|
+
state_path() {
|
|
6
|
+
# CLAUDE_PROJECT_DIR es la señal autoritativa que Claude Code fija en los hooks;
|
|
7
|
+
# git rev-parse solo aplica como fallback para invocación manual/dev (sin ese env).
|
|
8
|
+
local root; root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
9
|
+
echo "$root/.claude/state/build-state.json"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# config_get <clave.punteada> <default>
|
|
13
|
+
config_get() {
|
|
14
|
+
local key="$1" def="$2" file
|
|
15
|
+
file="${BUILD_CONFIG_FILE:-$(dirname "$(state_path)")/../config/build-config.json}"
|
|
16
|
+
command -v python3 >/dev/null 2>&1 || { echo "$def"; return; }
|
|
17
|
+
python3 - "$file" "$key" "$def" <<'PY' 2>/dev/null || echo "$def"
|
|
18
|
+
import json,sys
|
|
19
|
+
file,key,default=sys.argv[1],sys.argv[2],sys.argv[3]
|
|
20
|
+
try:
|
|
21
|
+
d=json.load(open(file))
|
|
22
|
+
for part in key.split("."): d=d[part]
|
|
23
|
+
print(d if not isinstance(d,bool) else str(d).lower())
|
|
24
|
+
except Exception:
|
|
25
|
+
print(default)
|
|
26
|
+
PY
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# state_atomic_patch <state-file> <expr-python-que-muta-`d`>
|
|
30
|
+
# Ejecuta la expresión con `d` = dict del estado; escribe atómico solo si válido.
|
|
31
|
+
state_atomic_patch() {
|
|
32
|
+
local file="$1" expr="$2"
|
|
33
|
+
[ -f "$file" ] || return 0
|
|
34
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
35
|
+
python3 - "$file" "$expr" <<'PY' 2>/dev/null || true
|
|
36
|
+
import json,sys,os,tempfile
|
|
37
|
+
path,expr=sys.argv[1],sys.argv[2]
|
|
38
|
+
try:
|
|
39
|
+
d=json.load(open(path))
|
|
40
|
+
if not isinstance(d,dict): sys.exit(0)
|
|
41
|
+
exec(expr, {}, {"d":d})
|
|
42
|
+
dirn=os.path.dirname(path) or "."
|
|
43
|
+
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".build-state.",suffix=".tmp")
|
|
44
|
+
try:
|
|
45
|
+
with os.fdopen(fd,"w") as out:
|
|
46
|
+
json.dump(d,out,indent=2,ensure_ascii=False); out.flush(); os.fsync(out.fileno())
|
|
47
|
+
os.replace(tmp,path)
|
|
48
|
+
except Exception:
|
|
49
|
+
try: os.unlink(tmp)
|
|
50
|
+
except OSError: pass
|
|
51
|
+
raise
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
PY
|
|
55
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# coste por edición no escale con el tamaño del repo.
|
|
9
9
|
set -uo pipefail
|
|
10
10
|
|
|
11
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
11
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
12
12
|
[ -f "$ROOT/package.json" ] || exit 0 # guard de auto-arme
|
|
13
13
|
|
|
14
14
|
# Guarda python3 [H4]: hook no-bloqueante; sin python3 no podemos extraer el archivo → omite.
|
|
@@ -3,14 +3,53 @@
|
|
|
3
3
|
# Inyecta al contexto: rama actual, harness_phase, slice activo y gates abiertos.
|
|
4
4
|
# Además sincroniza harness_phase (authoring -> active) según exista package.json.
|
|
5
5
|
set -uo pipefail
|
|
6
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/state-io.sh"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
# Lee el payload de SessionStart (JSON en stdin) para extraer session_id. Fail-open:
|
|
9
|
+
# si no hay stdin o no trae session_id, seguimos sin tocar session_continuity.
|
|
10
|
+
payload="$(cat 2>/dev/null || true)"
|
|
11
|
+
|
|
12
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
13
|
+
STATE="$(state_path)"
|
|
9
14
|
BRANCH="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'desconocida')"
|
|
10
15
|
|
|
11
16
|
# Determina la fase real del arnés.
|
|
12
17
|
if [ -f "$ROOT/package.json" ]; then PHASE="active"; else PHASE="authoring"; fi
|
|
13
18
|
|
|
19
|
+
# Ancla el estado a disco antes de leerlo (fail-open; nunca rompe SessionStart).
|
|
20
|
+
if [ -f "$STATE" ] && command -v python3 >/dev/null 2>&1; then
|
|
21
|
+
python3 "$(dirname "${BASH_SOURCE[0]}")/reconcile-build-state.py" "$STATE" 2>/dev/null || true
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Reset once-per-SESSION del guard de auto-handoff (A1): si esta sesión (session_id del
|
|
25
|
+
# payload de SessionStart) es distinta de la última registrada en session_continuity,
|
|
26
|
+
# reabrimos critical_recorded para que context-monitor.sh pueda volver a escribir handoff
|
|
27
|
+
# fresco en ESTA sesión. Fail-open: sin stdin/session_id/state, no se toca nada.
|
|
28
|
+
if [ -f "$STATE" ] && command -v python3 >/dev/null 2>&1; then
|
|
29
|
+
SESSION_ID="$(printf '%s' "$payload" | python3 -c '
|
|
30
|
+
import json,sys
|
|
31
|
+
try:
|
|
32
|
+
p=json.load(sys.stdin)
|
|
33
|
+
sid=p.get("session_id","")
|
|
34
|
+
print(sid if isinstance(sid,str) else "")
|
|
35
|
+
except Exception:
|
|
36
|
+
print("")
|
|
37
|
+
' 2>/dev/null || true)"
|
|
38
|
+
if [ -n "$SESSION_ID" ]; then
|
|
39
|
+
SID_JSON="$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$SESSION_ID" 2>/dev/null || true)"
|
|
40
|
+
if [ -n "$SID_JSON" ]; then
|
|
41
|
+
state_atomic_patch "$STATE" "
|
|
42
|
+
s = d.get('active_slice')
|
|
43
|
+
if isinstance(s, dict):
|
|
44
|
+
sc = s.setdefault('session_continuity', {})
|
|
45
|
+
if sc.get('last_session') != $SID_JSON:
|
|
46
|
+
sc['last_session'] = $SID_JSON
|
|
47
|
+
sc['critical_recorded'] = False
|
|
48
|
+
"
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
14
53
|
# Sincroniza harness_phase en el estado (si python3 disponible y el archivo existe).
|
|
15
54
|
# Escritura ATÓMICA + validada: este hook corre en CADA SessionStart (alta frecuencia,
|
|
16
55
|
# headless incluido); una escritura no atómica que se interrumpa truncaría la ÚNICA
|
|
@@ -79,6 +118,11 @@ else:
|
|
|
79
118
|
if log:
|
|
80
119
|
last=log[-1]
|
|
81
120
|
print(f" Última bitácora: [{last.get('by')}] {last.get('note')}")
|
|
121
|
+
sc=s.get("session_continuity") or {}
|
|
122
|
+
if sc.get("resume_hint"):
|
|
123
|
+
print(f" ▶️ Retomar: {sc.get('resume_hint')}" + (" [auto-continue]" if sc.get("auto_continue") else ""))
|
|
124
|
+
if s.get("branch_drift"):
|
|
125
|
+
print(f" ⚠️ Rama real ({s.get('branch_drift')}) != branch del slice ({s.get('branch')}).")
|
|
82
126
|
PY
|
|
83
127
|
fi
|
|
84
128
|
exit 0
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""reconcile-build-state.py — ancla el estado a la realidad (git + tests). Fail-open.
|
|
3
|
+
|
|
4
|
+
Deriva/reconcilia, nunca lanza:
|
|
5
|
+
- wiring_checklist: un item 'passing' sin 'evidence' se degrada a 'failing' (self-heal).
|
|
6
|
+
- branch_drift: si la rama git real != active_slice.branch, lo anota (no corrige).
|
|
7
|
+
- ratchet: no revierte gates booleanos true->false (solo señal explícita lo haría).
|
|
8
|
+
Uso: reconcile-build-state.py [<state-file>]
|
|
9
|
+
"""
|
|
10
|
+
import json, os, sys, subprocess, tempfile
|
|
11
|
+
|
|
12
|
+
def git_branch(root):
|
|
13
|
+
try:
|
|
14
|
+
return subprocess.run(["git","-C",root,"rev-parse","--abbrev-ref","HEAD"],
|
|
15
|
+
capture_output=True,text=True,timeout=5).stdout.strip() or None
|
|
16
|
+
except Exception:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
|
|
21
|
+
os.environ.get("CLAUDE_PROJECT_DIR",os.getcwd()), ".claude","state","build-state.json")
|
|
22
|
+
try:
|
|
23
|
+
with open(path) as fh:
|
|
24
|
+
d = json.load(fh)
|
|
25
|
+
except Exception:
|
|
26
|
+
return 0 # fail-open
|
|
27
|
+
if not isinstance(d, dict):
|
|
28
|
+
return 0
|
|
29
|
+
changed = False
|
|
30
|
+
s = d.get("active_slice")
|
|
31
|
+
if isinstance(s, dict):
|
|
32
|
+
try:
|
|
33
|
+
# 1) wiring: passing sin evidencia -> failing
|
|
34
|
+
wc = s.get("wiring_checklist")
|
|
35
|
+
wc = wc if isinstance(wc, list) else []
|
|
36
|
+
for w in wc:
|
|
37
|
+
if not isinstance(w, dict):
|
|
38
|
+
continue
|
|
39
|
+
if w.get("status") == "passing" and not (w.get("evidence") or "").strip():
|
|
40
|
+
w["status"] = "failing"; changed = True
|
|
41
|
+
# 2) branch drift
|
|
42
|
+
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(path))))
|
|
43
|
+
rb = git_branch(root)
|
|
44
|
+
if rb and s.get("branch") and rb != s["branch"] and rb not in ("HEAD",):
|
|
45
|
+
if s.get("branch_drift") != rb:
|
|
46
|
+
s["branch_drift"] = rb; changed = True
|
|
47
|
+
elif s.get("branch_drift") and rb == s.get("branch"):
|
|
48
|
+
s.pop("branch_drift", None); changed = True
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
if changed:
|
|
52
|
+
try:
|
|
53
|
+
dirn = os.path.dirname(path) or "."
|
|
54
|
+
fd, tmp = tempfile.mkstemp(dir=dirn, prefix=".build-state.", suffix=".tmp")
|
|
55
|
+
except Exception:
|
|
56
|
+
return 0
|
|
57
|
+
try:
|
|
58
|
+
with os.fdopen(fd,"w") as o:
|
|
59
|
+
json.dump(d,o,indent=2,ensure_ascii=False); o.flush(); os.fsync(o.fileno())
|
|
60
|
+
os.replace(tmp, path)
|
|
61
|
+
sys.stderr.write("reconcile: estado re-anclado a disco\n")
|
|
62
|
+
except Exception:
|
|
63
|
+
try:
|
|
64
|
+
os.unlink(tmp)
|
|
65
|
+
except OSError:
|
|
66
|
+
pass
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Determinista y barato: el razonamiento (qué se aprendió) lo hace el MODELO en /build:reflect.
|
|
6
6
|
set -uo pipefail
|
|
7
7
|
|
|
8
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
8
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
9
9
|
STATE="$ROOT/.claude/state/build-state.json"
|
|
10
10
|
[ -f "$STATE" ] || exit 0
|
|
11
11
|
command -v python3 >/dev/null 2>&1 || exit 0 # fail-open: jamás impide cerrar sesión
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# (lo computa la skill building-a-slice en la fase 8).
|
|
9
9
|
set -uo pipefail
|
|
10
10
|
|
|
11
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
11
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
12
12
|
STATE="$ROOT/.claude/state/build-state.json"
|
|
13
13
|
[ -f "$STATE" ] || exit 0
|
|
14
14
|
command -v python3 >/dev/null 2>&1 || exit 0 # fail-open: jamás impide cerrar sesión
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# AUTO-ARME: si no existe build-state.json, no hay nada que vigilar -> exit 0.
|
|
8
8
|
set -uo pipefail
|
|
9
9
|
|
|
10
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
10
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
11
11
|
STATE="$ROOT/.claude/state/build-state.json"
|
|
12
12
|
[ -f "$STATE" ] || exit 0
|
|
13
13
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# dependencias fuera de .claude/config/stack-allowlist.json (el contrato de stack del PRD).
|
|
6
6
|
set -uo pipefail
|
|
7
7
|
|
|
8
|
-
ROOT="$(git rev-parse --show-toplevel 2>/dev/null ||
|
|
8
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
9
9
|
ALLOW="$ROOT/.claude/config/stack-allowlist.json"
|
|
10
10
|
[ -f "$ALLOW" ] || exit 0
|
|
11
11
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# statusline-bridge.sh — statusLine command (canal CLI). Imprime la línea de estado
|
|
3
|
+
# y escribe el puente de contexto que lee context-monitor.sh. Fail-open siempre.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
payload="$(cat)"
|
|
6
|
+
command -v python3 >/dev/null 2>&1 || { echo "🏗️ build"; exit 0; }
|
|
7
|
+
echo "$payload" | python3 -c '
|
|
8
|
+
import json,sys,os,time,re,tempfile
|
|
9
|
+
try:
|
|
10
|
+
p=json.load(sys.stdin)
|
|
11
|
+
except Exception:
|
|
12
|
+
print("🏗️ build"); sys.exit(0)
|
|
13
|
+
sid=str(p.get("session_id","default"))
|
|
14
|
+
if re.search(r"[\\/]|\.\.", sid): sid=re.sub(r"[^A-Za-z0-9_-]","_",sid) # sanitiza path traversal
|
|
15
|
+
rem=None
|
|
16
|
+
try: rem=int(p.get("context_window",{}).get("remaining_percentage"))
|
|
17
|
+
except Exception: rem=None
|
|
18
|
+
if rem is not None:
|
|
19
|
+
d={"remaining_pct":rem,"used_pct":100-rem,"ts":int(time.time())}
|
|
20
|
+
path=os.path.join(tempfile.gettempdir(), f"claude-ctx-{sid}.json")
|
|
21
|
+
try:
|
|
22
|
+
fd,tmp=tempfile.mkstemp(dir=tempfile.gettempdir(),prefix=".ctx-",suffix=".tmp")
|
|
23
|
+
try:
|
|
24
|
+
with os.fdopen(fd,"w") as f: json.dump(d,f)
|
|
25
|
+
os.replace(tmp,path)
|
|
26
|
+
except Exception:
|
|
27
|
+
try: os.unlink(tmp)
|
|
28
|
+
except OSError: pass
|
|
29
|
+
except Exception: pass
|
|
30
|
+
tag = f"🏗️ build · ctx {rem}%" if rem is not None else "🏗️ build"
|
|
31
|
+
print(tag)
|
|
32
|
+
' 2>/dev/null || echo "🏗️ build"
|