@trycore/spec-build-harness 0.14.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bash
2
+ # test-worktree-identity.sh — identidad por árbol de trabajo [#74].
3
+ #
4
+ # El caso real: dos agentes construyendo dos épicas del mismo proyecto en la misma máquina,
5
+ # uno por worktree. El hub reparte las dos sin objeción; el bloqueo estaba en el cliente —
6
+ # un worktree sin `.claude/state/runtime.credentials` propio hereda el del clon principal
7
+ # (`config_root()` cae al `--git-common-dir`) y firma sus actos con la identidad ajena.
8
+ #
9
+ # Cubre: la compuerta de escritura de slice-ops.sh/release-ops.sh (rc 9), el banner de
10
+ # `status` y el alta explícita con `slice-ops.sh register` contra el stub HTTP.
11
+ set -uo pipefail
12
+ ROOT="$(git rev-parse --show-toplevel)"
13
+ fail=0
14
+ TMP="$(mktemp -d)"
15
+ STUBS=""
16
+ cleanup() {
17
+ for p in $STUBS; do kill "$p" 2>/dev/null; wait "$p" 2>/dev/null; done
18
+ git -C "$MAIN" worktree remove --force "$WT" >/dev/null 2>&1
19
+ rm -rf "$TMP"
20
+ }
21
+ SO="$ROOT/hooks/build/slice-ops.sh"
22
+ RO="$ROOT/hooks/build/release-ops.sh"
23
+ freeport() { python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()"; }
24
+ sha() { python3 -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$1" 2>/dev/null; }
25
+ cred() { python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2]) or '')" "$1" "$2" 2>/dev/null; }
26
+
27
+ # ── Escenario: clon principal registrado + worktree recién abierto, sin nada ────────────
28
+ MAIN="$(mkdir -p "$TMP/main" && cd "$TMP/main" && pwd -P)"
29
+ WT="$TMP/wt"
30
+ trap cleanup EXIT
31
+ mkdir -p "$MAIN/.claude/state" "$MAIN/.claude/config"
32
+ git -C "$MAIN" init -q 2>/dev/null
33
+ git -C "$MAIN" config user.email t@t.io; git -C "$MAIN" config user.name t
34
+ echo x > "$MAIN/f"; git -C "$MAIN" add -A >/dev/null 2>&1; git -C "$MAIN" commit -qm init >/dev/null 2>&1
35
+ git -C "$MAIN" worktree add -q "$WT" -b wt-test >/dev/null 2>&1
36
+ WT="$(cd "$WT" && pwd -P)"
37
+ echo '{"runtime":{"mode":"runtime","stale_seconds":900}}' > "$MAIN/.claude/config/build-config.json"
38
+
39
+ PORT="$(freeport)"
40
+ cat > "$TMP/routes.json" <<JSON
41
+ {"POST /agents/register": {"status": 200, "body": {"agent_key": "ak-wt", "project_id": "p-1", "poll_interval_s": 5, "lease_ttl_s": 300, "manifest_hash": "sha256:v1"}}}
42
+ JSON
43
+ : > "$TMP/calls.jsonl"
44
+ python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT" "$TMP/routes.json" "$TMP/calls.jsonl" &
45
+ STUBS="$STUBS $!"
46
+ sleep 0.3
47
+ cat > "$MAIN/.claude/state/runtime.credentials" <<JSON
48
+ {"runtime_url":"http://127.0.0.1:$PORT","project_token":"tok-main","project_id":"p-1","agent_key":"ak-main"}
49
+ JSON
50
+ MAIN_SHA_0="$(sha "$MAIN/.claude/state/runtime.credentials")"
51
+ MAINCRED="$MAIN/.claude/state/runtime.credentials"
52
+ WTCRED="$WT/.claude/state/runtime.credentials"
53
+
54
+ # `curl` falso al frente del PATH: sirve para probar que la compuerta corta ANTES de la red.
55
+ mkdir -p "$TMP/fakebin"
56
+ cat > "$TMP/fakebin/curl" <<'FAKE'
57
+ #!/usr/bin/env bash
58
+ echo "$@" >> "$FAKE_CURL_LOG"
59
+ exit 0
60
+ FAKE
61
+ chmod +x "$TMP/fakebin/curl"
62
+
63
+ # wt <args…> — corre slice-ops.sh DENTRO del worktree, como lo haría el agente de ese árbol.
64
+ wt() { (cd "$WT" && CLAUDE_PROJECT_DIR="$WT" bash "$SO" "$@"); }
65
+
66
+ # ── 1) Toda escritura aborta con rc 9 y sin tocar la red ────────────────────────────────
67
+ : > "$TMP/curl-guard.log"
68
+ for sub in claim submit escalate progress checkpoint "gate dor pass" "phase change" "fact foundation" \
69
+ "wiring seed" "archive" "propose-epic" "graph-sync"; do
70
+ rc=0
71
+ out="$(cd "$WT" && PATH="$TMP/fakebin:$PATH" FAKE_CURL_LOG="$TMP/curl-guard.log" \
72
+ CLAUDE_PROJECT_DIR="$WT" bash "$SO" $sub 2>&1)" || rc=$?
73
+ if [ "$rc" = 9 ]; then
74
+ echo "OK «${sub%% *}» aborta con rc 9 desde un worktree sin identidad"
75
+ else
76
+ echo "FAIL «$sub» rc=$rc (esperado 9): $out"; fail=1
77
+ fi
78
+ done
79
+ [ ! -s "$TMP/curl-guard.log" ] && echo "OK la compuerta corta ANTES de abrir un socket" \
80
+ || { echo "FAIL la compuerta dejó salir tráfico"; fail=1; }
81
+
82
+ rc=0; out="$(wt claim 2>&1)" || rc=$?
83
+ echo "$out" | grep -q "register" && echo "OK el mensaje del guard dice cómo salir (register)" \
84
+ || { echo "FAIL el guard no es accionable: $out"; fail=1; }
85
+ echo "$out" | grep -q "$MAIN" && echo "OK el mensaje nombra el clon principal suplantado" \
86
+ || { echo "FAIL el guard no nombra al principal: $out"; fail=1; }
87
+
88
+ rc=0
89
+ out="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" bash "$RO" front-integration f-1 --merge-status merged 2>&1)" || rc=$?
90
+ [ "$rc" = 9 ] && echo "OK release-ops front-integration aborta con rc 9" \
91
+ || { echo "FAIL front-integration rc=$rc: $out"; fail=1; }
92
+
93
+ # ── 2) Las lecturas siguen funcionando, con banner ──────────────────────────────────────
94
+ rc=0; out="$(wt status --no-refresh 2>&1)" || rc=$?
95
+ [ "$rc" = 0 ] && echo "OK status sigue funcionando con identidad heredada" \
96
+ || { echo "FAIL status rc=$rc: $out"; fail=1; }
97
+ echo "$out" | grep -q "HEREDADA" && echo "OK status avisa de la identidad heredada" \
98
+ || { echo "FAIL status no muestra el banner: $out"; fail=1; }
99
+ [ "$(wt mode)" = "runtime" ] && echo "OK mode se lee sin compuerta" || { echo "FAIL mode"; fail=1; }
100
+
101
+ # ── 3) register: el token NO se acepta por argumento ────────────────────────────────────
102
+ rc=0; out="$(wt register --token tok-x 2>&1)" || rc=$?
103
+ [ "$rc" = 2 ] && echo "OK register rechaza --token (el historial del shell no es un llavero)" \
104
+ || { echo "FAIL register --token rc=$rc: $out"; fail=1; }
105
+ [ ! -f "$WTCRED" ] && echo "OK register --token no escribió nada" || { echo "FAIL escribió credenciales"; fail=1; }
106
+
107
+ # ── 4) register con el token del clon principal: rechazo, y el principal INTACTO ────────
108
+ rc=0; out="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" bash "$SO" register <<< "tok-main" 2>&1)" || rc=$?
109
+ [ "$rc" = 6 ] && echo "OK register rechaza el token del clon principal" \
110
+ || { echo "FAIL token repetido rc=$rc: $out"; fail=1; }
111
+ echo "$out" | grep -qi "agent_id" && echo "OK el rechazo dice qué pedirle al ADMIN" \
112
+ || { echo "FAIL rechazo poco accionable: $out"; fail=1; }
113
+ [ ! -f "$WTCRED" ] && echo "OK el token repetido no dejó credenciales locales" \
114
+ || { echo "FAIL quedaron credenciales tras el rechazo"; fail=1; }
115
+ [ "$(sha "$MAINCRED")" = "$MAIN_SHA_0" ] && echo "OK el fichero del clon principal quedó intacto" \
116
+ || { echo "FAIL se tocó el fichero del clon principal"; fail=1; }
117
+
118
+ # ── 5) register cuando el hub devuelve el MISMO agent_key: rechazo + rollback ───────────
119
+ cat > "$TMP/routes.json" <<JSON
120
+ {"POST /agents/register": {"status": 200, "body": {"agent_key": "ak-main", "project_id": "p-1"}}}
121
+ JSON
122
+ rc=0; out="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" bash "$SO" register <<< "tok-otro" 2>&1)" || rc=$?
123
+ [ "$rc" = 6 ] && echo "OK register rechaza un token que resuelve al agent_key del principal" \
124
+ || { echo "FAIL agent_key repetido rc=$rc: $out"; fail=1; }
125
+ [ ! -f "$WTCRED" ] && echo "OK rollback: no quedan credenciales a medias" \
126
+ || { echo "FAIL quedaron credenciales tras el rollback"; fail=1; }
127
+ [ "$(sha "$MAINCRED")" = "$MAIN_SHA_0" ] && echo "OK el clon principal sigue intacto tras el rollback" \
128
+ || { echo "FAIL el rollback tocó al principal"; fail=1; }
129
+
130
+ # ── 6) Sin red: rollback y rc 5 ─────────────────────────────────────────────────────────
131
+ DEADPORT="$(freeport)"
132
+ rc=0; out="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" bash "$SO" register --runtime-url "http://127.0.0.1:$DEADPORT" <<< "tok-wt" 2>&1)" || rc=$?
133
+ [ "$rc" = 5 ] && echo "OK register sin red -> rc 5" || { echo "FAIL register offline rc=$rc: $out"; fail=1; }
134
+ [ ! -f "$WTCRED" ] && echo "OK register sin red no deja credenciales huérfanas" \
135
+ || { echo "FAIL quedaron credenciales tras el fallo de red"; fail=1; }
136
+
137
+ # ── 7) Camino feliz: identidad propia, solo en el worktree ──────────────────────────────
138
+ cat > "$TMP/routes.json" <<JSON
139
+ {"POST /agents/register": {"status": 200, "body": {"agent_key": "ak-wt", "project_id": "p-1", "lease_ttl_s": 300, "manifest_hash": "sha256:v1"}}}
140
+ JSON
141
+ : > "$TMP/calls.jsonl"
142
+ rc=0; out="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" bash "$SO" register <<< "tok-wt" 2>&1)" || rc=$?
143
+ [ "$rc" = 0 ] && echo "OK register con token propio -> rc 0" || { echo "FAIL register feliz rc=$rc: $out"; fail=1; }
144
+ echo "$out" | grep -q "tok-wt" && { echo "FAIL register imprimió el token"; fail=1; } \
145
+ || echo "OK register nunca imprime el token"
146
+ [ -f "$WTCRED" ] && echo "OK las credenciales quedan en el worktree" || { echo "FAIL sin credenciales locales"; fail=1; }
147
+ [ "$(cred "$WTCRED" agent_key)" = "ak-wt" ] && echo "OK el worktree tiene su propio agent_key" \
148
+ || { echo "FAIL agent_key local ($(cred "$WTCRED" agent_key))"; fail=1; }
149
+ [ "$(cred "$WTCRED" project_token)" = "tok-wt" ] && echo "OK el token propio quedó persistido" \
150
+ || { echo "FAIL token local"; fail=1; }
151
+ [ "$(cred "$WTCRED" project_id)" = "p-1" ] && echo "OK mismo proyecto, otro agente" || { echo "FAIL project_id"; fail=1; }
152
+ [ "$(sha "$MAINCRED")" = "$MAIN_SHA_0" ] && echo "OK el clon principal NUNCA se tocó" \
153
+ || { echo "FAIL el alta pisó al clon principal"; fail=1; }
154
+ grep -q '"auth": "Bearer tok-wt"' "$TMP/calls.jsonl" \
155
+ && echo "OK el registro viajó con el token NUEVO, no con el heredado" \
156
+ || { echo "FAIL el registro no usó el token nuevo: $(cat "$TMP/calls.jsonl")"; fail=1; }
157
+ perm="$(python3 -c "import os,stat,sys; print(oct(stat.S_IMODE(os.stat(sys.argv[1]).st_mode)))" "$WTCRED")"
158
+ [ "$perm" = "0o600" ] && echo "OK credenciales 0600" || { echo "FAIL permisos $perm"; fail=1; }
159
+ [ -f "$WT/.claude/config/build-config.json" ] \
160
+ && echo "OK register copió build-config.json (el árbol no cae a legacy)" \
161
+ || { echo "FAIL sin build-config el worktree registrado caería a legacy"; fail=1; }
162
+
163
+ # ── 8) Ya registrado: la compuerta se abre y register es idempotente ────────────────────
164
+ rc=0; out="$(wt claim 2>&1)" || rc=$?
165
+ [ "$rc" != 9 ] && echo "OK tras register el claim ya no choca con la compuerta (rc $rc)" \
166
+ || { echo "FAIL sigue bloqueado tras registrar: $out"; fail=1; }
167
+ rc=0; out="$(wt status --no-refresh 2>&1)" || rc=$?
168
+ echo "$out" | grep -q "HEREDADA" && { echo "FAIL status sigue diciendo heredada"; fail=1; } \
169
+ || echo "OK status ya no marca identidad heredada"
170
+ echo "$out" | grep -q "ak-wt" && echo "OK status informa del agente propio" || { echo "FAIL status no ve ak-wt: $out"; fail=1; }
171
+ rc=0; out="$(wt register 2>&1 </dev/null)" || rc=$?
172
+ [ "$rc" = 0 ] && echo "$out" | grep -q "YA tiene identidad" \
173
+ && echo "OK register es idempotente: no vuelve a pedir token" \
174
+ || { echo "FAIL register no idempotente rc=$rc: $out"; fail=1; }
175
+ [ "$(cred "$WTCRED" agent_key)" = "ak-wt" ] && echo "OK la identidad no cambió al repetir register" \
176
+ || { echo "FAIL register repetido alteró la identidad"; fail=1; }
177
+
178
+ # ── 8-bis) --force rota el token y ESTRENA identidad (no recicla el agent_key viejo) ────
179
+ cat > "$TMP/routes.json" <<JSON
180
+ {"POST /agents/register": {"status": 200, "body": {"agent_key": "ak-wt2", "project_id": "p-1"}}}
181
+ JSON
182
+ rc=0; out="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" bash "$SO" register --force <<< "tok-wt2" 2>&1)" || rc=$?
183
+ [ "$rc" = 0 ] && [ "$(cred "$WTCRED" agent_key)" = "ak-wt2" ] && [ "$(cred "$WTCRED" project_token)" = "tok-wt2" ] \
184
+ && echo "OK register --force rota token e identidad" \
185
+ || { echo "FAIL register --force rc=$rc key=$(cred "$WTCRED" agent_key): $out"; fail=1; }
186
+
187
+ # Un 200 SIN agent_key no puede pasar por bueno reciclando el de la identidad anterior.
188
+ cat > "$TMP/routes.json" <<JSON
189
+ {"POST /agents/register": {"status": 200, "body": {"project_id": "p-1"}}}
190
+ JSON
191
+ rc=0; out="$(cd "$WT" && CLAUDE_PROJECT_DIR="$WT" bash "$SO" register --force <<< "tok-wt3" 2>&1)" || rc=$?
192
+ [ "$rc" = 6 ] && echo "OK register rechaza un 200 sin agent_key" \
193
+ || { echo "FAIL 200 sin agent_key rc=$rc: $out"; fail=1; }
194
+ [ "$(cred "$WTCRED" agent_key)" = "ak-wt2" ] && [ "$(cred "$WTCRED" project_token)" = "tok-wt2" ] \
195
+ && echo "OK el rollback devuelve la identidad anterior intacta" \
196
+ || { echo "FAIL rollback de --force perdió la identidad previa"; fail=1; }
197
+ [ "$(sha "$MAINCRED")" = "$MAIN_SHA_0" ] && echo "OK el clon principal sigue intacto tras --force" \
198
+ || { echo "FAIL --force tocó al principal"; fail=1; }
199
+
200
+ # ── 9) El clon principal nunca pasa por la compuerta ────────────────────────────────────
201
+ rc=0
202
+ out="$(cd "$MAIN" && PATH="$TMP/fakebin:$PATH" FAKE_CURL_LOG="$TMP/curl-main.log" \
203
+ CLAUDE_PROJECT_DIR="$MAIN" bash "$SO" claim 2>&1)" || rc=$?
204
+ [ "$rc" != 9 ] && echo "OK el clon principal no queda bloqueado por su propio guard (rc $rc)" \
205
+ || { echo "FAIL el guard bloquea al dueño del estado: $out"; fail=1; }
206
+
207
+ exit $fail
@@ -1,40 +1,64 @@
1
1
  ---
