@trycore/spec-build-harness 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/GOVERNANCE.md +1 -1
- package/INSTALL.md +1 -1
- package/METODOLOGIA.md +15 -4
- package/README.md +42 -6
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +3 -0
- package/commands/build/front.md +8 -4
- package/docs/commands.md +9 -1
- package/docs/getting-started.md +1 -1
- package/docs/hooks.md +4 -2
- package/docs/runtime/guia-modo-dual-y-migracion.md +44 -3
- package/docs/runtime/protocolo-cliente-runtime.md +10 -0
- package/hooks/build/lib/config.sh +56 -8
- package/hooks/build/lib/harness-meta.sh +72 -0
- package/hooks/build/lib/runtime-client.sh +65 -2
- package/hooks/build/lib/runtime-ops.sh +25 -0
- package/hooks/build/release-ops.sh +25 -4
- package/hooks/build/session-start.sh +5 -61
- package/hooks/build/slice-ops.sh +206 -0
- package/package.json +1 -1
- package/scripts/lib/front-plan.py +55 -6
- package/scripts/smoke-test.sh +1 -1
- package/scripts/tests/test-config.sh +40 -0
- package/scripts/tests/test-front-plan.sh +63 -0
- package/scripts/tests/test-worktree-identity.sh +207 -0
- package/skills/managing-parallel-front/SKILL.md +35 -8
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "trycore-spec-build-harness",
|
|
4
4
|
"displayName": "Trycore — Spec & Build Harness",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.16.0",
|
|
6
6
|
"description": "Arnés de construcción de dos loops (slice por épica + release gate) para Claude Code, con gates de calidad, estado compartido y OpenSpec. Compañero de @trycore/spec-product-flow. Agnóstico al proyecto.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Trycore",
|
package/GOVERNANCE.md
CHANGED
|
@@ -8,7 +8,7 @@ evoluciona con el modelo y el proyecto. No es "instalar y olvidar".
|
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| Contexto | sección Construcción de CLAUDE.md, `openspec/project.md` | raíz / `openspec/` |
|
|
10
10
|
| Estado (modo `legacy`, default) | `build-state.json` (+schema, README) | `.claude/state/` |
|
|
11
|
-
| Cliente runtime (modo `dual`/`runtime`, opt-in — beta) | `runtime.credentials` (0600), `context.lock`, `runtime-projection.json`, `outbox/`; `slice-ops.sh`/`release-ops.sh` (
|
|
11
|
+
| Cliente runtime (modo `dual`/`runtime`, opt-in — beta) | `runtime.credentials` (0600), `context.lock`, `runtime-projection.json`, `outbox/`; `slice-ops.sh`/`release-ops.sh` (20 + 3 subcomandos) | `.claude/state/`, `.claude/hooks/build/` |
|
|
12
12
|
| Agentes | 14 agentes de build | `.claude/agents/build/` |
|
|
13
13
|
| Hooks | settings.json + 19 scripts (15 registrados + 4 invocados: `reconcile-build-state.py`, `context-sync.sh`, `heartbeat.sh` como daemon, `statusline-bridge.sh` como comando `statusLine`) | `.claude/settings.json`, `.claude/hooks/build/` |
|
|
14
14
|
| Skill | `building-a-slice` (+11 refs · `workflows/`) · `releasing-a-version` (`workflows/`) · `building-a-micro-change` (carril ligero de mantenimiento) · `managing-parallel-front` (front paralelo inter-épica) · `prototyping-screens` (prototipo HTML de referencia) | `.claude/skills/` |
|
package/INSTALL.md
CHANGED
|
@@ -107,7 +107,7 @@ Qué hace `init`:
|
|
|
107
107
|
1. **Verifica requisitos duros** (a menos que uses `--skip-doctor`).
|
|
108
108
|
2. **Siembra los assets** en rutas nativas de Claude Code:
|
|
109
109
|
- `.claude/agents/build/` — 14 agentes.
|
|
110
|
-
- `.claude/commands/opsx/` (10 comandos `/opsx:*`) y `.claude/commands/build/` (
|
|
110
|
+
- `.claude/commands/opsx/` (10 comandos `/opsx:*`) y `.claude/commands/build/` (14 comandos: `/build:onboard`, `/build:reflect`, `/build:architect`, `/build:prototype`, `/build:slice`, `/build:release`, `/build:work`, `/build:resume`, `/build:front`, `/build:claim`, `/build:status`, `/build:escalate`, `/build:epic`, `/build:graph-sync`). Los cinco últimos son la **superficie de agente del modo runtime** (§9): en `legacy`, `claim`, `escalate`, `epic` y `graph-sync` devuelven `rc 3` remitiendo al flujo del fichero, y `status` informa de que la fuente de verdad es `build-state.json`.
|
|
111
111
|
- `.claude/skills/` — 16 skills (`building-a-slice`, `building-a-micro-change`, `releasing-a-version`, `managing-parallel-front`, `setup-architecture`, `prototyping-screens`, `openspec-*`).
|
|
112
112
|
- `.claude/hooks/build/` — 19 hooks (bash + python; 6 son del cliente runtime opt-in — §9).
|
|
113
113
|
3. **Siembra el estado**: `state/build-state.schema.json` y `state/README.md` se versionan;
|
package/METODOLOGIA.md
CHANGED
|
@@ -332,15 +332,26 @@ worktree— y coordina la selección, la construcción y el merge desde el estad
|
|
|
332
332
|
(`parallel_front`). Es un concepto de **outer-loop**: no es una fase nueva del pipeline por-épica, es
|
|
333
333
|
orquestación **entre** épicas.
|
|
334
334
|
|
|
335
|
+
**Compuerta cero: un agente por worktree, un token por agente.** En modo `runtime`, cada worktree
|
|
336
|
+
se da de alta como **agente propio** con un token que el ADMIN emitió **para él**
|
|
337
|
+
(`slice-ops.sh register`, que lo pide por stdin). No es un trámite: el hub deriva el `agent_key`
|
|
338
|
+
del `agent_id` grabado en el token, así que dos worktrees con el mismo token son **un solo agente**
|
|
339
|
+
y el segundo `claim` devuelve el slice del primero. Y un worktree **sin** credenciales propias no
|
|
340
|
+
queda huérfano — hereda las del clon principal y **suplanta** a ese agente. Por eso toda escritura
|
|
341
|
+
desde un árbol sin identidad propia aborta con `rc 9` (issue #74).
|
|
342
|
+
|
|
335
343
|
**Compuertas del front:**
|
|
336
344
|
|
|
337
345
|
1. **Foundational-first (G1).** Ninguna épica `layer: foundational` entra jamás al front: el
|
|
338
346
|
cimiento se construye secuencial, antes que el negocio (§1-bis.2, §3.1). Solo épicas
|
|
339
347
|
`layer: business` son candidatas a paralelizarse.
|
|
340
|
-
2. **Disjunción por `files_scope` (G2).** `scripts/lib/front-plan.py` calcula, a
|
|
341
|
-
declarados en `files_scope` de cada épica candidata, qué subconjunto es
|
|
342
|
-
(`selected`, va al front) y cuál se solapa (`serialized`, espera y se
|
|
343
|
-
secuencial). Sin `files_scope` declarado, una épica no es candidata al
|
|
348
|
+
2. **Disjunción por `files_scope` y por el grafo (G2).** `scripts/lib/front-plan.py` calcula, a
|
|
349
|
+
partir de los globs declarados en `files_scope` de cada épica candidata, qué subconjunto es
|
|
350
|
+
mutuamente disjunto (`selected`, va al front) y cuál se solapa (`serialized`, espera y se
|
|
351
|
+
construye después, secuencial). Sin `files_scope` declarado, una épica **no es candidata** al
|
|
352
|
+
front: el script la serializa (fail-closed). Tampoco son paralelizables dos épicas unidas por
|
|
353
|
+
un camino en el DAG de `depends_on`, aunque sus ficheros sean disjuntos — la de abajo
|
|
354
|
+
construiría sobre lo que la de arriba todavía no ha mergeado.
|
|
344
355
|
3. **Merge en orden + re-smoke (G3).** El merge de los worktrees sigue un `merge_order`
|
|
345
356
|
determinista; tras **cada** merge se re-corre el `journey_smoke` completo sobre el árbol
|
|
346
357
|
principal (no basta con el smoke local del worktree). Un solape no capturado por la disjunción
|
package/README.md
CHANGED
|
@@ -121,14 +121,21 @@ Cuando hay **≥ 2 épicas de negocio (`layer: business`)** listas (DoR pasado)
|
|
|
121
121
|
archivos**, el comando **`/build:front`** (skill `managing-parallel-front`) las construye en
|
|
122
122
|
paralelo, cada una en su propio `git worktree`/rama/PR — el inner loop no cambia: cada worktree
|
|
123
123
|
mantiene su `active_slice` singular y su propio `build-state.json`. Tres compuertas gobiernan el
|
|
124
|
-
front:
|
|
124
|
+
front (más una compuerta cero, de identidad, cuando se corre contra el hub):
|
|
125
125
|
|
|
126
|
+
0. **Un agente por worktree, un token por agente** (modo `runtime`). Cada árbol se da de alta con
|
|
127
|
+
`slice-ops.sh register` y un token que el ADMIN emitió **para él**: el hub deriva el `agent_key`
|
|
128
|
+
del token, así que dos worktrees con el mismo token son un solo agente. Un árbol sin dar de alta
|
|
129
|
+
hereda las credenciales del clon principal y lo suplantaría — por eso toda escritura desde él
|
|
130
|
+
aborta con `rc 9`.
|
|
126
131
|
1. **Foundational-first.** Ninguna épica `layer: foundational` entra jamás al front; si una aparece
|
|
127
132
|
mientras el front está activo, el front pasa a `status: "draining"` (termina lo que ya empezó, no
|
|
128
133
|
admite épicas nuevas) hasta vaciarse.
|
|
129
|
-
2. **Disjunción por `files_scope
|
|
130
|
-
declarados en cada épica, qué candidatas son mutuamente disjuntas
|
|
131
|
-
esperar (`serialized`) por solaparse.
|
|
134
|
+
2. **Disjunción por `files_scope` y por el grafo.** `scripts/lib/front-plan.py` calcula, por
|
|
135
|
+
conjuntos de globs declarados en cada épica, qué candidatas son mutuamente disjuntas
|
|
136
|
+
(`selected`) y cuáles deben esperar (`serialized`) por solaparse. Sin `files_scope` declarado una
|
|
137
|
+
épica **no es candidata** (se serializa), y dos épicas unidas por un camino en el DAG de
|
|
138
|
+
`depends_on` tampoco van juntas aunque sus ficheros sean disjuntos.
|
|
132
139
|
3. **Merge en orden + re-smoke.** El merge de los worktrees sigue un `merge_order` determinista; tras
|
|
133
140
|
cada merge se re-corre el `journey_smoke` completo en el árbol principal. Un conflicto no
|
|
134
141
|
detectado por la disjunción declarada lo caza el re-smoke: la épica perdedora se serializa
|
|
@@ -144,7 +151,7 @@ trycore-spec-build-harness/
|
|
|
144
151
|
├── agents/build/ ← 14 agentes revisores (segunda opinión, contexto limpio)
|
|
145
152
|
├── commands/
|
|
146
153
|
│ ├── opsx/ ← 10 comandos /opsx:* (ciclo OpenSpec)
|
|
147
|
-
│ └── build/ ←
|
|
154
|
+
│ └── build/ ← 14 comandos /build:* (onboard, reflect, architect, prototype, slice, release, work, resume, front, claim, status, escalate, epic, graph-sync)
|
|
148
155
|
├── skills/ ← 16 skills (building-a-slice, building-a-micro-change, releasing-a-version, managing-parallel-front, setup-architecture, prototyping-screens, 10 openspec-*) + 3 plantillas *.workflow.js (opt-in, read-only)
|
|
149
156
|
├── hooks/build/ ← 19 hooks (gate-check, reflect-nudge, release-gate-nudge, scaffold-guard, gitflow-guard, stack-guard, statusline-bridge, context-monitor, reconcile-build-state, …) + 6 opt-in del cliente runtime (session-start, event-emitter, context-sync, heartbeat, dual-compare, session-stop — ver docs/hooks.md)
|
|
150
157
|
├── state/ ← máquina de estado legacy: build-state.json + schema + README
|
|
@@ -184,7 +191,36 @@ El core no menciona ningún dominio de cliente. Toda parametrización entra por
|
|
|
184
191
|
|
|
185
192
|
## Roadmap
|
|
186
193
|
|
|
187
|
-
- ✅ **v0.
|
|
194
|
+
- ✅ **v0.16.0 (actual) — identidad por árbol de trabajo** (#74) — construir dos épicas en paralelo
|
|
195
|
+
en la misma máquina, una por `git worktree`, ya es una operación segura. El reparto nunca fue el
|
|
196
|
+
problema del hub —ya entrega dos épicas a dos agentes sin objeción—: el bloqueo estaba en el
|
|
197
|
+
cliente. El estado del arnés está en `.gitignore` y no viaja a un worktree, así que el árbol nuevo
|
|
198
|
+
**heredaba en silencio** las credenciales del clon principal y lo suplantaba: reclamaba con su
|
|
199
|
+
`agent_key`, reportaba los sha del árbol equivocado (cegando la detección de drift justo en el
|
|
200
|
+
escenario paralelo) y compartía su heartbeat. Ahora toda **escritura** desde un árbol sin dar de
|
|
201
|
+
alta aborta con **`rc 9`** antes de abrir un socket (las lecturas siguen, y `status` avisa con
|
|
202
|
+
`identidad: ⚠ HEREDADA`), y el alta es explícita: **`slice-ops.sh register`**, con el token por
|
|
203
|
+
**stdin** —nunca por argumento—, siembra local antes de tocar la red, verificación de que el
|
|
204
|
+
`agent_key` **difiera** del principal y rollback total ante cualquier fallo. Dato que corrige la
|
|
205
|
+
documentación anterior: `POST /agents/register` **no crea identidad** —el `agent_key` es el
|
|
206
|
+
`agent_id` grabado en el token por el ADMIN—, así que es **un token por agente**, no uno por
|
|
207
|
+
proyecto. Además, `front-plan.py` cierra tres fallos silenciosos: `layer` normalizado (el bundle
|
|
208
|
+
del grafo emite `FOUNDATIONAL` y la exclusión foundational-first podía no disparar),
|
|
209
|
+
**fail-closed** sin `files_scope` declarado, y serialización de los pares con camino en el DAG de
|
|
210
|
+
`depends_on`.
|
|
211
|
+
- ✅ **v0.15.0 — el arnés propone el re-sync de su grafo** (EP-OR-17) — el grafo del hub se sembraba
|
|
212
|
+
una vez, con el import de admin de `/build:onboard`, y ahí se quedaba: las épicas que discovery
|
|
213
|
+
escribía después no tenían por dónde entrar y **todos** los eventos de un slice cuya épica falta
|
|
214
|
+
rebotan. Comando **`/build:graph-sync`** (+ `--check`) y subcomandos `graph-sync`/
|
|
215
|
+
`graph-sync-status`: el arnés construye el bundle desde los docs, el hub calcula el delta y un
|
|
216
|
+
**humano aprueba** en la consola — nada se aplica sin esa aprobación; tras ella, las historias
|
|
217
|
+
nuevas entran solas al reparto del claim. `client_event_id` determinista para que el reintento
|
|
218
|
+
deduplique en vez de dejar dos propuestas del mismo grafo esperando. En el camino, dos defectos
|
|
219
|
+
vivos desde antes: el `409` se leía en la raíz y con un nombre que no existe en el backend (el
|
|
220
|
+
claim no reintentaba ante grafo rancio ni ante drift), y el **espejo del modo `dual` era un 422 el
|
|
221
|
+
100 % de las veces** (mandaba un cuerpo que el contrato no tiene). Total: **14 comandos
|
|
222
|
+
`/build:*`**.
|
|
223
|
+
- ✅ **v0.14.0 — contexto vivo, épicas propuestas y versión de grafo** (#61, #62, #63) —
|
|
188
224
|
tres capacidades del cliente del runtime que componen entre sí. **(1)** El daemon de heartbeat
|
|
189
225
|
refresca `GET /agent/context` con **cadencia propia** (`runtime.context_refresh_s`, default 45 s,
|
|
190
226
|
`0` la desactiva): antes la caché se hidrataba al arrancar la sesión y en cada `claim` —y el
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.16.0
|
|
@@ -23,6 +23,9 @@ Eres el único que reporta gates y transiciona fases; los reviewers solo emiten
|
|
|
23
23
|
## Coexistencia con el front paralelo (outer-loop)
|
|
24
24
|
Si hay un front paralelo abierto, el paralelismo lo gobierna la skill
|
|
25
25
|
`managing-parallel-front`; cada worktree corre su propio inner loop con `active_slice` singular.
|
|
26
|
+
En modo `runtime`, cada worktree es además un **agente propio** con su token del ADMIN (alta con
|
|
27
|
+
`slice-ops.sh register`): si un árbol no está dado de alta, sus escrituras salen con `rc 9` porque
|
|
28
|
+
estaría operando con la identidad del clon principal — eso se arregla registrando, no reintentando.
|
|
26
29
|
**Regla de drenado:** si se necesita abrir una épica `layer=foundational`, primero pon
|
|
27
30
|
`parallel_front.status="draining"` (termina las en curso, no admite nuevas) y espera a cerrarlo.
|
|
28
31
|
|
package/commands/build/front.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "BUILD: Front"
|
|
3
|
-
description: Prepara y coordina un front paralelo de épicas NO fundacionales y disjuntas en archivos, cada una en su worktree/rama/PR con su propio agente registrado. Delega en la skill managing-parallel-front (selección disjunta local vía scripts/lib/front-plan.py, worktrees, merge en orden con re-smoke reportado por cada worktree). Planificar, abrir, drenar y cerrar el front son actos humanos en la consola.
|
|
3
|
+
description: Prepara y coordina un front paralelo de épicas NO fundacionales y disjuntas en archivos, cada una en su worktree/rama/PR con su propio agente registrado (un token por agente, alta con slice-ops.sh register). Delega en la skill managing-parallel-front (selección disjunta local vía scripts/lib/front-plan.py, worktrees, merge en orden con re-smoke reportado por cada worktree). Planificar, abrir, drenar y cerrar el front son actos humanos en la consola.
|
|
4
4
|
category: Workflow
|
|
5
5
|
tags: [build-harness, outer-loop, front-paralelo, trycore]
|
|
6
6
|
---
|
|
@@ -10,11 +10,15 @@ tags: [build-harness, outer-loop, front-paralelo, trycore]
|
|
|
10
10
|
Delega en la skill **managing-parallel-front**. Resumen:
|
|
11
11
|
1. Verifica precondiciones (scaffold confirmado; sin épica foundational abierta) con
|
|
12
12
|
`bash .claude/hooks/build/slice-ops.sh status`.
|
|
13
|
-
2. Reúne candidatas no fundacionales listas (DoR pasado) con `layer` y
|
|
13
|
+
2. Reúne candidatas no fundacionales listas (DoR pasado) con `layer`, `files_scope` y
|
|
14
|
+
`depends_on`. Sin `files_scope` declarado la épica **no es candidata** (se serializa).
|
|
14
15
|
3. Propón el conjunto disjunto y el `merge_order` (`scripts/lib/front-plan.py`, local) y
|
|
15
16
|
**preséntaselo a una persona**: abrir el front es acto de gobierno, en la consola del hub.
|
|
16
|
-
4. Un worktree por épica aprobada, cada uno
|
|
17
|
-
|
|
17
|
+
4. Un worktree por épica aprobada, cada uno dado de alta como **agente propio** con **su
|
|
18
|
+
propio token** emitido por el ADMIN: `trycore-build init` (sin `--runtime-token`) y luego
|
|
19
|
+
`bash .claude/hooks/build/slice-ops.sh register` (pide el token por stdin). Sin ese alta,
|
|
20
|
+
toda escritura desde el worktree sale con **rc 9**: estaría suplantando al clon principal.
|
|
21
|
+
Después, construye con `/build:slice`.
|
|
18
22
|
5. Tras cada merge, re-smoke y reporte desde el worktree:
|
|
19
23
|
`bash .claude/hooks/build/release-ops.sh front-integration <front_id> --merge-status … --resmoke …`.
|
|
20
24
|
|
package/docs/commands.md
CHANGED
|
@@ -121,7 +121,7 @@ El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y
|
|
|
121
121
|
|
|
122
122
|
| Slash command | Propósito |
|
|
123
123
|
|---|---|
|
|
124
|
-
| `/build:front` | Abre y coordina un **front paralelo** de épicas NO fundacionales y disjuntas en archivos, cada una en su propio worktree/rama/PR. Delega en la skill `managing-parallel-front`: verifica precondiciones (scaffold confirmado, sin épica foundational abierta), selecciona el conjunto disjunto (`scripts/lib/front-plan.py`) y mergea en orden con re-smoke. Úsalo solo con ≥2 épicas no fundacionales disjuntas listas; para una sola épica, usa `/build:slice`. |
|
|
124
|
+
| `/build:front` | Abre y coordina un **front paralelo** de épicas NO fundacionales y disjuntas en archivos, cada una en su propio worktree/rama/PR. Delega en la skill `managing-parallel-front`: verifica precondiciones (scaffold confirmado, sin épica foundational abierta), selecciona el conjunto disjunto (`scripts/lib/front-plan.py`, que serializa también las candidatas sin `files_scope` declarado y los pares con camino en el DAG de `depends_on`) y mergea en orden con re-smoke. En modo `runtime`, **cada worktree se da de alta como agente propio** con su token del ADMIN (`slice-ops.sh register`): sin ese alta, toda escritura desde el árbol sale con `rc 9` porque estaría firmando con la identidad del clon principal. Úsalo solo con ≥2 épicas no fundacionales disjuntas listas; para una sola épica, usa `/build:slice`. |
|
|
125
125
|
|
|
126
126
|
### `/build:prototype` — prototipo HTML de referencia (fuente de diseño)
|
|
127
127
|
|
|
@@ -134,6 +134,14 @@ El canal CLI namespacea por subcarpeta: `.claude/commands/opsx/` → `/opsx:*` y
|
|
|
134
134
|
Adaptadores delgados sobre `slice-ops.sh`; en modo `legacy` (default) devuelven `rc 3` y remiten al
|
|
135
135
|
protocolo del fichero (`/build:slice`) — no rompen el flujo normal si el proyecto no está migrado.
|
|
136
136
|
|
|
137
|
+
> **Un árbol de trabajo, un agente.** Estos comandos operan con la identidad de
|
|
138
|
+
> `.claude/state/runtime.credentials`. Ese fichero está en `.gitignore` y **no viaja** a un
|
|
139
|
+
> `git worktree`, así que un árbol nuevo hereda la del clon principal y lo suplantaría: por eso
|
|
140
|
+
> toda **escritura** desde un árbol sin dar de alta aborta con **`rc 9`**. El alta no es un slash
|
|
141
|
+
> command —pide el token por stdin y necesita un terminal humano—: se corre una vez por worktree
|
|
142
|
+
> con `bash .claude/hooks/build/slice-ops.sh register`. Ver
|
|
143
|
+
> [guía runtime → «Varios agentes en la misma máquina»](runtime/guia-modo-dual-y-migracion.md).
|
|
144
|
+
|
|
137
145
|
| Slash command | Propósito |
|
|
138
146
|
|---|---|
|
|
139
147
|
| `/build:claim` | Pide la siguiente tarea al runtime (`POST /tasks/next`): sincroniza contexto, reporta hashes locales (detección de drift) y reclama por lease. Si trae `CHECKPOINT`, continúa desde ahí — **nunca reinicia** un slice de otro agente. **No acepta épica dirigida**: el hub reparte por orden de cola y `claim --epic` falla explícito con `rc 2` (issue #39 — el servidor ignoraba el `epic_code` y entregaba otra épica como si fuera la pedida, dejando un lease huérfano). |
|
package/docs/getting-started.md
CHANGED
|
@@ -56,7 +56,7 @@ npm i -g @fission-ai/openspec @trycore/spec-build-harness
|
|
|
56
56
|
|
|
57
57
|
## 2 · `trycore-build init` (terminal)
|
|
58
58
|
|
|
59
|
-
Desde la raíz de tu proyecto. Es **idempotente** (re-correrlo es seguro) y siembra: **14 agentes**, **16 skills** (`building-a-slice`, `building-a-micro-change`, `releasing-a-version`, `managing-parallel-front`, `setup-architecture`, `prototyping-screens` + 10 `openspec-*`), **10 comandos `/opsx:*`** + **
|
|
59
|
+
Desde la raíz de tu proyecto. Es **idempotente** (re-correrlo es seguro) y siembra: **14 agentes**, **16 skills** (`building-a-slice`, `building-a-micro-change`, `releasing-a-version`, `managing-parallel-front`, `setup-architecture`, `prototyping-screens` + 10 `openspec-*`), **10 comandos `/opsx:*`** + **14 comandos `/build:*`** (`onboard`, `reflect`, `architect`, `prototype`, `slice`, `release`, `work`, `resume`, `front` + `claim`, `status`, `escalate`, `epic`, `graph-sync`, que son la superficie del modo runtime), **19 hooks**, el estado vacío `.claude/state/build-state.json` (gitignored, nunca se sobreescribe) y el bloque `<!-- BEGIN trycore-build-harness -->` en tu `CLAUDE.md` con `{{placeholders}}` sin resolver.
|
|
60
60
|
|
|
61
61
|
> **¿Ya tenías el arnés instalado?** Entonces esto es un `update`, no un `init` — el CLI lo detecta solo por `.claude/.build-harness-version`. Corre `npm i -g @trycore/spec-build-harness@latest` y luego `trycore-build update`; **no** repitas `/build:onboard`, tu dominio ya está parametrizado. La tabla «¿Qué camino me toca?» de [`INSTALL.md`](../INSTALL.md) cubre los cuatro casos.
|
|
62
62
|
|
package/docs/hooks.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hooks del arnés de construcción
|
|
2
2
|
|
|
3
|
-
Este documento describe los **19 hooks** que `@trycore/spec-build-harness` instala en el proyecto del consumidor. Todos viven en `hooks/build/` (un script bash o python por hook, más los helpers compartidos `lib/state-io.sh`, `lib/config.sh`, `lib/runtime-client.sh`, `lib/runtime-ops.sh`, `lib/agent-context.sh`, `lib/projection.sh`) y se cablean a los eventos de Claude Code mediante una **cadena de comando única**, idéntica en ambos canales de instalación.
|
|
3
|
+
Este documento describe los **19 hooks** que `@trycore/spec-build-harness` instala en el proyecto del consumidor. Todos viven en `hooks/build/` (un script bash o python por hook, más los helpers compartidos `lib/state-io.sh`, `lib/config.sh`, `lib/runtime-client.sh`, `lib/runtime-ops.sh`, `lib/agent-context.sh`, `lib/projection.sh`, `lib/harness-meta.sh`) y se cablean a los eventos de Claude Code mediante una **cadena de comando única**, idéntica en ambos canales de instalación.
|
|
4
4
|
|
|
5
5
|
Los hooks son el sistema nervioso del arnés: vigilan GitFlow, el stack declarado, el scaffold y la fuente de diseño (bloqueantes), inyectan y re-anclan el estado del build al iniciar la sesión, delegan estilo/typecheck a herramientas, vigilan la presión de contexto y escriben handoff automático, y recuerdan validar trazabilidad, gates abiertos, reflexionar al cerrar un slice y correr el Release Gate cuando se acumulan épicas sin auditar. No reemplazan a los agentes ni a las skills; los **complementan** liberando capacidad de razonamiento del modelo y poniendo barandillas mecánicas donde un olvido cuesta caro.
|
|
6
6
|
|
|
@@ -41,7 +41,9 @@ Los hooks son el sistema nervioso del arnés: vigilan GitFlow, el stack declarad
|
|
|
41
41
|
>
|
|
42
42
|
> `lib/state-io.sh` (`legacy`), `lib/config.sh` (`runtime.mode`, umbrales), `lib/runtime-client.sh`
|
|
43
43
|
> (cliente HTTP + cola offline), `lib/runtime-ops.sh` (helpers de `slice-ops.sh`/`release-ops.sh`),
|
|
44
|
-
> `lib/agent-context.sh` (normaliza `GET /agent/context`)
|
|
44
|
+
> `lib/agent-context.sh` (normaliza `GET /agent/context`), `lib/harness-meta.sh` (versión del arnés
|
|
45
|
+
> y catálogo de asset types, compartidos por el registro de sesión y el alta de un worktree) y
|
|
46
|
+
> `lib/projection.sh` (lee esa caché,
|
|
45
47
|
> sin red) **no son hooks**: son los helpers compartidos que los scripts de arriba `source`an.
|
|
46
48
|
|
|
47
49
|
---
|
|
@@ -120,12 +120,13 @@ arman peticiones a mano. En `dual`, cada transición también se **espeja** al s
|
|
|
120
120
|
identidad del fichero (que sigue mandando). Si el espejo falla (red caída, servidor rechaza), el
|
|
121
121
|
trabajo local **nunca se bloquea** — se avisa y queda como discrepancia para el comparador.
|
|
122
122
|
|
|
123
|
-
`slice-ops.sh` tiene **
|
|
124
|
-
leer un log o depurar, no para que los teclees):
|
|
123
|
+
`slice-ops.sh` tiene **20 subcomandos** (los conducen las skills; los listamos para que puedas
|
|
124
|
+
leer un log o depurar, no para que los teclees — `register` es la excepción: ese lo tecleas tú):
|
|
125
125
|
|
|
126
126
|
| Subcomando | Para qué |
|
|
127
127
|
|---|---|
|
|
128
128
|
| `mode` | imprime `legacy`\|`dual`\|`runtime` |
|
|
129
|
+
| `register` | da de alta **este árbol de trabajo** como agente propio, con su token (lo pide por stdin). Solo hace falta en un `git worktree` — ver «Varios agentes en la misma máquina» |
|
|
129
130
|
| `claim` | reclama trabajo (el hub reparte por orden de cola; sin épica dirigida) |
|
|
130
131
|
| `next-step` | deriva la siguiente acción desde la caché de proyección |
|
|
131
132
|
| `phase` | reporta la fase del pipeline (emite la cadena que falte; idempotente) |
|
|
@@ -142,7 +143,47 @@ leer un log o depurar, no para que los teclees):
|
|
|
142
143
|
Del lado release, `release-ops.sh` añade `verdict` y `front-integration`.
|
|
143
144
|
|
|
144
145
|
**Códigos de salida** (sobre estos ramifica la prosa de las skills): `0` ok · `2` uso · `3` legacy ·
|
|
145
|
-
`4` sin slice · `5` offline · `6` rechazado · `7` sin trabajo · `8` tipo no espejable
|
|
146
|
+
`4` sin slice · `5` offline · `6` rechazado · `7` sin trabajo · `8` tipo no espejable ·
|
|
147
|
+
`9` identidad heredada (este árbol no está dado de alta — ver abajo).
|
|
148
|
+
|
|
149
|
+
### Varios agentes en la misma máquina (0.16.0)
|
|
150
|
+
|
|
151
|
+
Si abres un `git worktree` por épica para construir en paralelo (`/build:front`), **cada árbol
|
|
152
|
+
necesita su propio token de agente**, emitido por el ADMIN para él. No es burocracia:
|
|
153
|
+
|
|
154
|
+
- El hub **no crea identidad** al registrar. El `agent_key` es el `agent_id` que va grabado en el
|
|
155
|
+
token, así que dos árboles con el mismo token son **un solo agente** — y el segundo `claim` te
|
|
156
|
+
devuelve el slice que ya tiene reclamado el primero.
|
|
157
|
+
- El estado del arnés está en `.gitignore` y **no viaja** al worktree. Sin credenciales propias, el
|
|
158
|
+
árbol nuevo hereda las del clon principal y **suplanta** a ese agente: reclamaría con su
|
|
159
|
+
`agent_key`, reportaría los hashes del árbol equivocado y compartiría su heartbeat.
|
|
160
|
+
|
|
161
|
+
Por eso, en un árbol sin dar de alta, **toda escritura** (`claim`, `phase`, `gate`, `submit`,
|
|
162
|
+
`archive`, `wiring`, `progress`, `checkpoint`, `fact`, `propose-*`, `graph-sync`, `escalate` y
|
|
163
|
+
`release-ops.sh front-integration`) aborta con `rc 9` **antes de tocar la red**. Las lecturas siguen
|
|
164
|
+
funcionando —son el diagnóstico de ese árbol— y `status` lo dice en su primera línea:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
identidad: ⚠ HEREDADA del clon principal (/ruta/al/clon) — solo lectura
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
El alta es un comando, en el terminal, una vez por worktree:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
cd .wt/ep-003
|
|
174
|
+
trycore-build init # tiende .claude/ (sin --runtime-token)
|
|
175
|
+
bash .claude/hooks/build/slice-ops.sh register # pide el token por stdin
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`register` **no acepta el token por argumento** (`--token` sale con `rc 2`): en argv quedaría en el
|
|
179
|
+
historial del shell y en `ps`. Por lo mismo lo corres tú y no un hook — las tool calls no tienen
|
|
180
|
+
TTY. Antes de tocar la red siembra el fichero local (si no, el registro habría escrito sobre las
|
|
181
|
+
credenciales del clon principal), y después verifica contra el hub que el `agent_key` obtenido
|
|
182
|
+
**difiere** del suyo; si coincide, lo rechaza y te dice que pidas al ADMIN un token con `agent_id`
|
|
183
|
+
distinto. Ante cualquier fallo revierte: el árbol queda con identidad propia y verificada, o
|
|
184
|
+
exactamente como estaba. El fichero del clon principal no se toca nunca.
|
|
185
|
+
|
|
186
|
+
Para rotar el token de un árbol ya registrado: `register --force`.
|
|
146
187
|
|
|
147
188
|
### Qué espeja el modo `dual` — y qué no (0.15.0)
|
|
148
189
|
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
- **Token de proyecto:** emitido por un ADMIN en el hub, entregado al desarrollador, configurado una vez con `trycore-build init --runtime-url --runtime-token` (se guarda en `.claude/state/runtime.credentials`, `0600`, sembrado en `.gitignore` por `init`). Todas las llamadas: `Authorization: Bearer <token>`.
|
|
10
10
|
- **Registro:** `POST /agents/register {harness_version, asset_types}` → el servidor responde `{agent_key, project_id, harness_version, poll_interval_s, lease_ttl_s, manifest_hash, catalog{…}}`; el cliente (`runtime_register`) persiste hoy solo `agent_key`, `project_id`, `poll_interval_s`, `lease_ttl_s` y `manifest_hash` — `catalog` (tipos de asset) todavía no se persiste (el cliente ya envía `asset_types` desde `.claude/asset-types.json`, pero no consume el `catalog` de vuelta), `harness_version` no se re-persiste (ya se conoce localmente). Los intervalos los dicta el servidor (el cliente no los hardcodea). `409 incompatible_version` ⇒ mensaje claro con la versión mínima requerida (cuerpo `{error, min_version}`).
|
|
11
11
|
- **Identidad local:** `agent_key`/`project_id` persisten en `.claude/state/runtime.credentials` (`0600`, protegido en `.gitignore` desde el primer `init`); un mismo clon re-registrado reactiva su agente (no crea otro): `runtime_register` hace upsert sobre el fichero existente, nunca lo recrea desde cero.
|
|
12
|
+
- **Un token = un agente.** `POST /agents/register` **no crea identidad**: el `agent_key` es el `agent_id` que el ADMIN grabó en el token al emitirlo. Dos árboles con el mismo token son **el mismo agente** para el hub, y el segundo `claim` devuelve el slice que ya tiene reclamado el primero. Para dos agentes hacen falta **dos tokens**.
|
|
13
|
+
- **Identidad por árbol de trabajo (`git worktree`, issue #74).** El estado del arnés está en `.gitignore` y **no viaja** a un `git worktree add`. `config_root()` cae entonces al clon principal (`--git-common-dir`) para que el árbol nuevo no quede huérfano de contexto — pero ese fallback vale para **leer**, no para **escribir**: sin credenciales propias, el worktree firmaría cada acto con el `agent_key` del clon principal, reportaría los hashes del árbol equivocado y compartiría su heartbeat. Por eso:
|
|
14
|
+
- Toda operación de **escritura** (`slice-ops.sh claim|phase|gate|submit|archive|wiring|progress|checkpoint|fact|propose-asset|propose-epic|epic-writeback|graph-sync|escalate` y `release-ops.sh front-integration`) aborta con **rc 9** antes de tocar la red si la identidad es heredada.
|
|
15
|
+
- Las **lecturas** (`mode`, `next-step`, `status`, `epic-status`, `graph-sync-status`) conservan el fallback: son el diagnóstico del árbol que aún no se dio de alta. `status` lo encabeza con `identidad: ⚠ HEREDADA del clon principal … — solo lectura`.
|
|
16
|
+
- El alta es explícita: **`slice-ops.sh register`** (§10). Con ella, `ops_context_hashes`, el heartbeat, la outbox y los ledgers pasan a ser por-worktree solos — todos cuelgan de `config_root()`.
|
|
12
17
|
|
|
13
18
|
## 2. Mapeo hook → endpoint (la telemetría es del harness, no del modelo)
|
|
14
19
|
|
|
@@ -215,6 +220,7 @@ modo, valida en local lo que es barato validar y encola lo que no pudo entregar.
|
|
|
215
220
|
| Comando | Superficie |
|
|
216
221
|
|---|---|
|
|
217
222
|
| `slice-ops.sh mode` | ninguna (lee `build-config.json`) |
|
|
223
|
+
| `slice-ops.sh register [--runtime-url U] [--force]` | `POST /agents/register` con un token **propio de este árbol**. Da de alta un worktree como agente distinto (§1). El token entra por **stdin** (prompt si hay TTY), nunca por argumento —`--token` sale con rc 2— porque en argv queda en el historial del shell y en `ps`; por lo mismo es un comando de terminal humano y **no** se invoca desde dentro de un claim (los hooks no tienen TTY y un `read` colgaría la llamada). Orden obligado: siembra el fichero local **primero** (si no, el upsert caería sobre las credenciales del clon principal y le pisaría la identidad), registra, y **verifica que el `agent_key` obtenido difiera** del del principal. Cualquier fallo revierte: el árbol queda con identidad propia y verificada, o exactamente como estaba; el fichero del clon principal no se toca nunca. `0` alta (o ya registrado) · `2` uso · `5` sin red · `6` mismo agente que el principal / token rechazado |
|
|
218
224
|
| `slice-ops.sh claim` | `POST /tasks/next` (§3); `--epic` falla explícito, rc 2 (no hay claim dirigido, §3.6) |
|
|
219
225
|
| `slice-ops.sh next-step` | ninguna: lo **deriva el cliente** desde la caché de proyección |
|
|
220
226
|
| `slice-ops.sh phase <to>` | evento(s) `phase_advanced {to}` en cadena (issue #52): el hub lleva la fase con orden ESTRICTO (`dor→change→red→green→refactor→smoke→api→data→dod→pr`, `domain.py` `PHASES`) y solo acepta la fase siguiente exacta. El cliente emite la cadena que falte desde la fase conocida (proyección ∪ `phase_advanced` pendientes en la cola) hasta `<to>`; idempotente (nada si ya está). `archived` no se reporta aquí (lo produce `archive`). Siempre por cola en `runtime` (tipo protegido de la cota: evictar un eslabón haría ilegal todo lo posterior); espejo en `dual` (`phase_advanced` ∈ `TIPOS_DE_ESPEJO`) |
|
|
@@ -235,6 +241,10 @@ modo, valida en local lo que es barato validar y encola lo que no pudo entregar.
|
|
|
235
241
|
| `release-ops.sh close-hint <line>` | **ninguna**: el cierre es humano, con PDP |
|
|
236
242
|
| `release-ops.sh front-integration <front>` | `POST /fronts/{id}/members/{agent_key}/integration`; **sin fallback offline** (rc 5: `front_integration_reported` no existe en el catálogo v2, issue #44) |
|
|
237
243
|
|
|
244
|
+
**Códigos de salida.** `0` ok · `2` uso · `3` legacy · `4` sin slice · `5` offline · `6` rechazado ·
|
|
245
|
+
`7` sin trabajo · `8` fuera del catálogo de espejo · **`9` identidad heredada** (#74: este árbol
|
|
246
|
+
opera con las credenciales del clon principal; da de alta el worktree con `register`).
|
|
247
|
+
|
|
238
248
|
**Forma del borrador de `propose-epic --file`:**
|
|
239
249
|
|
|
240
250
|
```json
|
|
@@ -16,23 +16,71 @@
|
|
|
16
16
|
# Por eso, cuando el árbol actual no tiene estado, se cae al clon principal
|
|
17
17
|
# (--git-common-dir), que es donde vive de verdad. Idempotente para un clon
|
|
18
18
|
# normal: si el candidato ya tiene estado, se devuelve tal cual.
|
|
19
|
+
#
|
|
20
|
+
# [#74] Ese fallback vale para LEER, nunca para ESCRIBIR: quien vaya a transicionar algo
|
|
21
|
+
# tiene que preguntar antes por `config_is_inherited_root` (más abajo) y abortar, o estará
|
|
22
|
+
# operando con la identidad del clon principal sin saberlo.
|
|
19
23
|
config_root() {
|
|
20
24
|
local candidate
|
|
21
25
|
candidate="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
22
26
|
[ -f "$candidate/.claude/state/runtime.credentials" ] && { echo "$candidate"; return; }
|
|
23
27
|
|
|
24
|
-
local
|
|
25
|
-
|
|
26
|
-
if [ -n "$
|
|
27
|
-
|
|
28
|
-
main="$(cd "$common/.." 2>/dev/null && pwd)" || main=""
|
|
29
|
-
if [ -n "$main" ] && [ -f "$main/.claude/state/runtime.credentials" ]; then
|
|
30
|
-
echo "$main"; return
|
|
31
|
-
fi
|
|
28
|
+
local main
|
|
29
|
+
main="$(config_main_clone_root)"
|
|
30
|
+
if [ -n "$main" ] && [ -f "$main/.claude/state/runtime.credentials" ]; then
|
|
31
|
+
echo "$main"; return
|
|
32
32
|
fi
|
|
33
33
|
echo "$candidate"
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
# ── Identidad por árbol de trabajo [#74] ─────────────────────────────────────────────
|
|
37
|
+
# El fallback de config_root es deliberado para LEER (un worktree sin estado propio sigue
|
|
38
|
+
# viendo el contexto del proyecto), pero es veneno para ESCRIBIR: un worktree sin
|
|
39
|
+
# credenciales propias impersona en silencio al agente del clon principal — reclama con su
|
|
40
|
+
# `agent_key`, reporta los hashes de SU árbol (no del propio), comparte su heartbeat y, al
|
|
41
|
+
# registrarse, le pisaría el fichero de credenciales. Estos tres helpers son la señal que
|
|
42
|
+
# permite a las operaciones de escritura fallar rápido en vez de suplantar.
|
|
43
|
+
|
|
44
|
+
# _config_phys <ruta> — forma FÍSICA de una ruta (resuelve symlinks). En macOS /var es un
|
|
45
|
+
# symlink a /private/var: comparar rutas sin normalizar mide cómo se escribió el path, no a
|
|
46
|
+
# qué apunta. Ante una ruta inaccesible devuelve la entrada tal cual — nunca lanza.
|
|
47
|
+
_config_phys() {
|
|
48
|
+
(cd "$1" 2>/dev/null && pwd -P) || printf '%s\n' "$1"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# config_candidate_root — la raíz que corresponde a ESTE árbol de trabajo, SIN fallback:
|
|
52
|
+
# `CLAUDE_PROJECT_DIR` o el toplevel del árbol actual. Es el otro extremo de la comparación
|
|
53
|
+
# de identidad (config_root es «de quién es el estado que voy a usar»; esto es «dónde estoy»).
|
|
54
|
+
config_candidate_root() {
|
|
55
|
+
local candidate
|
|
56
|
+
candidate="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
57
|
+
_config_phys "$candidate"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# config_main_clone_root — raíz del clon principal según `--git-common-dir` ("" si no hay
|
|
61
|
+
# git). En un clon normal coincide con el propio árbol; en un worktree apunta al clon que lo
|
|
62
|
+
# creó. Es la única forma de leer la identidad del principal sin depender de config_root
|
|
63
|
+
# (que es justo lo que puede estar heredado).
|
|
64
|
+
config_main_clone_root() {
|
|
65
|
+
local candidate common main
|
|
66
|
+
candidate="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
67
|
+
common="$(git -C "$candidate" rev-parse --git-common-dir 2>/dev/null)" || common=""
|
|
68
|
+
[ -n "$common" ] || { echo ""; return; }
|
|
69
|
+
case "$common" in /*) ;; *) common="$candidate/$common" ;; esac
|
|
70
|
+
main="$(cd "$common/.." 2>/dev/null && pwd -P)" || main=""
|
|
71
|
+
echo "$main"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# config_is_inherited_root — rc 0 si el estado que se va a usar NO es el de este árbol, es
|
|
75
|
+
# decir: config_root cayó al clon principal. rc 1 en un clon normal, en un worktree ya
|
|
76
|
+
# registrado y en un proyecto sin arnés (donde no hay identidad que heredar).
|
|
77
|
+
config_is_inherited_root() {
|
|
78
|
+
local here there
|
|
79
|
+
here="$(config_candidate_root)"
|
|
80
|
+
there="$(_config_phys "$(config_root)")"
|
|
81
|
+
[ -n "$here" ] && [ -n "$there" ] && [ "$here" != "$there" ]
|
|
82
|
+
}
|
|
83
|
+
|
|
36
84
|
# config_get <clave.punteada> <default>
|
|
37
85
|
config_get() {
|
|
38
86
|
local key="$1" def="$2" file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# harness-meta.sh — metadatos del PAQUETE instalado (versión del arnés y catálogo de asset
|
|
3
|
+
# types). Extraído de session-start.sh [#74] para que `slice-ops.sh register` mande
|
|
4
|
+
# exactamente el mismo `harness_version` que el registro de arranque de sesión: dos lecturas
|
|
5
|
+
# distintas de la misma versión acabarían con un worktree rechazado por
|
|
6
|
+
# `incompatible_version` y el clon principal aceptado, o al revés.
|
|
7
|
+
# Fail-open, igual que config.sh: nunca lanza; sin fichero devuelve un valor neutro.
|
|
8
|
+
|
|
9
|
+
source "$(dirname "${BASH_SOURCE[0]}")/config.sh"
|
|
10
|
+
|
|
11
|
+
# harness_pkg_root — raíz del PAQUETE (no del proyecto). Los hooks se instalan como symlinks
|
|
12
|
+
# por fichero (linkChildren), así que hay que resolver la cadena de enlaces a mano:
|
|
13
|
+
# `readlink -f` no es portable al BSD readlink de macOS. Este fichero vive en
|
|
14
|
+
# `<pkg>/hooks/build/lib/`, de ahí los tres niveles.
|
|
15
|
+
harness_pkg_root() {
|
|
16
|
+
local src="${BASH_SOURCE[0]}" target i=0
|
|
17
|
+
while [ -L "$src" ] && [ "$i" -lt 10 ]; do
|
|
18
|
+
target="$(readlink "$src" 2>/dev/null)" || break
|
|
19
|
+
case "$target" in
|
|
20
|
+
/*) src="$target" ;;
|
|
21
|
+
*) src="$(dirname "$src")/$target" ;;
|
|
22
|
+
esac
|
|
23
|
+
i=$((i + 1))
|
|
24
|
+
done
|
|
25
|
+
(cd "$(dirname "$src")/../../.." >/dev/null 2>&1 && pwd -P) 2>/dev/null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# harness_version — versión instalada en el consumidor; si no, la del paquete.
|
|
29
|
+
# [#74] El marcador `.claude/.build-harness-version` está en .gitignore en modo symlink: NO
|
|
30
|
+
# viaja a un `git worktree add`. Por eso se mira también el clon principal antes de caer al
|
|
31
|
+
# paquete — un worktree recién dado de alta no tiene por qué mentir sobre su versión.
|
|
32
|
+
harness_version() {
|
|
33
|
+
local f pkg root
|
|
34
|
+
for root in "$(config_root)" "$(config_candidate_root)" "$(config_main_clone_root)"; do
|
|
35
|
+
[ -n "$root" ] || continue
|
|
36
|
+
f="$root/.claude/.build-harness-version"
|
|
37
|
+
[ -f "$f" ] && { tr -d ' \n' < "$f"; return; }
|
|
38
|
+
done
|
|
39
|
+
pkg="$(harness_pkg_root)"
|
|
40
|
+
if [ -n "$pkg" ] && [ -f "$pkg/VERSION" ]; then
|
|
41
|
+
tr -d ' \n' < "$pkg/VERSION"
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
echo "0.0.0"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# harness_asset_types — el array `types` de asset-types.json del paquete (protocolo §6).
|
|
48
|
+
# Cadena de resolución: plugin > copia en .claude/ (la siembra es del sub-slice D) >
|
|
49
|
+
# paquete resuelto por symlink. Sin fichero: "[]" — el registro sigue funcionando.
|
|
50
|
+
harness_asset_types() {
|
|
51
|
+
local pkg candidates f
|
|
52
|
+
pkg="$(harness_pkg_root)"
|
|
53
|
+
candidates="${CLAUDE_PLUGIN_ROOT:-}/asset-types.json
|
|
54
|
+
$(config_root)/.claude/asset-types.json
|
|
55
|
+
${pkg:-}/asset-types.json"
|
|
56
|
+
while IFS= read -r f; do
|
|
57
|
+
case "$f" in /asset-types.json|asset-types.json) continue ;; esac
|
|
58
|
+
[ -f "$f" ] || continue
|
|
59
|
+
python3 - "$f" <<'PY' 2>/dev/null && return
|
|
60
|
+
import json,sys
|
|
61
|
+
try:
|
|
62
|
+
d=json.load(open(sys.argv[1]))
|
|
63
|
+
t=d.get("types")
|
|
64
|
+
print(json.dumps(t if isinstance(t,list) else [],ensure_ascii=False))
|
|
65
|
+
except Exception:
|
|
66
|
+
raise SystemExit(1)
|
|
67
|
+
PY
|
|
68
|
+
done <<EOS
|
|
69
|
+
$candidates
|
|
70
|
+
EOS
|
|
71
|
+
echo "[]"
|
|
72
|
+
}
|
|
@@ -46,11 +46,39 @@ except Exception:
|
|
|
46
46
|
PY
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
# runtime_credentials_field_at <fichero> <clave> — lee un campo de un fichero de credenciales
|
|
50
|
+
# CONCRETO, sin pasar por config_root(). [#74] Es la única forma de mirar la identidad del clon
|
|
51
|
+
# principal desde un worktree: `runtime_field` resuelve por config_root(), que es justo lo que
|
|
52
|
+
# puede estar heredado — preguntarle «¿cuál es el agent_key del principal?» devolvía el propio.
|
|
53
|
+
runtime_credentials_field_at() {
|
|
54
|
+
local file="$1" key="$2"
|
|
55
|
+
[ -f "$file" ] || { echo ""; return; }
|
|
56
|
+
command -v python3 >/dev/null 2>&1 || { echo ""; return; }
|
|
57
|
+
python3 - "$file" "$key" <<'PY' 2>/dev/null
|
|
58
|
+
import json,sys
|
|
59
|
+
file,key=sys.argv[1],sys.argv[2]
|
|
60
|
+
try:
|
|
61
|
+
d=json.load(open(file))
|
|
62
|
+
v=d.get(key)
|
|
63
|
+
print(v if v is not None else "")
|
|
64
|
+
except Exception:
|
|
65
|
+
print("")
|
|
66
|
+
PY
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
# runtime_credentials_merge <json> — hace upsert de las claves del fragmento sobre el
|
|
50
70
|
# fichero existente (o uno nuevo). Escritura atómica + chmod 0600. Nunca imprime el token.
|
|
51
71
|
runtime_credentials_merge() {
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
runtime_credentials_merge_at "$(runtime_credentials_path)" "$1"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# runtime_credentials_merge_at <fichero> <json> — el mismo upsert sobre una RUTA EXPLÍCITA.
|
|
76
|
+
# [#74] `register` tiene que sembrar el fichero del worktree ANTES de que exista, y por tanto
|
|
77
|
+
# antes de que config_root() pueda anclar en local: pasando por runtime_credentials_path()
|
|
78
|
+
# habría hecho merge del token nuevo sobre el fichero del clon principal, pisándole la
|
|
79
|
+
# identidad al agente que está construyendo ahí.
|
|
80
|
+
runtime_credentials_merge_at() {
|
|
81
|
+
local file="$1" fragment="$2" dir
|
|
54
82
|
dir="$(dirname "$file")"
|
|
55
83
|
mkdir -p "$dir"
|
|
56
84
|
command -v python3 >/dev/null 2>&1 || return 1
|
|
@@ -78,6 +106,41 @@ except Exception:
|
|
|
78
106
|
PY
|
|
79
107
|
}
|
|
80
108
|
|
|
109
|
+
# runtime_credentials_seed_at <fichero> <runtime_url> — siembra credenciales en una RUTA
|
|
110
|
+
# EXPLÍCITA con el token tomado de `TRYCORE_REGISTER_TOKEN`. [#74] El token viaja por ENTORNO
|
|
111
|
+
# y no por argv a propósito: `runtime_credentials_merge_at` recibe el fragmento como argumento
|
|
112
|
+
# y en una máquina compartida un `ps` lo vería. Es el único camino por el que un secreto entra
|
|
113
|
+
# de nuevo al fichero, así que es el único que necesita esta precaución.
|
|
114
|
+
# Escribe un documento NUEVO, no un upsert: dar de alta un árbol es estrenar identidad, y
|
|
115
|
+
# conservar el `agent_key` anterior haría que una respuesta 200 sin `agent_key` pareciera un alta
|
|
116
|
+
# buena (`register --force` para rotar el token es justo el caso). Lo que el hub devuelva vuelve a
|
|
117
|
+
# entrar por `runtime_register`. Escritura atómica + chmod 0600.
|
|
118
|
+
runtime_credentials_seed_at() {
|
|
119
|
+
local file="$1" url="$2" dir
|
|
120
|
+
dir="$(dirname "$file")"
|
|
121
|
+
mkdir -p "$dir"
|
|
122
|
+
command -v python3 >/dev/null 2>&1 || return 1
|
|
123
|
+
python3 - "$file" "$url" <<'PY' 2>/dev/null || return 1
|
|
124
|
+
import json,os,sys,tempfile
|
|
125
|
+
path,url=sys.argv[1],sys.argv[2]
|
|
126
|
+
tok=os.environ.get("TRYCORE_REGISTER_TOKEN","")
|
|
127
|
+
if not tok:
|
|
128
|
+
raise SystemExit(1)
|
|
129
|
+
d={"runtime_url":url,"project_token":tok}
|
|
130
|
+
dirn=os.path.dirname(path) or "."
|
|
131
|
+
fd,tmp=tempfile.mkstemp(dir=dirn,prefix=".runtime-credentials.",suffix=".tmp")
|
|
132
|
+
try:
|
|
133
|
+
with os.fdopen(fd,"w") as out:
|
|
134
|
+
json.dump(d,out,indent=2,ensure_ascii=False); out.flush(); os.fsync(out.fileno())
|
|
135
|
+
os.chmod(tmp,0o600)
|
|
136
|
+
os.replace(tmp,path)
|
|
137
|
+
except Exception:
|
|
138
|
+
try: os.unlink(tmp)
|
|
139
|
+
except OSError: pass
|
|
140
|
+
raise
|
|
141
|
+
PY
|
|
142
|
+
}
|
|
143
|
+
|
|
81
144
|
runtime_url() {
|
|
82
145
|
runtime_field runtime_url
|
|
83
146
|
}
|
|
@@ -22,6 +22,31 @@ OPS_RC_OFFLINE=5
|
|
|
22
22
|
OPS_RC_REJECTED=6
|
|
23
23
|
OPS_RC_NO_WORK=7
|
|
24
24
|
OPS_RC_NOT_MIRRORABLE=8
|
|
25
|
+
OPS_RC_NO_IDENTITY=9
|
|
26
|
+
|
|
27
|
+
# ── Guard de identidad heredada [#74] ────────────────────────────────────────────────
|
|
28
|
+
# Un worktree sin `.claude/state/runtime.credentials` propio hereda el del clon principal por
|
|
29
|
+
# el fallback de `config_root()`: lee bien, pero al ESCRIBIR suplanta — reclama con el
|
|
30
|
+
# `agent_key` ajeno, reporta al hub los hashes del árbol equivocado y comparte su heartbeat.
|
|
31
|
+
# El hub reparte dos épicas a dos agentes sin objeción; el bloqueo del multi-agente en una
|
|
32
|
+
# misma máquina siempre estuvo aquí, en el cliente. Por eso el guard es fail-FAST y no
|
|
33
|
+
# fail-open: la regla general de este cliente («ante la duda, no bloquees el trabajo local»)
|
|
34
|
+
# protege del runtime caído, no de mandar actos firmados con la identidad de otro.
|
|
35
|
+
#
|
|
36
|
+
# ops_guard_own_identity <subcomando> — rc 0 si este árbol tiene identidad propia;
|
|
37
|
+
# OPS_RC_NO_IDENTITY (9) con mensaje accionable si la heredó.
|
|
38
|
+
ops_guard_own_identity() {
|
|
39
|
+
local sub="${1:-esta operación}" main
|
|
40
|
+
config_is_inherited_root || return 0
|
|
41
|
+
main="$(config_main_clone_root)"
|
|
42
|
+
echo "⛔ este worktree no tiene identidad propia: «${sub}» operaría como el agente del clon" >&2
|
|
43
|
+
echo " principal (${main:-?}) — su agent_key, su lease, sus hashes de contexto." >&2
|
|
44
|
+
echo " Da de alta este árbol con un token de agente NUEVO emitido por el ADMIN:" >&2
|
|
45
|
+
echo " bash .claude/hooks/build/slice-ops.sh register" >&2
|
|
46
|
+
echo " (el token se pide por stdin, nunca por argumento). Las lecturas — \`status\`," >&2
|
|
47
|
+
echo " \`next-step\`, \`mode\` — siguen funcionando con el contexto heredado." >&2
|
|
48
|
+
return $OPS_RC_NO_IDENTITY
|
|
49
|
+
}
|
|
25
50
|
|
|
26
51
|
# Tope de texto libre que el cliente manda al servidor (evidencias, notas): el protocolo §8
|
|
27
52
|
# dice que el servidor rechaza payloads sobredimensionados; se trunca aquí, marcándolo.
|