@trycore/spec-build-harness 0.11.1 → 0.13.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/VERSION +1 -1
- package/commands/build/onboard.md +25 -8
- package/dist/commands/doctor.js +12 -1
- package/dist/commands/status.js +9 -1
- package/dist/lib/normalize.js +11 -0
- package/dist/lib/runtime-client.js +34 -0
- package/dist/lib/state-bundle.js +15 -1
- package/docs/runtime/protocolo-cliente-runtime.md +30 -2
- package/hooks/build/lib/runtime-client.sh +42 -5
- package/hooks/build/lib/runtime-ops.sh +64 -0
- package/hooks/build/slice-ops.sh +137 -3
- package/package.json +1 -1
- package/scripts/lib/graph-bundle.py +165 -1
- package/scripts/tests/test-skill-ops.sh +356 -9
- package/skills/building-a-slice/SKILL.md +10 -0
- package/skills/building-a-slice/references/runtime-protocol.md +7 -2
- package/skills/openspec-archive-change/SKILL.md +4 -1
|
@@ -83,7 +83,7 @@ rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
|
83
83
|
# protegidos porque la capa de compat del despacho los traduce y entrega.)
|
|
84
84
|
for t in slice_archived wiring_item_updated slice_escalated gate_verdict checkpoint_recorded \
|
|
85
85
|
project_fact_updated wiring_checklist_seeded slice_submitted handoff_recorded telemetry_gap \
|
|
86
|
-
checkpoint_created verdict_reported escalation_raised; do
|
|
86
|
+
phase_advanced checkpoint_created verdict_reported escalation_raised; do
|
|
87
87
|
case " $RUNTIME_OUTBOX_PROTECTED " in
|
|
88
88
|
*" $t "*) : ;;
|
|
89
89
|
*) echo "FAIL $t no está en RUNTIME_OUTBOX_PROTECTED"; fail=1 ;;
|
|
@@ -394,6 +394,28 @@ rc=$?
|
|
|
394
394
|
echo "$out" | grep -q "modo:" && echo "$out" | grep -q "sl-7" && echo "$out" | grep -qi "cola" \
|
|
395
395
|
&& echo "OK status informa modo, slice y cola" || { echo "FAIL status incompleto: $out"; fail=1; }
|
|
396
396
|
|
|
397
|
+
# [issue #52] Visibilidad de outbox/rejected/: sin rechazados, status lo dice («ninguno»);
|
|
398
|
+
# con rechazados, reporta el conteo y la RAZÓN del más reciente — hoy un slice_archived
|
|
399
|
+
# rechazado era invisible (cola 0 pendientes = «éxito» aparente).
|
|
400
|
+
echo "$out" | grep -qi "rechazados" && echo "$out" | grep -qi "ninguno" \
|
|
401
|
+
&& echo "OK status sin rechazados reporta «ninguno»" || { echo "FAIL status sin línea de rechazados: $out"; fail=1; }
|
|
402
|
+
mkdir -p "$PROJ/.claude/state/outbox/rejected"
|
|
403
|
+
cat > "$PROJ/.claude/state/outbox/rejected/20260828000000000001-cid-rej-1.json" <<'JSON'
|
|
404
|
+
{"client_event_id": "cid-rej-1", "type": "progress_noted", "payload": {"note": "x"},
|
|
405
|
+
"enqueued_at": "2026-08-28T00:00:00.000000Z", "rejected_reason": "razón vieja"}
|
|
406
|
+
JSON
|
|
407
|
+
cat > "$PROJ/.claude/state/outbox/rejected/20260828000000000002-cid-rej-2.json" <<'JSON'
|
|
408
|
+
{"client_event_id": "cid-rej-2", "type": "slice_archived", "payload": {},
|
|
409
|
+
"enqueued_at": "2026-08-28T00:00:01.000000Z", "slice_id": "sl-7",
|
|
410
|
+
"rejected_reason": "archivar exige estar en la fase pr"}
|
|
411
|
+
JSON
|
|
412
|
+
out="$(bash "$SO" status --no-refresh)"
|
|
413
|
+
echo "$out" | grep -q "2 evento(s)" && echo "$out" | grep -q "⚠" \
|
|
414
|
+
&& echo "OK status reporta el conteo de rechazados con ⚠" || { echo "FAIL status sin conteo de rechazados: $out"; fail=1; }
|
|
415
|
+
echo "$out" | grep -q "slice_archived" && echo "$out" | grep -q "archivar exige estar en la fase pr" \
|
|
416
|
+
&& echo "OK status muestra tipo y razón del rechazo más reciente" || { echo "FAIL status sin razón del último rechazo: $out"; fail=1; }
|
|
417
|
+
rm -rf "$PROJ/.claude/state/outbox/rejected"
|
|
418
|
+
|
|
397
419
|
# status es informativo: NUNCA falla aunque no haya nada.
|
|
398
420
|
rm -f "$PROJ/.claude/state/runtime-projection.json"
|
|
399
421
|
rc=0; out="$(TRYCORE_RUNTIME_MODE=legacy bash "$SO" status --no-refresh)" || rc=$?
|
|
@@ -430,20 +452,20 @@ n="$(find "$PROJ/.claude/state/outbox" -name '*.json' 2>/dev/null | wc -l | tr -
|
|
|
430
452
|
|| { echo "FAIL escalate con outbox no escribible (rc=$rc n=$n out=$out)"; fail=1; }
|
|
431
453
|
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
432
454
|
|
|
433
|
-
# usage: lista los
|
|
434
|
-
# skills) —
|
|
455
|
+
# usage: lista los 14 subcomandos REALMENTE implementados (contrato con la prosa de las
|
|
456
|
+
# skills) — 13 del plan EP-OR-08-C + `phase` (issue #52).
|
|
435
457
|
usage_out="$(bash "$SO" --help 2>&1)"
|
|
436
|
-
for sub in mode claim next-step gate submit archive wiring progress checkpoint fact propose-asset status escalate; do
|
|
458
|
+
for sub in mode claim next-step phase gate submit archive wiring progress checkpoint fact propose-asset status escalate; do
|
|
437
459
|
echo "$usage_out" | grep -q -- "$sub" || { echo "FAIL usage no documenta $sub"; fail=1; }
|
|
438
460
|
done
|
|
439
461
|
echo "$usage_out" | grep -qi "pendientes" \
|
|
440
462
|
&& { echo "FAIL usage todavía anuncia pendientes tras completar la Task 4"; fail=1; } \
|
|
441
|
-
|| echo "OK usage documenta los
|
|
463
|
+
|| echo "OK usage documenta los 14 subcomandos como implementados, sin pendientes"
|
|
442
464
|
|
|
443
465
|
# Regresión de modo LEGACY sobre TODOS los subcomandos IMPLEMENTADOS que transicionan:
|
|
444
|
-
# rc 3 y cero red (los
|
|
466
|
+
# rc 3 y cero red (los 14, ya con phase del issue #52).
|
|
445
467
|
: > "$TMP/curl-legacy.log"
|
|
446
|
-
for sub in "gate tdd pass" "submit" "archive" "wiring update w1 passing" "progress algo" "checkpoint --note algo" "fact project-kind greenfield" "propose-asset --type-key t --path p --content-file $TMP/asset.md" "escalate algo --gate fidelity"; do
|
|
468
|
+
for sub in "phase pr" "gate tdd pass" "submit" "archive" "wiring update w1 passing" "progress algo" "checkpoint --note algo" "fact project-kind greenfield" "propose-asset --type-key t --path p --content-file $TMP/asset.md" "escalate algo --gate fidelity"; do
|
|
447
469
|
rc=0
|
|
448
470
|
bash -c "PATH=\"$TMP/fakebin:\$PATH\" FAKE_CURL_LOG=\"$TMP/curl-legacy.log\" TRYCORE_RUNTIME_MODE=legacy CLAUDE_PROJECT_DIR=\"$PROJ\" BUILD_CONFIG_FILE=\"$BUILD_CONFIG_FILE\" bash \"$SO\" $sub" >/dev/null 2>&1 || rc=$?
|
|
449
471
|
[ "$rc" = 3 ] || { echo "FAIL «${sub}» en legacy devolvió $rc (esperado 3)"; fail=1; }
|
|
@@ -694,6 +716,186 @@ n="$(find "$PROJ/.claude/state/outbox" -name '*.json' 2>/dev/null | wc -l | tr -
|
|
|
694
716
|
|| { echo "FAIL archive con outbox no escribible (rc=$rc n=$n out=$out)"; fail=1; }
|
|
695
717
|
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
696
718
|
|
|
719
|
+
# ══════════════ slice-ops.sh: phase (cadena de fases del hub) [issue #52] ══════════════
|
|
720
|
+
# El hub modela la fase con `phase_advanced {to}` de ORDEN ESTRICTO (domain.py:400-407,
|
|
721
|
+
# PHASES dor→…→pr→archived) y `slice_archived` exige phase == pr (domain.py:499-500).
|
|
722
|
+
# Sin este subcomando el slice se quedaba en dor server-side y el archivado caía a
|
|
723
|
+
# rejected/ EN SILENCIO (piloto 2026-08-28).
|
|
724
|
+
|
|
725
|
+
# Sin argumento: uso incorrecto.
|
|
726
|
+
rc=0; bash "$SO" phase >/dev/null 2>&1 || rc=$?
|
|
727
|
+
[ "$rc" = 2 ] && echo "OK phase sin argumento -> rc 2" || { echo "FAIL phase sin argumento rc ($rc)"; fail=1; }
|
|
728
|
+
|
|
729
|
+
# Fase desconocida (o archived, que no se reporta por aquí): rechazo LOCAL, sin red ni cola.
|
|
730
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
731
|
+
: > "$TMP/calls-ga.jsonl"
|
|
732
|
+
rc=0; bash "$SO" phase inventada >/dev/null 2>&1 || rc=$?
|
|
733
|
+
n="$(find "$PROJ/.claude/state/outbox" -name '*.json' 2>/dev/null | wc -l | tr -d ' ')"
|
|
734
|
+
[ "$rc" = 6 ] && [ "$n" = 0 ] && echo "OK phase desconocida -> rc 6 sin encolar" || { echo "FAIL phase desconocida (rc=$rc n=$n)"; fail=1; }
|
|
735
|
+
rc=0; bash "$SO" phase archived >/dev/null 2>&1 || rc=$?
|
|
736
|
+
[ "$rc" = 6 ] && echo "OK phase archived -> rc 6 (archived se alcanza con archive, no con phase)" || { echo "FAIL phase archived rc ($rc)"; fail=1; }
|
|
737
|
+
|
|
738
|
+
# Runtime, un paso: el servidor conoce «red» -> `phase green` encola EXACTAMENTE un
|
|
739
|
+
# phase_advanced {to: green} con slice_id.
|
|
740
|
+
python3 - "$TMP/routes-ga.json" <<'PYX'
|
|
741
|
+
import json,sys
|
|
742
|
+
p=sys.argv[1]; d=json.load(open(p))
|
|
743
|
+
d["GET /agent/context"]={"status":200,"body":{"project":{"scaffold_confirmed":True},"active_slice":{"slice_id":"sl-7","phase":"red","gates":{"dor":True}},"context":{"manifest_hash":"sha256:v1"}}}
|
|
744
|
+
json.dump(d,open(p,"w"))
|
|
745
|
+
PYX
|
|
746
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
747
|
+
rc=0; out="$(bash "$SO" phase green)" || rc=$?
|
|
748
|
+
[ "$rc" = 0 ] && echo "OK phase green -> rc 0" || { echo "FAIL phase green rc ($rc)"; fail=1; }
|
|
749
|
+
[ "$(python3 - "$PROJ/.claude/state/outbox" <<'PYX'
|
|
750
|
+
import json,sys,glob,os
|
|
751
|
+
evs=[json.load(open(p)) for p in sorted(glob.glob(os.path.join(sys.argv[1],"*.json"))) if not os.path.basename(p).startswith(".")]
|
|
752
|
+
pa=[e for e in evs if e.get("type")=="phase_advanced"]
|
|
753
|
+
ok = len(evs)==1 and len(pa)==1 and pa[0]["payload"]=={"to":"green"} and pa[0].get("slice_id")=="sl-7"
|
|
754
|
+
print("SI" if ok else "NO")
|
|
755
|
+
PYX
|
|
756
|
+
)" = SI ] && echo "OK phase green encola un único phase_advanced {to} con slice_id" \
|
|
757
|
+
|| { echo "FAIL evento(s) de phase green"; fail=1; }
|
|
758
|
+
|
|
759
|
+
# Cadena con huecos: el servidor sigue en «red»; `phase pr` emite green→…→pr EN ORDEN
|
|
760
|
+
# (el orden estricto del hub no admite saltos) y avisa del hueco.
|
|
761
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
762
|
+
rc=0; out="$(bash "$SO" phase pr 2>&1)" || rc=$?
|
|
763
|
+
[ "$rc" = 0 ] && echo "OK phase pr con huecos -> rc 0" || { echo "FAIL phase pr rc ($rc)"; fail=1; }
|
|
764
|
+
[ "$(python3 - "$PROJ/.claude/state/outbox" <<'PYX'
|
|
765
|
+
import json,sys,glob,os
|
|
766
|
+
tos=[json.load(open(p))["payload"].get("to") for p in sorted(glob.glob(os.path.join(sys.argv[1],"*.json")))
|
|
767
|
+
if not os.path.basename(p).startswith(".") and json.load(open(p)).get("type")=="phase_advanced"]
|
|
768
|
+
print("SI" if tos==["green","refactor","smoke","api","data","dod","pr"] else "NO:"+",".join(str(t) for t in tos))
|
|
769
|
+
PYX
|
|
770
|
+
)" = SI ] && echo "OK phase pr encola la cadena green→…→pr en orden" || { echo "FAIL cadena de phase pr"; fail=1; }
|
|
771
|
+
echo "$out" | grep -q "⚠" && echo "OK phase pr avisa del hueco rellenado" || { echo "FAIL phase pr sin aviso de hueco ($out)"; fail=1; }
|
|
772
|
+
|
|
773
|
+
# Idempotencia contra la COLA: la cadena ya está pendiente en la outbox -> repetir
|
|
774
|
+
# `phase pr` no encola nada nuevo (el daemon aún no drenó, pero el cliente ya lo sabe).
|
|
775
|
+
n_antes="$(find "$PROJ/.claude/state/outbox" -name '*.json' ! -name '.dispatch-state.json' | wc -l | tr -d ' ')"
|
|
776
|
+
rc=0; bash "$SO" phase pr >/dev/null 2>&1 || rc=$?
|
|
777
|
+
n_despues="$(find "$PROJ/.claude/state/outbox" -name '*.json' ! -name '.dispatch-state.json' | wc -l | tr -d ' ')"
|
|
778
|
+
[ "$rc" = 0 ] && [ "$n_antes" = "$n_despues" ] \
|
|
779
|
+
&& echo "OK phase pr repetido es idempotente (cuenta la cola pendiente)" \
|
|
780
|
+
|| { echo "FAIL idempotencia por cola (antes=$n_antes despues=$n_despues rc=$rc)"; fail=1; }
|
|
781
|
+
|
|
782
|
+
# Idempotencia contra la PROYECCIÓN: el servidor ya está en pr -> nada que emitir.
|
|
783
|
+
python3 - "$TMP/routes-ga.json" <<'PYX'
|
|
784
|
+
import json,sys
|
|
785
|
+
p=sys.argv[1]; d=json.load(open(p))
|
|
786
|
+
d["GET /agent/context"]={"status":200,"body":{"project":{"scaffold_confirmed":True},"active_slice":{"slice_id":"sl-7","phase":"pr","gates":{"dor":True}},"context":{"manifest_hash":"sha256:v1"}}}
|
|
787
|
+
json.dump(d,open(p,"w"))
|
|
788
|
+
PYX
|
|
789
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
790
|
+
rc=0; out="$(bash "$SO" phase pr)" || rc=$?
|
|
791
|
+
n="$(find "$PROJ/.claude/state/outbox" -name '*.json' 2>/dev/null | wc -l | tr -d ' ')"
|
|
792
|
+
[ "$rc" = 0 ] && [ "$n" = 0 ] && echo "OK phase pr con el servidor ya en pr -> no-op idempotente" \
|
|
793
|
+
|| { echo "FAIL idempotencia por proyección (rc=$rc n=$n out=$out)"; fail=1; }
|
|
794
|
+
|
|
795
|
+
# Catch-up en archive [issue #52]: slice_archived exige phase == pr — si el servidor quedó
|
|
796
|
+
# atrás (pipeline corrido con una skill vieja, sin llamadas a `phase`), archive emite la
|
|
797
|
+
# cadena que falte ANTES del archivado, con aviso. Es el workaround del piloto hecho sistema.
|
|
798
|
+
python3 - "$TMP/routes-ga.json" <<'PYX'
|
|
799
|
+
import json,sys
|
|
800
|
+
p=sys.argv[1]; d=json.load(open(p))
|
|
801
|
+
d["GET /agent/context"]={"status":200,"body":{"project":{"scaffold_confirmed":True},"active_slice":{"slice_id":"sl-7","phase":"dor","gates":{"dor":True}},"context":{"manifest_hash":"sha256:v1"}}}
|
|
802
|
+
json.dump(d,open(p,"w"))
|
|
803
|
+
PYX
|
|
804
|
+
agent_context_fetch_and_cache >/dev/null
|
|
805
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
806
|
+
rc=0; out="$(bash "$SO" archive --epic EP-003 2>&1)" || rc=$?
|
|
807
|
+
[ "$rc" = 0 ] && echo "OK archive con catch-up -> rc 0" || { echo "FAIL archive catch-up rc ($rc)"; fail=1; }
|
|
808
|
+
[ "$(python3 - "$PROJ/.claude/state/outbox" <<'PYX'
|
|
809
|
+
import json,sys,glob,os
|
|
810
|
+
evs=[json.load(open(p)) for p in sorted(glob.glob(os.path.join(sys.argv[1],"*.json"))) if not os.path.basename(p).startswith(".")]
|
|
811
|
+
tos=[e["payload"].get("to") for e in evs if e.get("type")=="phase_advanced"]
|
|
812
|
+
tipos=[e.get("type") for e in evs]
|
|
813
|
+
ok = tos==["change","red","green","refactor","smoke","api","data","dod","pr"] \
|
|
814
|
+
and tipos[-1]=="slice_archived" and all(t=="phase_advanced" for t in tipos[:-1]) \
|
|
815
|
+
and all(e.get("slice_id")=="sl-7" for e in evs)
|
|
816
|
+
print("SI" if ok else "NO:"+",".join(tipos))
|
|
817
|
+
PYX
|
|
818
|
+
)" = SI ] && echo "OK archive encola la cadena dor→…→pr ANTES de slice_archived" \
|
|
819
|
+
|| { echo "FAIL catch-up de archive"; fail=1; }
|
|
820
|
+
echo "$out" | grep -q "server-side" && echo "OK archive avisa del catch-up (slice atrasado server-side)" \
|
|
821
|
+
|| { echo "FAIL archive sin aviso de catch-up ($out)"; fail=1; }
|
|
822
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
823
|
+
|
|
824
|
+
# archive con el servidor ya en pr: SOLO slice_archived, sin cadena ni aviso.
|
|
825
|
+
python3 - "$TMP/routes-ga.json" <<'PYX'
|
|
826
|
+
import json,sys
|
|
827
|
+
p=sys.argv[1]; d=json.load(open(p))
|
|
828
|
+
d["GET /agent/context"]={"status":200,"body":{"project":{"scaffold_confirmed":True},"active_slice":{"slice_id":"sl-7","phase":"pr","gates":{"dor":True}},"context":{"manifest_hash":"sha256:v1"}}}
|
|
829
|
+
json.dump(d,open(p,"w"))
|
|
830
|
+
PYX
|
|
831
|
+
agent_context_fetch_and_cache >/dev/null
|
|
832
|
+
rc=0; out="$(bash "$SO" archive --epic EP-003 2>&1)" || rc=$?
|
|
833
|
+
n_pa="$(grep -l '"type": "phase_advanced"' "$PROJ/.claude/state/outbox/"*.json 2>/dev/null | wc -l | tr -d ' ')"
|
|
834
|
+
n_ar="$(grep -l '"type": "slice_archived"' "$PROJ/.claude/state/outbox/"*.json 2>/dev/null | wc -l | tr -d ' ')"
|
|
835
|
+
[ "$rc" = 0 ] && [ "$n_pa" = 0 ] && [ "$n_ar" = 1 ] && ! echo "$out" | grep -q "server-side" \
|
|
836
|
+
&& echo "OK archive con el servidor en pr no emite cadena ni aviso" \
|
|
837
|
+
|| { echo "FAIL archive sin catch-up (rc=$rc pa=$n_pa ar=$n_ar)"; fail=1; }
|
|
838
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
839
|
+
|
|
840
|
+
# dual: phase ESPEJA (phase_advanced ∈ TIPOS_DE_ESPEJO del hub) con la identidad local,
|
|
841
|
+
# sin tocar la cola — mismo patrón que gate/submit en dual.
|
|
842
|
+
cat > "$PROJ/.claude/state/build-state.json" <<'JSON'
|
|
843
|
+
{"version":"1.0.0","active_slice":{"epica":"EP-009","openspec_change":"add-pagos","branch":"feature/pagos","phase":"green","gates":{"dor":true}}}
|
|
844
|
+
JSON
|
|
845
|
+
python3 - "$TMP/routes-ga.json" <<'PYX'
|
|
846
|
+
import json,sys
|
|
847
|
+
p=sys.argv[1]; d=json.load(open(p))
|
|
848
|
+
d["GET /agent/context"]={"status":200,"body":{"project":{"scaffold_confirmed":True},"active_slice":{"slice_id":"sl-7","phase":"red","gates":{"dor":True}},"context":{"manifest_hash":"sha256:v1"}}}
|
|
849
|
+
json.dump(d,open(p,"w"))
|
|
850
|
+
PYX
|
|
851
|
+
: > "$TMP/calls-ga.jsonl"
|
|
852
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
853
|
+
rc=0; TRYCORE_RUNTIME_MODE=dual bash "$SO" phase green >/dev/null 2>&1 || rc=$?
|
|
854
|
+
n="$(find "$PROJ/.claude/state/outbox" -name '*.json' 2>/dev/null | wc -l | tr -d ' ')"
|
|
855
|
+
[ "$rc" = 0 ] && [ "$n" = 0 ] && echo "OK phase en dual -> rc 0 sin encolar" || { echo "FAIL phase dual (rc=$rc n=$n)"; fail=1; }
|
|
856
|
+
[ "$(python3 - "$TMP/calls-ga.jsonl" <<'PYX'
|
|
857
|
+
import json,sys
|
|
858
|
+
tos=[]
|
|
859
|
+
for line in open(sys.argv[1]):
|
|
860
|
+
c=json.loads(line)
|
|
861
|
+
if c["path"]=="/projects/p-1/mirror/transitions":
|
|
862
|
+
b=json.loads(c["body"])
|
|
863
|
+
if b.get("kind")=="phase_advanced":
|
|
864
|
+
tos.append((b["payload"].get("to"), b["payload"].get("slice_ref",{}).get("epic_code")))
|
|
865
|
+
print("SI" if tos==[("green","EP-009")] else "NO:"+repr(tos))
|
|
866
|
+
PYX
|
|
867
|
+
)" = SI ] && echo "OK phase en dual espeja phase_advanced {to} con slice_ref" \
|
|
868
|
+
|| { echo "FAIL espejo de phase en dual"; fail=1; }
|
|
869
|
+
|
|
870
|
+
# dual: archive también hace catch-up por el espejo (cadena red→…→pr antes del archivado).
|
|
871
|
+
: > "$TMP/calls-ga.jsonl"
|
|
872
|
+
rc=0; out="$(TRYCORE_RUNTIME_MODE=dual bash "$SO" archive --epic EP-009 2>&1)" || rc=$?
|
|
873
|
+
[ "$rc" = 0 ] && echo "$out" | grep -q "server-side" \
|
|
874
|
+
&& echo "OK archive en dual avisa del catch-up" || { echo "FAIL archive dual catch-up (rc=$rc out=$out)"; fail=1; }
|
|
875
|
+
[ "$(python3 - "$TMP/calls-ga.jsonl" <<'PYX'
|
|
876
|
+
import json,sys
|
|
877
|
+
kinds=[]
|
|
878
|
+
for line in open(sys.argv[1]):
|
|
879
|
+
c=json.loads(line)
|
|
880
|
+
if c["path"]=="/projects/p-1/mirror/transitions":
|
|
881
|
+
b=json.loads(c["body"])
|
|
882
|
+
kinds.append((b.get("kind"), b["payload"].get("to")))
|
|
883
|
+
esperado=[("phase_advanced",t) for t in ["green","refactor","smoke","api","data","dod","pr"]]+[("slice_archived",None)]
|
|
884
|
+
print("SI" if kinds==esperado else "NO:"+repr(kinds))
|
|
885
|
+
PYX
|
|
886
|
+
)" = SI ] && echo "OK archive en dual espeja la cadena hasta pr y luego slice_archived" \
|
|
887
|
+
|| { echo "FAIL cadena del archive dual"; fail=1; }
|
|
888
|
+
|
|
889
|
+
# Restaura la ruta y la caché al estado que esperan los bloques siguientes (sl-7, fase tdd).
|
|
890
|
+
python3 - "$TMP/routes-ga.json" <<'PYX'
|
|
891
|
+
import json,sys
|
|
892
|
+
p=sys.argv[1]; d=json.load(open(p))
|
|
893
|
+
d["GET /agent/context"]={"status":200,"body":{"project":{"scaffold_confirmed":True},"active_slice":{"slice_id":"sl-7","phase":"tdd","gates":{"dor":True,"tdd":False}},"context":{"manifest_hash":"sha256:v1"}}}
|
|
894
|
+
json.dump(d,open(p,"w"))
|
|
895
|
+
PYX
|
|
896
|
+
agent_context_fetch_and_cache >/dev/null
|
|
897
|
+
rm -f "$PROJ/.claude/state/outbox/"*.json
|
|
898
|
+
|
|
697
899
|
# dual: el fichero es primario -> se espeja la transición con la identidad LOCAL.
|
|
698
900
|
cat > "$PROJ/.claude/state/build-state.json" <<'JSON'
|
|
699
901
|
{"version":"1.0.0","active_slice":{"epica":"EP-009","openspec_change":"add-pagos","branch":"feature/pagos","phase":"tdd","gates":{"dor":true}}}
|
|
@@ -1036,6 +1238,14 @@ nr="$(find "$PROJ/.claude/state/outbox/rejected" -name '*.json' 2>/dev/null | wc
|
|
|
1036
1238
|
[ "$n" = 0 ] && [ "$nr" = 1 ] \
|
|
1037
1239
|
&& echo "OK el elemento rechazado por el hub se aparta a rejected/ (no se reenvía)" \
|
|
1038
1240
|
|| { echo "FAIL elemento rechazado (pendientes=$n rechazados=$nr)"; fail=1; }
|
|
1241
|
+
# [issue #52] La razón del hub queda PERSISTIDA en el fichero apartado (rejected_reason):
|
|
1242
|
+
# es lo que `slice-ops.sh status` y `trycore-build status/doctor` muestran después.
|
|
1243
|
+
fr="$(find "$PROJ/.claude/state/outbox/rejected" -name '*.json' | head -1)"
|
|
1244
|
+
[ -n "$fr" ] && [ "$(python3 -c "
|
|
1245
|
+
import json;d=json.load(open('$fr'))
|
|
1246
|
+
print('SI' if 'desconocido para el catálogo v2' in (d.get('rejected_reason') or '') else 'NO')")" = SI ] \
|
|
1247
|
+
&& echo "OK el fichero apartado conserva la razón del hub (rejected_reason)" \
|
|
1248
|
+
|| { echo "FAIL rejected/ sin rejected_reason persistida"; fail=1; }
|
|
1039
1249
|
# [#44] El fichero 0.10.x `verdict_reported {gate, status}` viajó TRADUCIDO al catálogo v2
|
|
1040
1250
|
# (gate_verdict {gate, verdict: PASS}) — la traducción vive en el único punto de salida.
|
|
1041
1251
|
[ "$(python3 - "$TMP/calls-ga.jsonl" <<'PYX'
|
|
@@ -1107,6 +1317,15 @@ nr="$(find "$PROJ/.claude/state/outbox/rejected" -name '*.json' 2>/dev/null | wc
|
|
|
1107
1317
|
[ "$rc" = 0 ] && [ "$n" = 0 ] && [ "$nr" = 3 ] && ! grep -q '"path": "/events"' "$TMP/calls-ga.jsonl" \
|
|
1108
1318
|
&& echo "OK los tipos sin equivalente se apartan localmente a rejected/ sin tocar la red" \
|
|
1109
1319
|
|| { echo "FAIL pre-filtro de tipos sin equivalente (rc=$rc pendientes=$n rechazados=$nr)"; fail=1; }
|
|
1320
|
+
# [issue #52] También el apartado local persiste su razón (rejected_reason).
|
|
1321
|
+
[ "$(python3 - "$PROJ/.claude/state/outbox/rejected" <<'PYX'
|
|
1322
|
+
import json,glob,os,sys
|
|
1323
|
+
ok=all("catálogo" in (json.load(open(p)).get("rejected_reason") or "")
|
|
1324
|
+
for p in glob.glob(os.path.join(sys.argv[1],"*.json")))
|
|
1325
|
+
print("SI" if ok else "NO")
|
|
1326
|
+
PYX
|
|
1327
|
+
)" = SI ] && echo "OK los tipos sin equivalente llevan rejected_reason" \
|
|
1328
|
+
|| { echo "FAIL apartado local sin rejected_reason"; fail=1; }
|
|
1110
1329
|
rm -rf "$PROJ/.claude/state/outbox/rejected"
|
|
1111
1330
|
|
|
1112
1331
|
# [#44] telemetry_gap de la evicción con el payload del catálogo: {dropped, window_h,
|
|
@@ -1154,6 +1373,13 @@ n="$(find "$PROJ/.claude/state/outbox" -maxdepth 1 -name '*.json' ! -name '.disp
|
|
|
1154
1373
|
nr="$(find "$PROJ/.claude/state/outbox/rejected" -name '*.json' 2>/dev/null | wc -l | tr -d ' ')"
|
|
1155
1374
|
[ "$n" = 0 ] && [ "$nr" = 1 ] && echo "OK el lote 4xx se mueve a rejected/ y no se reintenta" \
|
|
1156
1375
|
|| { echo "FAIL lote 4xx (pendientes=$n rechazados=$nr)"; fail=1; }
|
|
1376
|
+
# [issue #52] El lote 4xx también persiste su razón (HTTP status) en cada fichero apartado.
|
|
1377
|
+
fr="$(find "$PROJ/.claude/state/outbox/rejected" -name '*.json' | head -1)"
|
|
1378
|
+
[ -n "$fr" ] && [ "$(python3 -c "
|
|
1379
|
+
import json;d=json.load(open('$fr'))
|
|
1380
|
+
print('SI' if '422' in (d.get('rejected_reason') or '') else 'NO')")" = SI ] \
|
|
1381
|
+
&& echo "OK el lote 4xx conserva la razón (HTTP 422) en rejected_reason" \
|
|
1382
|
+
|| { echo "FAIL lote 4xx sin rejected_reason"; fail=1; }
|
|
1157
1383
|
echo "$err" | grep -q "dict_type" && echo "OK el 4xx loggea el cuerpo del error del hub" \
|
|
1158
1384
|
|| { echo "FAIL 4xx sin cuerpo del error ($err)"; fail=1; }
|
|
1159
1385
|
[ ! -f "$PROJ/.claude/state/outbox/.dispatch-state.json" ] \
|
|
@@ -1202,6 +1428,7 @@ CATALOGO = {
|
|
|
1202
1428
|
"handoff_recorded": {"claves": {"note", "resume_hint", "stopped_at"},
|
|
1203
1429
|
"obligatorias": {"note"}, "ambito": "slice"},
|
|
1204
1430
|
"slice_escalated": {"claves": {"cause"}, "obligatorias": {"cause"}, "ambito": "slice"},
|
|
1431
|
+
"phase_advanced": {"claves": {"to"}, "obligatorias": {"to"}, "ambito": "slice"},
|
|
1205
1432
|
"slice_submitted": {"claves": set(), "obligatorias": set(), "ambito": "slice"},
|
|
1206
1433
|
"slice_archived": {"claves": set(), "obligatorias": set(), "ambito": "slice"},
|
|
1207
1434
|
"wiring_checklist_seeded": {"claves": {"items"}, "obligatorias": {"items"}, "ambito": "slice"},
|
|
@@ -1240,7 +1467,8 @@ fuera=sorted(t for t in encontrados if t not in catalogo)
|
|
|
1240
1467
|
# el guard está ciego y también es un fallo.
|
|
1241
1468
|
esperados={"gate_verdict","checkpoint_recorded","progress_noted","slice_escalated",
|
|
1242
1469
|
"slice_submitted","slice_archived","wiring_checklist_seeded",
|
|
1243
|
-
"wiring_item_updated","project_fact_updated","handoff_recorded"
|
|
1470
|
+
"wiring_item_updated","project_fact_updated","handoff_recorded",
|
|
1471
|
+
"phase_advanced"}
|
|
1244
1472
|
ciegos=sorted(esperados-encontrados)
|
|
1245
1473
|
if fuera:
|
|
1246
1474
|
print("FUERA_DE_CATALOGO:"+",".join(fuera))
|
|
@@ -1258,6 +1486,7 @@ PYX
|
|
|
1258
1486
|
runtime_credentials_merge '{"runtime_url":"http://127.0.0.1:9"}'
|
|
1259
1487
|
rm -f "$PROJ/.claude/state/outbox/"*.json "$PROJ/.claude/state/outbox/.dispatch-state.json"
|
|
1260
1488
|
bash "$SO" gate tdd pass --evidence "verde" >/dev/null 2>&1
|
|
1489
|
+
bash "$SO" phase red >/dev/null 2>&1
|
|
1261
1490
|
bash "$SO" submit --pr-url "https://example.invalid/pr/9" >/dev/null 2>&1
|
|
1262
1491
|
bash "$SO" archive --epic EP-003 >/dev/null 2>&1
|
|
1263
1492
|
bash "$SO" wiring seed --file "$TMP/items-small.json" >/dev/null 2>&1
|
|
@@ -1303,7 +1532,7 @@ for path in sorted(glob.glob(os.path.join(sys.argv[2],"*.json"))):
|
|
|
1303
1532
|
errores.append("wiring_checklist_seeded: item fuera de schema %r" % it); break
|
|
1304
1533
|
esperados={"gate_verdict","slice_submitted","slice_archived","wiring_checklist_seeded",
|
|
1305
1534
|
"wiring_item_updated","progress_noted","checkpoint_recorded","slice_escalated",
|
|
1306
|
-
"project_fact_updated","telemetry_gap"}
|
|
1535
|
+
"project_fact_updated","telemetry_gap","phase_advanced"}
|
|
1307
1536
|
faltantes=sorted(esperados-vistos)
|
|
1308
1537
|
if faltantes:
|
|
1309
1538
|
errores.append("productores sin evento encolado (¿cambió un subcomando?): %s" % faltantes)
|
|
@@ -1364,4 +1593,122 @@ echo 'no-json' | python3 "$GB" > /dev/null 2> "$TMP/gb3.err"
|
|
|
1364
1593
|
rc=$?
|
|
1365
1594
|
[ "$rc" = 1 ] && ! grep -q "Traceback" "$TMP/gb3.err" && echo "OK graph-bundle con entrada ilegible -> rc 1 limpio" || { echo "FAIL entrada ilegible ($rc)"; fail=1; }
|
|
1366
1595
|
|
|
1596
|
+
# ══════════ graph-bundle.py --from-docs / --print-deps [issue #56] ══════════
|
|
1597
|
+
# El grafo llegaba al hub con `depends_on: []` (41/41 épicas en el piloto): la
|
|
1598
|
+
# extracción vivía en el LLM y el ejemplo de onboard anclaba en []. El extractor
|
|
1599
|
+
# es ahora DETERMINISTA y lee la prosa de `docs/03-backlog/epicas.md`.
|
|
1600
|
+
mkdir -p "$TMP/docs"
|
|
1601
|
+
cat > "$TMP/docs/epicas.md" <<'MD'
|
|
1602
|
+
# Backlog — épicas
|
|
1603
|
+
|
|
1604
|
+
**Dependencias entre épicas**: EP-999 (esto está FUERA de toda sección: se ignora)
|
|
1605
|
+
|
|
1606
|
+
## EP-001 — Cimiento
|
|
1607
|
+
|
|
1608
|
+
**Depende de**: nada.
|
|
1609
|
+
|
|
1610
|
+
**Resumen**: base técnica.
|
|
1611
|
+
|
|
1612
|
+
---
|
|
1613
|
+
|
|
1614
|
+
## EP-002 — Cobros
|
|
1615
|
+
|
|
1616
|
+
**Anexo A**: Fase 3 · **Depende de**: **EP-001** (base de datos), EP-404 (no existe en el grafo)
|
|
1617
|
+
|
|
1618
|
+
**Resumen**: cobra.
|
|
1619
|
+
|
|
1620
|
+
## EP-003 — Reportes
|
|
1621
|
+
|
|
1622
|
+
**Depende de**: EP-002 (pipeline de export),
|
|
1623
|
+
EP-001 (cimiento). Ninguna bloquea: todas archivadas.
|
|
1624
|
+
|
|
1625
|
+
**Resumen**: reporta.
|
|
1626
|
+
|
|
1627
|
+
## EP-004 — Estructurada
|
|
1628
|
+
|
|
1629
|
+
depende_de: [EP-003]
|
|
1630
|
+
|
|
1631
|
+
**Depende de**: EP-001 en prosa — gana el campo estructurado.
|
|
1632
|
+
|
|
1633
|
+
## EP-005 — Autorreferente
|
|
1634
|
+
|
|
1635
|
+
**Depende de**: EP-005 (a sí misma: se excluye), EP-001
|
|
1636
|
+
MD
|
|
1637
|
+
|
|
1638
|
+
# 1) --print-deps: extracción pura, determinista, inspeccionable.
|
|
1639
|
+
out="$(python3 "$GB" --print-deps "$TMP/docs/epicas.md" 2> "$TMP/gb4.err")"
|
|
1640
|
+
rc=$?
|
|
1641
|
+
# `**Depende de**: nada.` es una declaración, no una ausencia: entra con lista vacía.
|
|
1642
|
+
esperado='{"EP-001": [], "EP-002": ["EP-001", "EP-404"], "EP-003": ["EP-001", "EP-002"], "EP-004": ["EP-003"], "EP-005": ["EP-001"]}'
|
|
1643
|
+
[ "$rc" = 0 ] && [ "$(echo "$out" | python3 -c 'import json,sys;print(json.dumps(json.load(sys.stdin),sort_keys=True))')" = "$esperado" ] \
|
|
1644
|
+
&& echo "OK --print-deps extrae de la prosa (multilínea, campo estructurado, sin autorreferencia)" \
|
|
1645
|
+
|| { echo "FAIL --print-deps ($rc) -> $out"; fail=1; }
|
|
1646
|
+
|
|
1647
|
+
# 2) --from-docs puebla el grafo aunque el JSON del modelo traiga depends_on vacíos.
|
|
1648
|
+
cat > "$TMP/epics56.json" <<'JSON'
|
|
1649
|
+
{"project_ref":"acme","epics":[
|
|
1650
|
+
{"code":"EP-001","title":"Cimiento","layer":"foundational","depends_on":[]},
|
|
1651
|
+
{"code":"EP-002","title":"Cobros","layer":"business","depends_on":[]},
|
|
1652
|
+
{"code":"EP-003","title":"Reportes","layer":"business","depends_on":[]},
|
|
1653
|
+
{"code":"EP-004","title":"Estructurada","layer":"business","depends_on":["EP-002"]}
|
|
1654
|
+
]}
|
|
1655
|
+
JSON
|
|
1656
|
+
out="$(python3 "$GB" --from-docs "$TMP/docs/epicas.md" < "$TMP/epics56.json" 2> "$TMP/gb5.err")"
|
|
1657
|
+
rc=$?
|
|
1658
|
+
[ "$rc" = 0 ] && [ "$(echo "$out" | python3 -c 'import json,sys;d=json.load(sys.stdin);print(";".join(e["code"]+"="+",".join(e["depends_on"]) for e in d["epics"]))')" = "EP-001=;EP-002=EP-001,EP-404;EP-003=EP-001,EP-002;EP-004=EP-002,EP-003" ] \
|
|
1659
|
+
&& echo "OK --from-docs puebla depends_on desde los docs (unión con lo declarado)" \
|
|
1660
|
+
|| { echo "FAIL --from-docs ($rc) -> $out"; fail=1; }
|
|
1661
|
+
grep -q "EP-404" "$TMP/gb5.err" \
|
|
1662
|
+
&& echo "OK --from-docs mantiene el aviso de dependencia inexistente" || { echo "FAIL sin aviso de dep inexistente con --from-docs"; fail=1; }
|
|
1663
|
+
grep -q "EP-005" "$TMP/gb5.err" \
|
|
1664
|
+
&& echo "OK --from-docs avisa de la épica que está en docs pero no en el JSON" || { echo "FAIL sin aviso de épica ausente del JSON"; fail=1; }
|
|
1665
|
+
echo "$out" | grep -q '"depends_on_source"' \
|
|
1666
|
+
&& echo "OK el bundle deja traza de la fuente de dependencias" || { echo "FAIL el bundle no declara depends_on_source"; fail=1; }
|
|
1667
|
+
|
|
1668
|
+
# 3) Los ciclos que vienen de la prosa se cazan igual (el aviso ya existía).
|
|
1669
|
+
cat > "$TMP/docs/ciclo.md" <<'MD'
|
|
1670
|
+
## EP-010 — A
|
|
1671
|
+
**Depende de**: EP-011
|
|
1672
|
+
## EP-011 — B
|
|
1673
|
+
**Depende de**: EP-010
|
|
1674
|
+
MD
|
|
1675
|
+
echo '{"epics":[{"code":"EP-010","title":"A","layer":"business"},{"code":"EP-011","title":"B","layer":"business"}]}' \
|
|
1676
|
+
| python3 "$GB" --from-docs "$TMP/docs/ciclo.md" > /dev/null 2> "$TMP/gb6.err"
|
|
1677
|
+
rc=$?
|
|
1678
|
+
[ "$rc" = 0 ] && grep -qi "ciclo" "$TMP/gb6.err" \
|
|
1679
|
+
&& echo "OK --from-docs + ciclo de la prosa -> aviso, sin bloquear" || { echo "FAIL ciclo desde docs ($rc)"; fail=1; }
|
|
1680
|
+
|
|
1681
|
+
# 4) Ruta ilegible: rc 1 con mensaje, jamás traceback ni silencio (el fallo del piloto
|
|
1682
|
+
# fue precisamente un grafo vacío que no falló).
|
|
1683
|
+
echo '{"epics":[]}' | python3 "$GB" --from-docs "$TMP/docs/no-existe.md" > /dev/null 2> "$TMP/gb7.err"
|
|
1684
|
+
rc=$?
|
|
1685
|
+
[ "$rc" = 1 ] && ! grep -q "Traceback" "$TMP/gb7.err" \
|
|
1686
|
+
&& echo "OK --from-docs con ruta ilegible -> rc 1 limpio" || { echo "FAIL ruta ilegible ($rc)"; fail=1; }
|
|
1687
|
+
# Flag sin argumento: mismo trato.
|
|
1688
|
+
echo '{"epics":[]}' | python3 "$GB" --from-docs > /dev/null 2> "$TMP/gb8.err"
|
|
1689
|
+
[ "$?" = 1 ] && ! grep -q "Traceback" "$TMP/gb8.err" \
|
|
1690
|
+
&& echo "OK --from-docs sin argumento -> rc 1 limpio" || { echo "FAIL --from-docs sin argumento"; fail=1; }
|
|
1691
|
+
|
|
1692
|
+
# 4-bis) Sección presente pero SIN el campo: aviso distinto al de sección ausente
|
|
1693
|
+
# (un backlog que dibuja sus dependencias en ASCII no es un backlog sin épicas).
|
|
1694
|
+
printf '## EP-020 — Con seccion, sin campo\n\n**Resumen**: x.\n' > "$TMP/docs/sin-campo.md"
|
|
1695
|
+
echo '{"epics":[{"code":"EP-020","title":"X","layer":"business"},{"code":"EP-021","title":"Y","layer":"business"}]}' \
|
|
1696
|
+
| python3 "$GB" --from-docs "$TMP/docs/sin-campo.md" > /dev/null 2> "$TMP/gb10.err"
|
|
1697
|
+
grep -q "EP-020 tiene sección" "$TMP/gb10.err" && grep -q "EP-021 no aparece como sección" "$TMP/gb10.err" \
|
|
1698
|
+
&& echo "OK distingue sección sin campo de épica ausente de los docs" || { echo "FAIL avisos de cobertura de docs"; fail=1; }
|
|
1699
|
+
|
|
1700
|
+
# 4-ter) Las dos banderas juntas son un error explícito (ignorar una en silencio es el
|
|
1701
|
+
# patrón que causó #56).
|
|
1702
|
+
echo '{"epics":[]}' | python3 "$GB" --from-docs "$TMP/docs/epicas.md" --print-deps "$TMP/docs/epicas.md" > /dev/null 2> "$TMP/gb11.err"
|
|
1703
|
+
[ "$?" = 1 ] && grep -qi "excluyentes" "$TMP/gb11.err" \
|
|
1704
|
+
&& echo "OK --from-docs y --print-deps juntas -> rc 1 explícito" || { echo "FAIL banderas excluyentes"; fail=1; }
|
|
1705
|
+
|
|
1706
|
+
# 5) Sin sección de épica reconocible: aviso explícito (no silencio).
|
|
1707
|
+
printf 'texto sin encabezados de epica\n' > "$TMP/docs/vacio.md"
|
|
1708
|
+
echo '{"epics":[{"code":"EP-001","title":"X","layer":"business"}]}' \
|
|
1709
|
+
| python3 "$GB" --from-docs "$TMP/docs/vacio.md" > /dev/null 2> "$TMP/gb9.err"
|
|
1710
|
+
grep -qi "ninguna secci" "$TMP/gb9.err" \
|
|
1711
|
+
&& echo "OK avisa cuando los docs no traen secciones de épica" || { echo "FAIL sin aviso de docs sin épicas"; fail=1; }
|
|
1712
|
+
|
|
1713
|
+
|
|
1367
1714
|
exit $fail
|
|
@@ -133,6 +133,16 @@ Espejo de la Fase 0, para proyectos **con UI**. Antes de abrir el primer slice c
|
|
|
133
133
|
| 7 · pr | Abrir PR + archivar change en el mismo PR | `opsx:archive`, `opsx:sync` | — | `gitflow.md` |
|
|
134
134
|
| 8 · release? | Preguntar si correr el Release Gate ahora | usuario (default computado) | — | abajo |
|
|
135
135
|
|
|
136
|
+
> **Reporta la fase al runtime en cada transición** (modos `dual`/`runtime`; en `legacy` el
|
|
137
|
+
> comando devuelve rc 3 y no haces nada): `bash .claude/hooks/build/slice-ops.sh phase <to>`
|
|
138
|
+
> **al entrar** en cada fase. Las fases del hub son más finas que las de esta tabla:
|
|
139
|
+
> `dor → change → red → green → refactor → smoke → api → data → dod → pr` — la fase 3 · tdd
|
|
140
|
+
> reporta **tres** (`phase red` al escribir el test rojo, `phase green` al pasarlo,
|
|
141
|
+
> `phase refactor` al refactorizar) y la 5 reporta `api` y luego `data`; la 7 · pr reporta
|
|
142
|
+
> `phase pr` al abrir el PR. El subcomando es idempotente y rellena huecos solo (cadena
|
|
143
|
+
> completa, el orden del hub es estricto); sin estos reportes el slice queda «en
|
|
144
|
+
> construcción» en el hub aunque su PR ya esté mergeado. Ver `references/runtime-protocol.md`.
|
|
145
|
+
|
|
136
146
|
Los gates `stack`, `security`, `smell`, `ux` y la coherencia triple completa **ya no se cierran
|
|
137
147
|
aquí**: pertenecen al Release Gate. Las **deps** siguen vigiladas en tiempo real por el hook
|
|
138
148
|
`stack-guard.sh`; lint/tsc/gitflow por sus hooks.
|
|
@@ -35,16 +35,17 @@ acto pasa por `slice-ops.sh` (inner loop) y `release-ops.sh` (outer loop), insta
|
|
|
35
35
|
|---|---|---|
|
|
36
36
|
| Reclamar | `slice-ops.sh claim [--epic EP-XXX]` | Sincroniza el contexto **antes** (regla: no se abre trabajo con contexto viejo) y reporta los sha256 locales de los archivos gobernados. Si la respuesta trae `checkpoint`, **continúa desde él; jamás reinicies**. |
|
|
37
37
|
| Saber el paso | `slice-ops.sh next-step` | Lo deriva el **cliente** desde la proyección: `resume_hint` → wiring `failing` → sub-slice pendiente → primer gate abierto. |
|
|
38
|
+
| Reportar la fase | `slice-ops.sh phase <to>` | **Al entrar en cada fase del pipeline** (`dor → change → red → green → refactor → smoke → api → data → dod → pr`, orden estricto del hub). Emite la cadena `phase_advanced` que falte desde la fase que el servidor conoce (nada si ya está: idempotente). Sin esto el slice se queda en `dor` server-side y el archivado se rechaza. `archived` no se reporta aquí: lo produce `archive`. |
|
|
38
39
|
| Sembrar el cableado | `slice-ops.sh wiring seed --file items.json` | Un item por escenario AC y por punto de integración entre capas. Nacen `failing`. |
|
|
39
40
|
| Cablear un item | `slice-ops.sh wiring update <item_id> passing --kind K --ref HU-XXX --evidence-file f` | `passing` **solo tras prueba real ejecutada**. Sin evidencia el comando avisa: no lo ignores. |
|
|
40
41
|
| Cerrar un gate | `slice-ops.sh gate <nombre> <pass\|fail\|na> --evidence-file f` | Gates canónicos: `dor coherence_link tdd journey_smoke fidelity api data wiring_verified dod`. Un `na` es legítimo cuando el gate no aplica. |
|
|
41
42
|
| Dejar bitácora | `slice-ops.sh progress «hito»` | Sustituye a `progress_log[]`: el event stream **es** la bitácora. |
|
|
42
43
|
| Guardar continuidad | `slice-ops.sh checkpoint --note «dónde me quedé»` | Rama + commit reales. Es lo que permite que otro agente continúe tras una caída. |
|
|
43
44
|
| Entregar | `slice-ops.sh submit --pr-url U --openspec-change C` | Tras abrir el PR. |
|
|
44
|
-
| Archivar | `slice-ops.sh archive --epic EP-XXX --openspec-change C --pr-url U` | Lo dispara `opsx:archive` en el mismo PR. Va por cola (idempotente, protegido de la cota). |
|
|
45
|
+
| Archivar | `slice-ops.sh archive --epic EP-XXX --openspec-change C --pr-url U` | Lo dispara `opsx:archive` en el mismo PR. Va por cola (idempotente, protegido de la cota). **Catch-up automático**: si la fase server-side quedó atrás, emite la cadena `phase_advanced` hasta `pr` antes del archivado y lo avisa (`⚠ … server-side`). |
|
|
45
46
|
| Hechos de proyecto | `slice-ops.sh fact scaffold-confirmed --by «persona»` · `fact design-source …` · `fact project-kind …` · `fact foundation …` | **Reportan lo que un humano confirmó**; el agente nunca confirma por su cuenta. |
|
|
46
47
|
| Bloqueo | `slice-ops.sh escalate «razón» --gate G` | Deja el rastro; la decisión (recortar/diferir/desbloquear) es del equipo. |
|
|
47
|
-
| Ver el estado | `slice-ops.sh status` | Modo, slice, gates, wiring, contexto, lease y
|
|
48
|
+
| Ver el estado | `slice-ops.sh status` | Modo, slice, gates, wiring, contexto, lease, cola pendiente y **rechazados** (`outbox/rejected/`: conteo + razón del más reciente — un rechazo del hub NO se reintenta solo; corrige la causa y re-emite). |
|
|
48
49
|
|
|
49
50
|
## Reglas duras
|
|
50
51
|
|
|
@@ -60,6 +61,10 @@ acto pasa por `slice-ops.sh` (inner loop) y `release-ops.sh` (outer loop), insta
|
|
|
60
61
|
reconectar y el `client_event_id` evita duplicados.
|
|
61
62
|
7. **El agente no gobierna.** No cierra releases, no publica contexto, no importa el grafo,
|
|
62
63
|
no abre/drena/cierra fronts: prepara, propone, reporta — y el humano decide en la consola.
|
|
64
|
+
8. **La fase se reporta al entrar, no al final.** El hub lleva la fase con `phase_advanced`
|
|
65
|
+
de orden estricto: invoca `phase <to>` en cada transición del pipeline. Si un pipeline
|
|
66
|
+
corrió sin reportarla, `archive` completa la cadena solo (catch-up) — pero la telemetría
|
|
67
|
+
intermedia del hub habrá estado ciega todo el slice.
|
|
63
68
|
|
|
64
69
|
## Qué queda del fichero (legado)
|
|
65
70
|
|
|
@@ -95,7 +95,10 @@ Archive a completed change in the experimental workflow.
|
|
|
95
95
|
|
|
96
96
|
It is queued and idempotent (protected from the outbox cap), so it survives an offline
|
|
97
97
|
archive; `rc 3` simply means legacy mode (the file protocol applies). Never block the
|
|
98
|
-
archive on this step.
|
|
98
|
+
archive on this step. If the pipeline ran without phase reports, the command catches up
|
|
99
|
+
by itself: it emits the missing `phase_advanced` chain up to `pr` before the archive
|
|
100
|
+
event (the hub rejects `slice_archived` on any earlier phase) and prints a `⚠ … server-side`
|
|
101
|
+
notice — informative, not an error.
|
|
99
102
|
|
|
100
103
|
6. **Display summary**
|
|
101
104
|
|