2
2
  name: managing-parallel-front
3
- description: Use when building multiple NON-foundational, file-disjoint epics in parallel via git worktrees. Prepares the disjoint selection and merge order locally (front-plan.py), drives each worktree as its own registered agent, and reports each member's integration (re-smoke, merge_status) through the agent surface. Planning, opening, draining and closing the front are human acts in the hub console. Never parallelizes foundational epics or overlapping file scopes.
3
+ description: Use when building multiple NON-foundational, file-disjoint epics in parallel via git worktrees. Prepares the disjoint selection and merge order locally (front-plan.py), enrolls each worktree as its own agent with its own ADMIN-issued token (slice-ops.sh register), and reports each member's integration (re-smoke, merge_status) through the agent surface. Planning, opening, draining and closing the front are human acts in the hub console. Never parallelizes foundational epics, overlapping file scopes, or two worktrees sharing one token.
4
4
  ---
5
5
 
6
6
  # Gestionar el front paralelo inter-épica (outer-loop)
7
7
 
8
8
  **Invariante:** el paralelismo es outer-loop. Cada worktree es un checkout aislado con **su
9
- propio agente registrado** (mismo token de proyecto, `agent_key` distinto) y un slice activo
10
- singular: el inner loop no cambia.
9
+ propio agente registrado** un token propio, emitido por el ADMIN para ese agente — y un slice
10
+ activo singular: el inner loop no cambia.
11
+
12
+ > **Un token por agente, no uno por proyecto.** El hub **no crea identidad** en
13
+ > `POST /agents/register`: el `agent_key` es el `agent_id` que el ADMIN grabó en el token al
14
+ > emitirlo. Dos worktrees con el mismo token son **el mismo agente**, y el segundo `claim`
15
+ > devuelve el slice que ya tiene reclamado el primero. Y un worktree **sin** credenciales
16
+ > propias no queda huérfano: hereda en silencio las del clon principal (`config_root()` cae
17
+ > al `--git-common-dir`) y **suplanta** a ese agente. Por eso toda escritura —`claim`, `gate`,
18
+ > `submit`, `archive`, `wiring`, `progress`, `checkpoint`, `fact`, `escalate`,
19
+ > `front-integration`— aborta con **rc 9** hasta que el árbol se da de alta. Las lecturas
20
+ > (`status`, `next-step`) siguen funcionando, y `status` avisa con `identidad: ⚠ HEREDADA`.
11
21
 
