@trycore/spec-build-harness 0.13.0 → 0.14.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/INSTALL.md +56 -3
- package/METODOLOGIA.md +6 -1
- package/README.md +46 -2
- package/VERSION +1 -1
- package/agents/build/dor-dod-gatekeeper.md +10 -3
- package/commands/build/epic.md +109 -0
- package/commands/build/onboard.md +34 -9
- package/commands/build/slice.md +4 -2
- package/commands/build/work.md +6 -2
- package/config/build-config.template.json +2 -1
- package/dist/commands/init.js +13 -0
- package/dist/commands/status.js +13 -1
- package/dist/lib/paths.js +1 -0
- package/dist/lib/runtime-client.js +20 -0
- package/docs/commands.md +3 -2
- package/docs/getting-started.md +11 -1
- package/docs/hooks.md +11 -1
- package/docs/runtime/guia-modo-dual-y-migracion.md +89 -9
- package/docs/runtime/protocolo-cliente-runtime.md +64 -0
- package/hooks/build/design-source-guard.sh +12 -1
- package/hooks/build/heartbeat.sh +64 -4
- package/hooks/build/lib/agent-context.sh +16 -7
- package/hooks/build/lib/config.sh +25 -1
- package/hooks/build/lib/runtime-client.sh +540 -9
- package/hooks/build/lib/runtime-ops.sh +108 -0
- package/hooks/build/scaffold-guard.sh +14 -1
- package/hooks/build/slice-ops.sh +498 -16
- package/package.json +1 -1
- package/scripts/tests/test-config.sh +41 -0
- package/scripts/tests/test-hooks-runtime.sh +198 -3
- package/scripts/tests/test-install.sh +23 -0
- package/scripts/tests/test-runtime-client.sh +500 -0
- package/scripts/tests/test-skill-ops.sh +456 -0
- package/skills/building-a-slice/references/dor.md +21 -9
- package/skills/building-a-slice/references/foundation-contract.md +4 -0
- package/state/README.md +12 -0
|
@@ -126,6 +126,45 @@ got="$(projection_get active_slice.slice_id "")"
|
|
|
126
126
|
[ "$got" = "sl-9" ] && echo "OK fetch_and_cache escribe la cache" || { echo "FAIL cache tras fetch ($got)"; fail=1; }
|
|
127
127
|
if projection_is_stale; then echo "FAIL cache recien traida es stale"; fail=1; else echo "OK cache recien traida es fresca"; fi
|
|
128
128
|
|
|
129
|
+
# [#63] La versión de grafo se normaliza igual que el manifest_hash: un solo sitio conoce la
|
|
130
|
+
# forma cruda del servidor.
|
|
131
|
+
python3 - "$TMP/routes-ac.json" <<'PY'
|
|
132
|
+
import json,sys
|
|
133
|
+
p=sys.argv[1]; d=json.load(open(p))
|
|
134
|
+
r=d["GET /agent/context"]["body"]
|
|
135
|
+
r.setdefault("context",{})["graph_version"]=42
|
|
136
|
+
json.dump(d,open(p,"w"))
|
|
137
|
+
PY
|
|
138
|
+
agent_context_fetch_and_cache >/dev/null 2>&1
|
|
139
|
+
got="$(projection_get context.graph_version "")"
|
|
140
|
+
[ "$got" = 42 ] && echo "OK agent-context normaliza context.graph_version" \
|
|
141
|
+
|| { echo "FAIL graph_version sin normalizar ($got)"; fail=1; }
|
|
142
|
+
[ "$(_runtime_graph_version)" = 42 ] && echo "OK _runtime_graph_version lee la version conocida" \
|
|
143
|
+
|| { echo "FAIL _runtime_graph_version"; fail=1; }
|
|
144
|
+
|
|
145
|
+
# Hub que NO versiona: graph_version queda null y nada cambia.
|
|
146
|
+
python3 - "$TMP/routes-ac.json" <<'PY'
|
|
147
|
+
import json,sys
|
|
148
|
+
p=sys.argv[1]; d=json.load(open(p))
|
|
149
|
+
d["GET /agent/context"]["body"]["context"].pop("graph_version",None)
|
|
150
|
+
json.dump(d,open(p,"w"))
|
|
151
|
+
PY
|
|
152
|
+
agent_context_fetch_and_cache >/dev/null 2>&1
|
|
153
|
+
[ "$(projection_get context.graph_version "")" = "" ] && [ "$(_runtime_graph_version)" = "" ] \
|
|
154
|
+
&& echo "OK sin versión en el hub, graph_version queda vacia" \
|
|
155
|
+
|| { echo "FAIL graph_version inventada sin hub que versione"; fail=1; }
|
|
156
|
+
|
|
157
|
+
# Marcador de desfase: se anota y se limpia.
|
|
158
|
+
runtime_graph_note_stale 42 45
|
|
159
|
+
runtime_graph_status_read | python3 -c "
|
|
160
|
+
import json,sys
|
|
161
|
+
d=json.load(sys.stdin)
|
|
162
|
+
print('SI' if d.get('local')==42 and d.get('server')==45 and d.get('seen_at') else 'NO')" | grep -q SI \
|
|
163
|
+
&& echo "OK runtime_graph_note_stale anota el desfase" || { echo "FAIL note_stale"; fail=1; }
|
|
164
|
+
runtime_graph_clear_stale
|
|
165
|
+
[ "$(runtime_graph_status_read)" = "{}" ] && echo "OK runtime_graph_clear_stale borra el marcador" \
|
|
166
|
+
|| { echo "FAIL clear_stale"; fail=1; }
|
|
167
|
+
|
|
129
168
|
kill "$STUB_AC" 2>/dev/null; wait "$STUB_AC" 2>/dev/null
|
|
130
169
|
# Servidor caído: rc 1 y la caché ANTERIOR intacta (nunca se pisa con vacío).
|
|
131
170
|
agent_context_fetch_and_cache
|
|
@@ -284,9 +323,142 @@ sleep 4
|
|
|
284
323
|
kill -0 "$HBPID3" 2>/dev/null && { echo "FAIL daemon sobrevive sin sesiones"; fail=1; } || echo "OK daemon muere con la ultima sesion"
|
|
285
324
|
[ ! -f "$PROJ/.claude/state/heartbeat.pid" ] && echo "OK daemon limpia el pidfile" || { echo "FAIL pidfile huerfano"; fail=1; }
|
|
286
325
|
|
|
326
|
+
# [#61] Con el daemon vivo, un cambio en /agent/context llega a la proyección local SIN
|
|
327
|
+
# reiniciar la sesión ni hacer claim: hoy la caché solo se hidrataba en session-start y claim.
|
|
328
|
+
export TRYCORE_CONTEXT_REFRESH_S=1
|
|
329
|
+
cat > "$TMP/routes-hb.json" <<JSON
|
|
330
|
+
{"PUT /leases/renew": {"status": 200, "body": {"expires_at": "2099-01-01T00:00:00Z"}},
|
|
331
|
+
"GET /agent/context": {"status": 200, "body": {"project": {"scaffold": {"confirmed": true}}, "active_slice": {"slice_id": "sl-9", "epic_code": "EP-077", "phase": "green"}, "nudges": [{"kind": "info", "message": "épica EP-077 aprobada"}], "context": {"manifest_hash": "sha256:v2", "version": 9}}},
|
|
332
|
+
"POST /events": {"status": 200, "body": {"results": []}}}
|
|
333
|
+
JSON
|
|
334
|
+
sleep 60 & SESSC=$!
|
|
335
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --ensure "$SESSC" >/dev/null 2>&1
|
|
336
|
+
sleep 4
|
|
337
|
+
got="$(python3 -c "
|
|
338
|
+
import json
|
|
339
|
+
d=json.load(open('$PROJ/.claude/state/runtime-projection.json'))
|
|
340
|
+
print((d.get('active_slice') or {}).get('epic_code'))" 2>/dev/null)"
|
|
341
|
+
[ "$got" = "EP-077" ] && echo "OK el daemon refresca la proyeccion sin claim ni reinicio" \
|
|
342
|
+
|| { echo "FAIL la proyeccion no se refresco ($got)"; fail=1; }
|
|
343
|
+
got="$(python3 -c "
|
|
344
|
+
import json
|
|
345
|
+
d=json.load(open('$PROJ/.claude/state/runtime-projection.json'))
|
|
346
|
+
print((d.get('nudges') or [{}])[0].get('message'))" 2>/dev/null)"
|
|
347
|
+
[ "$got" = "épica EP-077 aprobada" ] && echo "OK el nudge del hub llega a la proyeccion local" \
|
|
348
|
+
|| { echo "FAIL el nudge no llego ($got)"; fail=1; }
|
|
349
|
+
# [Ronda final] `last_context_status` tiene UN tipo: cadena. El éxito lo escribía como número
|
|
350
|
+
# y el fallo como cadena — el mismo campo con dos tipos según cómo fue, y quien lo lee no
|
|
351
|
+
# puede compararlo sin adivinar.
|
|
352
|
+
tipo="$(python3 -c "
|
|
353
|
+
import json
|
|
354
|
+
print(type(json.load(open('$PROJ/.claude/state/heartbeat-status.json')).get('last_context_status')).__name__)" 2>/dev/null)"
|
|
355
|
+
[ "$tipo" = str ] && echo "OK last_context_status es siempre una cadena (tambien en el exito)" \
|
|
356
|
+
|| { echo "FAIL last_context_status cambia de tipo segun el resultado ($tipo)"; fail=1; }
|
|
357
|
+
|
|
358
|
+
# Fallo de red en el refresco: la caché ANTERIOR queda intacta y marcada stale. Nunca dejar
|
|
359
|
+
# al agente sin proyección: eso lo convierte en huérfano (regresión del PR #60).
|
|
360
|
+
cat > "$TMP/routes-hb.json" <<JSON
|
|
361
|
+
{"PUT /leases/renew": {"status": 200, "body": {"expires_at": "2099-01-01T00:00:00Z"}},
|
|
362
|
+
"GET /agent/context": {"status": 500, "body": {"error": "boom"}},
|
|
363
|
+
"POST /events": {"status": 200, "body": {"results": []}}}
|
|
364
|
+
JSON
|
|
365
|
+
sleep 4
|
|
366
|
+
got="$(python3 -c "
|
|
367
|
+
import json
|
|
368
|
+
d=json.load(open('$PROJ/.claude/state/runtime-projection.json'))
|
|
369
|
+
print((d.get('active_slice') or {}).get('epic_code'))" 2>/dev/null)"
|
|
370
|
+
[ "$got" = "EP-077" ] && echo "OK un refresco fallido conserva la cache anterior" \
|
|
371
|
+
|| { echo "FAIL la cache se perdio con el fallo de red ($got)"; fail=1; }
|
|
372
|
+
stale="$(python3 -c "
|
|
373
|
+
import json
|
|
374
|
+
print(json.load(open('$PROJ/.claude/state/heartbeat-status.json')).get('context_stale'))" 2>/dev/null)"
|
|
375
|
+
[ "$stale" = True ] && echo "OK un refresco fallido marca la proyeccion stale" \
|
|
376
|
+
|| { echo "FAIL refresco fallido sin marcar stale ($stale)"; fail=1; }
|
|
377
|
+
|
|
378
|
+
# Los guards nunca leen una caché a medio escribir: mientras el daemon refresca en bucle,
|
|
379
|
+
# un lector concurrente parsea el fichero 300 veces sin un solo JSON inválido.
|
|
380
|
+
cat > "$TMP/routes-hb.json" <<JSON
|
|
381
|
+
{"PUT /leases/renew": {"status": 200, "body": {"expires_at": "2099-01-01T00:00:00Z"}},
|
|
382
|
+
"GET /agent/context": {"status": 200, "body": {"project": {}, "active_slice": {"slice_id": "sl-9", "epic_code": "EP-077", "phase": "green"}, "nudges": [], "context": {"manifest_hash": "sha256:v2", "version": 9}}},
|
|
383
|
+
"POST /events": {"status": 200, "body": {"results": []}}}
|
|
384
|
+
JSON
|
|
385
|
+
bad="$(python3 - "$PROJ/.claude/state/runtime-projection.json" <<'PY'
|
|
386
|
+
import json,sys,time
|
|
387
|
+
path=sys.argv[1]; bad=0
|
|
388
|
+
for _ in range(300):
|
|
389
|
+
try:
|
|
390
|
+
json.load(open(path))
|
|
391
|
+
except Exception:
|
|
392
|
+
bad+=1
|
|
393
|
+
time.sleep(0.01)
|
|
394
|
+
print(bad)
|
|
395
|
+
PY
|
|
396
|
+
)"
|
|
397
|
+
[ "$bad" = 0 ] && echo "OK el lector concurrente nunca ve la cache a medio escribir" \
|
|
398
|
+
|| { echo "FAIL $bad lecturas parciales de la proyeccion"; fail=1; }
|
|
399
|
+
|
|
400
|
+
# [#61 · ronda final] El refresco tiene DOS fallos distintos y no se pueden anunciar igual:
|
|
401
|
+
# «el hub no respondió» y «respondió 200 pero no pudimos escribir la proyección». Con un solo
|
|
402
|
+
# código, `slice-ops.sh status` imprimía «no pudo refrescar (último intento: 200)» y mandaba a
|
|
403
|
+
# mirar la red teniendo el problema en el disco. Fixture: la proyección es un DIRECTORIO, así
|
|
404
|
+
# que el `os.replace` final falla con el 200 ya en la mano.
|
|
405
|
+
rm -f "$PROJ/.claude/state/runtime-projection.json"
|
|
406
|
+
mkdir -p "$PROJ/.claude/state/runtime-projection.json"
|
|
407
|
+
sleep 4
|
|
408
|
+
ctxst="$(python3 -c "
|
|
409
|
+
import json
|
|
410
|
+
print(json.load(open('$PROJ/.claude/state/heartbeat-status.json')).get('last_context_status'))" 2>/dev/null)"
|
|
411
|
+
[ "$ctxst" = "write_error" ] && echo "OK un 200 que no se pudo escribir no se reporta como 200" \
|
|
412
|
+
|| { echo "FAIL el fallo de escritura se anuncia como estado HTTP ($ctxst)"; fail=1; }
|
|
413
|
+
rmdir "$PROJ/.claude/state/runtime-projection.json" 2>/dev/null
|
|
414
|
+
sleep 3
|
|
415
|
+
|
|
416
|
+
# Sin red, `runtime_http_status` vale `000` — que NO es un número JSON válido. Emitido sin
|
|
417
|
+
# comillas rompía el fragmento entero y `hb_write_status` no escribía NADA: ni el estado ni el
|
|
418
|
+
# `context_stale: true`, o sea el fallo desaparecía en silencio. El campo es SIEMPRE cadena.
|
|
419
|
+
python3 -c "
|
|
420
|
+
import json
|
|
421
|
+
d=json.load(open('$PROJ/.claude/state/heartbeat-status.json'))
|
|
422
|
+
d['context_stale']=False
|
|
423
|
+
json.dump(d,open('$PROJ/.claude/state/heartbeat-status.json','w'))"
|
|
424
|
+
kill "$STUB_HB" 2>/dev/null; wait "$STUB_HB" 2>/dev/null
|
|
425
|
+
sleep 4
|
|
426
|
+
ctx000="$(python3 -c "
|
|
427
|
+
import json
|
|
428
|
+
d=json.load(open('$PROJ/.claude/state/heartbeat-status.json'))
|
|
429
|
+
print(repr(d.get('last_context_status')), d.get('context_stale'))" 2>/dev/null)"
|
|
430
|
+
[ "$ctx000" = "'000' True" ] && echo "OK sin red el estado del refresco se registra como cadena \"000\"" \
|
|
431
|
+
|| { echo "FAIL el fragmento con 000 no se registro ($ctx000)"; fail=1; }
|
|
432
|
+
python3 "$ROOT/scripts/tests/lib/http-stub.py" "$PORT_HB" "$TMP/routes-hb.json" "$TMP/calls-hb.jsonl" &
|
|
433
|
+
STUB_HB=$!
|
|
434
|
+
sleep 0.5
|
|
435
|
+
|
|
436
|
+
# legacy|dual: el daemon NO refresca (AC de #61).
|
|
437
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
438
|
+
python3 -c "
|
|
439
|
+
import json
|
|
440
|
+
json.dump({'fetched_at':'2020-01-01T00:00:00Z','active_slice':{'slice_id':'sl-9','epic_code':'EP-VIEJA'},'nudges':[]},
|
|
441
|
+
open('$PROJ/.claude/state/runtime-projection.json','w'))"
|
|
442
|
+
: > "$TMP/calls-hb.jsonl"
|
|
443
|
+
TRYCORE_RUNTIME_MODE=dual bash "$ROOT/hooks/build/heartbeat.sh" --ensure "$SESSC" >/dev/null 2>&1
|
|
444
|
+
sleep 4
|
|
445
|
+
got="$(python3 -c "
|
|
446
|
+
import json
|
|
447
|
+
d=json.load(open('$PROJ/.claude/state/runtime-projection.json'))
|
|
448
|
+
print((d.get('active_slice') or {}).get('epic_code'))" 2>/dev/null)"
|
|
449
|
+
[ "$got" = "EP-VIEJA" ] && echo "OK en dual el daemon no refresca el contexto" \
|
|
450
|
+
|| { echo "FAIL el daemon refresco en dual ($got)"; fail=1; }
|
|
451
|
+
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
452
|
+
kill "$SESSC" 2>/dev/null; wait "$SESSC" 2>/dev/null
|
|
453
|
+
unset TRYCORE_CONTEXT_REFRESH_S
|
|
454
|
+
|
|
287
455
|
# 409 en el renew: se marca lease perdido y NO se apaga el daemon (spec §4.3).
|
|
456
|
+
# [#61] heartbeat-status.json es un MERGE, no un reemplazo: dos deberes del tick (lease y
|
|
457
|
+
# contexto) escriben el mismo fichero y ninguno puede borrar el diagnóstico del otro.
|
|
458
|
+
export TRYCORE_CONTEXT_REFRESH_S=1
|
|
288
459
|
cat > "$TMP/routes-hb.json" <<JSON
|
|
289
460
|
{"PUT /leases/renew": {"status": 409, "body": {"error": "lease_conflict"}},
|
|
461
|
+
"GET /agent/context": {"status": 200, "body": {"project": {"scaffold": {"confirmed": true}}, "active_slice": {"slice_id": "sl-9", "phase": "green"}, "nudges": [], "context": {"manifest_hash": "sha256:v2", "version": 9}}},
|
|
290
462
|
"POST /events": {"status": 200, "body": {"results": []}}}
|
|
291
463
|
JSON
|
|
292
464
|
sleep 60 & SESS3=$!
|
|
@@ -297,12 +469,22 @@ lost="$(python3 -c "import json;print(json.load(open('$PROJ/.claude/state/heartb
|
|
|
297
469
|
HBPID4="$(cat "$PROJ/.claude/state/heartbeat.pid" 2>/dev/null)"
|
|
298
470
|
kill -0 "$HBPID4" 2>/dev/null && echo "OK daemon sigue vivo tras 409" || { echo "FAIL daemon murio por 409"; fail=1; }
|
|
299
471
|
|
|
472
|
+
# El daemon lleva ya un 409 anotado (lease_lost: true). Un refresco de contexto posterior
|
|
473
|
+
# NO puede borrarlo.
|
|
474
|
+
sleep 4
|
|
475
|
+
merged="$(python3 -c "
|
|
476
|
+
import json
|
|
477
|
+
d=json.load(open('$PROJ/.claude/state/heartbeat-status.json'))
|
|
478
|
+
print('SI' if d.get('lease_lost') is True and 'context_stale' in d else 'NO')" 2>/dev/null)"
|
|
479
|
+
[ "$merged" = SI ] && echo "OK heartbeat-status conserva lease_lost tras el refresco de contexto" \
|
|
480
|
+
|| { echo "FAIL heartbeat-status se reemplaza en vez de mezclarse ($merged)"; fail=1; }
|
|
481
|
+
|
|
300
482
|
bash "$ROOT/hooks/build/heartbeat.sh" --stop >/dev/null 2>&1
|
|
301
483
|
sleep 1
|
|
302
484
|
kill -0 "$HBPID4" 2>/dev/null && { echo "FAIL --stop no apago el daemon"; fail=1; } || echo "OK --stop apaga el daemon"
|
|
303
485
|
kill "$SESS3" 2>/dev/null; wait "$SESS3" 2>/dev/null
|
|
304
486
|
kill "$STUB_HB" 2>/dev/null; wait "$STUB_HB" 2>/dev/null
|
|
305
|
-
unset TRYCORE_HEARTBEAT_TICK_S TRYCORE_HEARTBEAT_INTERVAL_S
|
|
487
|
+
unset TRYCORE_HEARTBEAT_TICK_S TRYCORE_HEARTBEAT_INTERVAL_S TRYCORE_CONTEXT_REFRESH_S
|
|
306
488
|
|
|
307
489
|
# --- session-start.sh ---
|
|
308
490
|
PORT_SS="$(python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()")"
|
|
@@ -458,10 +640,23 @@ rm -f "$PROJ/.claude/state/build-state.json"
|
|
|
458
640
|
# --- guards desde la caché de proyección (modo runtime) ---
|
|
459
641
|
HOOK_IN='{"tool_name":"Write","tool_input":{"file_path":"'"$PROJ"'/src/x.ts","content":"x"}}'
|
|
460
642
|
|
|
461
|
-
# Sin
|
|
643
|
+
# Sin caché hay DOS escenarios opuestos y el guard debe distinguirlos. Hasta 0.13.0
|
|
644
|
+
# los dos caían en el mismo exit 0, y por eso un agente desconectado del hub (todo
|
|
645
|
+
# worktree lo está: el estado no viaja) podía escribir código sin que nada lo
|
|
646
|
+
# parase, que es como dos workers acaban pisándose.
|
|
462
647
|
rm -f "$PROJ/.claude/state/runtime-projection.json"
|
|
648
|
+
|
|
649
|
+
# (a) Con credenciales = proyecto registrado que NO ve al hub → bloquea.
|
|
650
|
+
echo "$HOOK_IN" | bash "$ROOT/hooks/build/scaffold-guard.sh" >/dev/null 2>&1
|
|
651
|
+
[ $? -eq 2 ] && echo "OK scaffold-guard bloquea si está desconectado del hub" \
|
|
652
|
+
|| { echo "FAIL scaffold-guard no bloquea desconectado"; fail=1; }
|
|
653
|
+
|
|
654
|
+
# (b) Sin credenciales = el proyecto aún no usa el arnés → auto-arme, deja pasar.
|
|
655
|
+
mv "$PROJ/.claude/state/runtime.credentials" "$PROJ/.claude/state/creds.bak"
|
|
463
656
|
echo "$HOOK_IN" | bash "$ROOT/hooks/build/scaffold-guard.sh" >/dev/null 2>&1
|
|
464
|
-
[ $? -eq 0 ] && echo "OK scaffold-guard
|
|
657
|
+
[ $? -eq 0 ] && echo "OK scaffold-guard respeta el auto-arme sin credenciales" \
|
|
658
|
+
|| { echo "FAIL scaffold-guard rompe el auto-arme"; fail=1; }
|
|
659
|
+
mv "$PROJ/.claude/state/creds.bak" "$PROJ/.claude/state/runtime.credentials"
|
|
465
660
|
|
|
466
661
|
# Fase de código + scaffold NO confirmado -> bloquea.
|
|
467
662
|
runtime_projection_write "{\"fetched_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":{\"scaffold_confirmed\":false,\"design_source_applies\":true,\"design_source_confirmed\":false},\"active_slice\":{\"slice_id\":\"sl-11\",\"phase\":\"green\",\"gates\":{\"fidelity\":false},\"wiring_failing\":[]},\"nudges\":[]}"
|
|
@@ -173,6 +173,29 @@ else
|
|
|
173
173
|
echo "FAIL install: .gitignore no protege los artefactos del runtime"
|
|
174
174
|
fail=1
|
|
175
175
|
fi
|
|
176
|
+
# [Ronda de arreglo 1, Minor 7 / #63] El ledger de propuestas y el marcador de desfase de
|
|
177
|
+
# grafo son estado derivado del hub, igual que los de arriba: mismo blindaje.
|
|
178
|
+
if [ -f "$GI" ] \
|
|
179
|
+
&& grep -q '^\.claude/state/epic-proposals\.json$' "$GI" \
|
|
180
|
+
&& grep -q '^\.claude/state/graph-status\.json$' "$GI"; then
|
|
181
|
+
echo "OK install: .gitignore protege epic-proposals/graph-status"
|
|
182
|
+
else
|
|
183
|
+
echo "FAIL install: .gitignore no protege epic-proposals.json/graph-status.json"
|
|
184
|
+
fail=1
|
|
185
|
+
fi
|
|
186
|
+
# [Ronda final · C5] Los tres ficheros del daemon de heartbeat (EP-OR-08-B) son estado LOCAL
|
|
187
|
+
# de la máquina —un pid y unos ppids no significan nada en otro clon— y llevaban desde
|
|
188
|
+
# entonces fuera de la semilla: un consumidor los commiteaba y el siguiente `git pull` traía
|
|
189
|
+
# un pidfile ajeno que el relanzamiento del daemon toma por bueno hasta el `kill -0`.
|
|
190
|
+
if [ -f "$GI" ] \
|
|
191
|
+
&& grep -q '^\.claude/state/heartbeat\.pid$' "$GI" \
|
|
192
|
+
&& grep -q '^\.claude/state/heartbeat-sessions\.json$' "$GI" \
|
|
193
|
+
&& grep -q '^\.claude/state/heartbeat-status\.json$' "$GI"; then
|
|
194
|
+
echo "OK install: .gitignore protege los tres ficheros del heartbeat"
|
|
195
|
+
else
|
|
196
|
+
echo "FAIL install: .gitignore no protege heartbeat.pid/heartbeat-sessions.json/heartbeat-status.json"
|
|
197
|
+
fail=1
|
|
198
|
+
fi
|
|
176
199
|
rm -rf "$TMP_BF"
|
|
177
200
|
|
|
178
201
|
# 6) Ambiguo: manifiesto + código pero SIN repo git (sin historial) → project_kind null.
|