12
22
  **Reparto agente ↔ humano (spec §2).** El agente **prepara y reporta**; la persona **decide**:
13
23
 
14
24
  | Acto | Quién |
15
25
  |---|---|
16
26
  | Proponer el conjunto disjunto y el `merge_order` (`front-plan.py`, local) | agente |
27
+ | **Emitir un token por agente** (uno por worktree, `agent_id` distinto) | **humano** (ADMIN), en la consola del hub |
17
28
  | **Planificar, abrir, drenar y cerrar** el front | **humano**, en la consola del hub |
18
29
  | Construir cada worktree (inner loop) | agente (uno por worktree) |
19
30
  | Reportar re-smoke y `merge_status` de su miembro | agente, vía `release-ops.sh front-integration` |
20
31
 
21
32
  ## Precondiciones (compuertas)
22
33
  - Scaffold confirmado (`bash .claude/hooks/build/slice-ops.sh status`).
34
+ - **Un token de agente por worktree**, ya emitido por el ADMIN (paso 3). Sin él no hay front:
35
+ no es un trámite, es lo que hace que dos agentes sean dos.
23
36
  - **G1 Fundacionales primero:** ninguna épica `layer=foundational` abierta. Si aparece una, el
24
37
  front debe **drenarse** (terminar en curso, no admitir nuevas) antes de abrirla — el drenaje
25
38
  lo hace una persona en la consola.
26
39
 
27
40
  ## Procedimiento
28
- 1. **Reunir candidatas** no fundacionales listas (DoR pasado), cada una con `layer` y `files_scope`.
41
+ 1. **Reunir candidatas** no fundacionales listas (DoR pasado), cada una con `layer`,
42
+ `files_scope` y `depends_on` (las dependencias declaradas del grafo; `graph-bundle.py` ya las
43
+ computa desde los docs de discovery). Sin `files_scope` declarado una épica **no es
44
+ candidata** y `front-plan.py` la serializa — no es un olvido tolerable, es la compuerta.
29
45
  2. **Seleccionar el conjunto disjunto** (G2), en local:
30
46
  `echo "$CANDS" | python3 .claude/scripts/lib/front-plan.py`
31
- → `selected` propuestas al front; `serialized` esperan (construir secuencial después);
47
+ → `selected` propuestas al front; `serialized` esperan (construir secuencial después) por
48
+ solape de ficheros, por camino en el DAG de `depends_on` o por no declarar `files_scope`;
32
49
  `excluded_foundational` nunca en paralelo. **Presenta el plan al humano**: es él quien abre el
33
50
  front en la consola con ese `merge_order`.
34
- 3. **Abrir un worktree por épica aprobada:**
51
+ 3. **Abrir un worktree por épica aprobada y darlo de alta con su propio token:**
35
52
  `git worktree add ".wt/<epica>" -b feature/<slug>` (rama por `1 épica = 1 rama = 1 PR`).
36
- En cada worktree corre `trycore-build init` con el **mismo token de proyecto**: se registra
37
- como agente propio y obtiene su `agent_key`.
53
+ El estado del arnés está en `.gitignore`, así que **no viaja al worktree**: en cada uno,
54
+ 1. `trycore-build init` (sin `--runtime-token`) para tender `.claude/`;
55
+ 2. `bash .claude/hooks/build/slice-ops.sh register` — **pide el token por stdin**, nunca por
56
+ argumento (un `--token` queda en el historial del shell y en `ps`). Cada worktree usa el
57
+ token que el ADMIN emitió **para ese agente**.
58
+ `register` siembra las credenciales locales antes de tocar la red, verifica contra el hub que
59
+ el `agent_key` obtenido **difiere** del del clon principal y, ante cualquier fallo, revierte:
60
+ el fichero del clon principal nunca se toca. Si el token repetido es el del principal, lo
61
+ rechaza y te manda a pedir uno con `agent_id` distinto.
38
62
  4. **Construir cada worktree** con la skill `building-a-slice` (inner loop normal, en su cwd).
39
63
  Cada uno reclama su slice y mantiene su `journey_smoke` verde localmente.
40
64
  5. **Coordinar merge (G3)** en el `merge_order` aprobado:
@@ -46,6 +70,9 @@ singular: el inner loop no cambia.
46
70
  (el agente solo reporta que su miembro terminó).
47
71
 
48
72
  ## Reglas duras
73
+ - **Un token por agente.** Dos worktrees con el mismo token son un solo agente para el hub.
74
+ - Un worktree sin dar de alta **no trabaja**: toda escritura sale con rc 9 hasta que corras
75
+ `register`. No lo esquives exportando `CLAUDE_PROJECT_DIR` — el guard compara el árbol real.
49
76
  - Nunca escritores paralelos sobre el mismo árbol (por eso worktrees).
50
77
  - Nunca paralelizar dentro de una épica (preserva la "regla del esqueleto que camina").
51
78
  - Un solape de `files_scope` no detectado que cause conflicto de merge → el re-smoke (G3) lo caza.