@trycore/spec-build-harness 0.8.5 → 0.10.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.
Files changed (106) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/GOVERNANCE.md +27 -4
  3. package/INSTALL.md +27 -5
  4. package/METODOLOGIA.md +55 -5
  5. package/README.md +39 -6
  6. package/VERSION +1 -1
  7. package/agents/build/build-orchestrator.md +33 -7
  8. package/agents/build/dor-dod-gatekeeper.md +13 -5
  9. package/agents/build/wiring-adversarial-verifier.md +52 -5
  10. package/commands/build/architect.md +1 -1
  11. package/commands/build/claim.md +46 -0
  12. package/commands/build/escalate.md +36 -0
  13. package/commands/build/front.md +9 -3
  14. package/commands/build/onboard.md +59 -14
  15. package/commands/build/prototype.md +3 -2
  16. package/commands/build/reflect.md +60 -40
  17. package/commands/build/release.md +10 -7
  18. package/commands/build/resume.md +33 -13
  19. package/commands/build/slice.md +32 -27
  20. package/commands/build/status.md +35 -0
  21. package/commands/build/work.md +11 -8
  22. package/config/build-config.template.json +4 -0
  23. package/dist/cli.js +22 -0
  24. package/dist/commands/doctor.js +42 -0
  25. package/dist/commands/init.js +84 -1
  26. package/dist/commands/migrate.js +48 -0
  27. package/dist/commands/status.js +34 -0
  28. package/dist/lib/normalize.js +276 -0
  29. package/dist/lib/paths.js +6 -0
  30. package/dist/lib/runtime-client.js +196 -0
  31. package/dist/lib/settings-merge.js +3 -3
  32. package/dist/lib/state-bundle.js +46 -0
  33. package/docs/commands.md +25 -8
  34. package/docs/getting-started.md +1 -0
  35. package/docs/hooks.md +114 -27
  36. package/docs/runtime/guia-modo-dual-y-migracion.md +136 -0
  37. package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
  38. package/docs/runtime/protocolo-cliente-runtime.md +109 -34
  39. package/hooks/build/build-gate-check.sh +21 -0
  40. package/hooks/build/context-monitor.sh +82 -15
  41. package/hooks/build/context-sync.sh +192 -0
  42. package/hooks/build/design-source-guard.sh +30 -2
  43. package/hooks/build/dual-compare.sh +92 -0
  44. package/hooks/build/event-emitter.sh +75 -0
  45. package/hooks/build/gitflow-guard.sh +164 -14
  46. package/hooks/build/heartbeat.sh +259 -0
  47. package/hooks/build/lib/agent-context.sh +139 -0
  48. package/hooks/build/lib/config.sh +27 -0
  49. package/hooks/build/lib/projection.sh +71 -0
  50. package/hooks/build/lib/runtime-client.sh +465 -0
  51. package/hooks/build/lib/runtime-ops.sh +221 -0
  52. package/hooks/build/lib/state-io.sh +5 -18
  53. package/hooks/build/load-build-state.sh +64 -2
  54. package/hooks/build/reflect-nudge.sh +15 -0
  55. package/hooks/build/release-gate-nudge.sh +15 -0
  56. package/hooks/build/release-ops.sh +164 -0
  57. package/hooks/build/scaffold-guard.sh +29 -2
  58. package/hooks/build/session-start.sh +103 -0
  59. package/hooks/build/session-stop.sh +22 -0
  60. package/hooks/build/slice-ops.sh +877 -0
  61. package/hooks/build/stack-guard.sh +8 -0
  62. package/hooks/build/statusline-bridge.sh +24 -3
  63. package/hooks/build-harness.json +16 -0
  64. package/package.json +3 -3
  65. package/scripts/check-agnostic.sh +3 -1
  66. package/scripts/check-pack-clean.sh +31 -0
  67. package/scripts/check-runtime-purity.sh +43 -0
  68. package/scripts/lib/front-plan.py +4 -0
  69. package/scripts/lib/graph-bundle.py +133 -0
  70. package/scripts/runtime-purity-allow.txt +5 -0
  71. package/scripts/smoke-test.sh +1 -1
  72. package/scripts/tests/lib/http-stub.py +46 -0
  73. package/scripts/tests/test-baseline-verdict.sh +92 -0
  74. package/scripts/tests/test-config.sh +25 -0
  75. package/scripts/tests/test-hooks-runtime.sh +853 -0
  76. package/scripts/tests/test-install.sh +57 -0
  77. package/scripts/tests/test-runtime-client.sh +298 -0
  78. package/scripts/tests/test-schema.sh +29 -1
  79. package/scripts/tests/test-skill-ops.sh +847 -0
  80. package/skills/building-a-micro-change/SKILL.md +22 -4
  81. package/skills/building-a-slice/SKILL.md +55 -21
  82. package/skills/building-a-slice/assets/baseline-verdict.sh +172 -0
  83. package/skills/building-a-slice/references/dod.md +12 -3
  84. package/skills/building-a-slice/references/dor.md +3 -2
  85. package/skills/building-a-slice/references/evidence-budget.md +51 -0
  86. package/skills/building-a-slice/references/exploration-fanout.md +1 -1
  87. package/skills/building-a-slice/references/gitflow.md +1 -1
  88. package/skills/building-a-slice/references/regression-baseline.md +67 -0
  89. package/skills/building-a-slice/references/runtime-protocol.md +75 -0
  90. package/skills/building-a-slice/references/state-protocol.md +12 -1
  91. package/skills/building-a-slice/workflows/README.md +7 -3
  92. package/skills/building-a-slice/workflows/explore-fanout.workflow.js +3 -3
  93. package/skills/building-a-slice/workflows/wiring-verify.workflow.js +26 -4
  94. package/skills/managing-parallel-front/SKILL.md +32 -16
  95. package/skills/openspec-archive-change/SKILL.md +15 -0
  96. package/skills/prototyping-screens/SKILL.md +9 -5
  97. package/skills/releasing-a-version/SKILL.md +25 -16
  98. package/skills/releasing-a-version/references/release-dod.md +7 -5
  99. package/skills/releasing-a-version/workflows/README.md +2 -1
  100. package/skills/releasing-a-version/workflows/release-gate.workflow.js +6 -5
  101. package/skills/setup-architecture/SKILL.md +4 -2
  102. package/state/README.md +16 -1
  103. package/state/build-state.schema.json +2 -1
  104. package/templates/CLAUDE.md.template +16 -0
  105. package/templates/settings-hooks.template.json +8 -4
  106. package/internal/skills/auditar-arnes/SKILL.md +0 -29
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env bash
2
- # state-io.sh — helpers compartidos: rutas, lectura de config y patch atómico del estado.
2
+ # state-io.sh — helpers compartidos: rutas y patch atómico del estado.
3
+ # config_get vive en lib/config.sh [EP-OR-08-A] — se re-exporta sourceándolo aquí para
4
+ # que los llamadores existentes sigan funcionando sin cambios.
3
5
  # Diseño: fail-open. Nunca lanza; ante fallo deja el archivo intacto.
4
6
 
7
+ source "$(dirname "${BASH_SOURCE[0]}")/config.sh"
8
+
5
9
  state_path() {
6
10
  # CLAUDE_PROJECT_DIR es la señal autoritativa que Claude Code fija en los hooks;
7
11
  # git rev-parse solo aplica como fallback para invocación manual/dev (sin ese env).
@@ -9,23 +13,6 @@ state_path() {
9
13
  echo "$root/.claude/state/build-state.json"
10
14
  }
11
15
 
12
- # config_get <clave.punteada> <default>
13
- config_get() {
14
- local key="$1" def="$2" file
15
- file="${BUILD_CONFIG_FILE:-$(dirname "$(state_path)")/../config/build-config.json}"
16
- command -v python3 >/dev/null 2>&1 || { echo "$def"; return; }
17
- python3 - "$file" "$key" "$def" <<'PY' 2>/dev/null || echo "$def"
18
- import json,sys
19
- file,key,default=sys.argv[1],sys.argv[2],sys.argv[3]
20
- try:
21
- d=json.load(open(file))
22
- for part in key.split("."): d=d[part]
23
- print(d if not isinstance(d,bool) else str(d).lower())
24
- except Exception:
25
- print(default)
26
- PY
27
- }
28
-
29
16
  # state_atomic_patch <state-file> <expr-python-que-muta-`d`>
30
17
  # Ejecuta la expresión con `d` = dict del estado; escribe atómico solo si válido.
31
18
  state_atomic_patch() {
@@ -1,9 +1,71 @@
1
1
  #!/usr/bin/env bash
2
2
  # load-build-state.sh — SessionStart
3
3
  # Inyecta al contexto: rama actual, harness_phase, slice activo y gates abiertos.
4
- # Además sincroniza harness_phase (authoring -> active) según exista package.json.
4
+ # [EP-OR-08-B] Dos caminos según runtime_mode():
5
+ # - legacy|dual: comportamiento v0.8.5 exacto (el fichero es primario; además sincroniza
6
+ # harness_phase y resetea el guard de handoff por sesión).
7
+ # - runtime: render desde la caché de proyección; NO se sourcea state-io.sh ni se invoca
8
+ # el reconciliador (la maquinaria legacy no se carga en runtime, spec §4.2-E).
5
9
  set -uo pipefail
6
- source "$(dirname "${BASH_SOURCE[0]}")/lib/state-io.sh"
10
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ source "$HERE/lib/runtime-client.sh"
12
+ source "$HERE/lib/projection.sh"
13
+
14
+ # render_runtime — pinta el mismo bloque de contexto que el camino legacy, pero con los
15
+ # hechos del runtime. Todo lo que falte degrada a un texto neutro: jamás rompe el arranque.
16
+ render_runtime() {
17
+ local root branch slice gates wiring subs hint ver age
18
+ root="$(config_root)"
19
+ branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'desconocida')"
20
+ echo "🏗️ Arnés de construcción · modo runtime"
21
+ echo " Rama: $branch | Fase del arnés: $(projection_get project.harness_phase desconocida)"
22
+ ver="$(projection_get context.version "")"
23
+ [ -n "$ver" ] && echo " Contexto sincronizado: v$ver"
24
+ if projection_is_stale; then
25
+ age="$(projection_age_seconds)"
26
+ echo " ⚠ Proyección local sin refrescar (${age}s): el runtime no respondió. Los guards siguen operando con esta caché."
27
+ fi
28
+ if [ "$(cat "$root/.claude/state/heartbeat-status.json" 2>/dev/null | grep -c '"lease_lost": true')" != "0" ]; then
29
+ echo " ⚠ Lease perdido: otro agente pudo tomar el slice. Haz checkpoint y vuelve a reclamar (/build:claim)."
30
+ fi
31
+ slice="$(projection_get active_slice.slice_id "")"
32
+ if [ -z "$slice" ]; then
33
+ echo " Slice activo: ninguno. Reclama el siguiente con /build:claim (skill building-a-slice)."
34
+ return 0
35
+ fi
36
+ echo " Slice activo: $(projection_get active_slice.epic_code '?') [$(projection_get active_slice.hus '[]' | tr -d '[]"')] · change=$(projection_get active_slice.openspec_change '—') · fase=$(projection_get active_slice.phase '—')"
37
+ gates="$(projection_get active_slice.gates '{}' | python3 -c 'import json,sys
38
+ try: g=json.load(sys.stdin)
39
+ except Exception: g={}
40
+ abiertos=[k for k,v in g.items() if v is False]
41
+ print(", ".join(abiertos) if abiertos else "ninguno ✅")' 2>/dev/null)"
42
+ echo " Gates pendientes: ${gates:-desconocidos}"
43
+ wiring="$(projection_get active_slice.wiring_failing '[]' | python3 -c 'import json,sys
44
+ try: w=json.load(sys.stdin)
45
+ except Exception: w=[]
46
+ if w:
47
+ muestra="; ".join("%s(%s)" % (i.get("item_id"), i.get("kind")) for i in w[:8])
48
+ extra=" (+%d más)" % (len(w)-8) if len(w)>8 else ""
49
+ print("%d|%s%s" % (len(w), muestra, extra))' 2>/dev/null)"
50
+ if [ -n "$wiring" ]; then
51
+ echo " ⚠️ Cableado pendiente (${wiring%%|*} item/s failing): ${wiring#*|}"
52
+ echo " NO declares el slice terminado mientras queden items failing (verifica con prueba real)."
53
+ fi
54
+ subs="$(projection_get active_slice.sub_slices_pending '[]' | tr -d '[]"')"
55
+ [ -n "$subs" ] && echo " ⚠️ Sub-slices pendientes: $subs"
56
+ hint="$(projection_get active_slice.resume_hint "")"
57
+ [ -n "$hint" ] && echo " ▶️ Retomar: $hint"
58
+ return 0
59
+ }
60
+
61
+ if [ "$(runtime_mode)" = "runtime" ]; then
62
+ cat >/dev/null 2>&1 # consumir el payload del hook (protocolo)
63
+ render_runtime
64
+ exit 0
65
+ fi
66
+
67
+ # ── A partir de aquí, camino legacy|dual: v0.8.5 exacto ──
68
+ source "$HERE/lib/state-io.sh"
7
69
 
8
70
  # Lee el payload de SessionStart (JSON en stdin) para extraer session_id. Fail-open:
9
71
  # si no hay stdin o no trae session_id, seguimos sin tocar session_continuity.
@@ -4,6 +4,21 @@
4
4
  # Sugiere /build:reflect SOLO si hay slice(s) archivado(s) sin reflexionar (reflected != true).
5
5
  # Determinista y barato: el razonamiento (qué se aprendió) lo hace el MODELO en /build:reflect.
6
6
  set -uo pipefail
7
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "$HERE/lib/projection.sh"
9
+
10
+ # [EP-OR-08-B] En runtime el cómputo («qué slices quedaron sin reflexionar») lo hace el
11
+ # servidor sobre TODO el proyecto, no este clon: aquí solo se renderiza su nudge.
12
+ if [ "$(runtime_mode)" = "runtime" ]; then
13
+ projection_get nudges '[]' | python3 -c 'import json,sys
14
+ try: ns=json.load(sys.stdin)
15
+ except Exception: ns=[]
16
+ for n in ns:
17
+ if isinstance(n,dict) and n.get("kind")=="reflect" and n.get("message"):
18
+ print("💡 "+n["message"])
19
+ print(" Ejecuta /build:reflect para proponer convenciones aprendidas a CLAUDE.md (se aplican tras tu aprobación).")' 2>/dev/null
20
+ exit 0
21
+ fi
7
22
 
8
23
  ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
9
24
  STATE="$ROOT/.claude/state/build-state.json"
@@ -7,6 +7,21 @@
7
7
  # El criterio "cierra una línea de release" vive en docs/02-user-story-map/ y NO se intenta aquí
8
8
  # (lo computa la skill building-a-slice en la fase 8).
9
9
  set -uo pipefail
10
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ source "$HERE/lib/projection.sh"
12
+
13
+ # [EP-OR-08-B] Igual que reflect-nudge: la aritmética de conjuntos (archivadas − cubiertas)
14
+ # es del servidor; este hook renderiza el nudge tipado y jamás bloquea el cierre.
15
+ if [ "$(runtime_mode)" = "runtime" ]; then
16
+ projection_get nudges '[]' | python3 -c 'import json,sys
17
+ try: ns=json.load(sys.stdin)
18
+ except Exception: ns=[]
19
+ for n in ns:
20
+ if isinstance(n,dict) and n.get("kind")=="release_gate" and n.get("message"):
21
+ print("💡 Release Gate sugerido: "+n["message"])
22
+ print(" Ejecuta /build:release (o la skill releasing-a-version): corre los gates pesados UNA vez sobre el diff acumulado.")' 2>/dev/null
23
+ exit 0
24
+ fi
10
25
 
11
26
  ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
12
27
  STATE="$ROOT/.claude/state/build-state.json"
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env bash
2
+ # release-ops.sh — operaciones del OUTER LOOP que conducen las skills [EP-OR-08-C]:
3
+ # veredictos del Release Gate y reporte de integración de un miembro del front paralelo.
4
+ # NO es un hook registrado: lo invocan `releasing-a-version` y `managing-parallel-front`.
5
+ #
6
+ # Línea de corte (spec §2): **reportar un hecho medido es acto de agente; decidir es acto
7
+ # humano**. Por eso aquí están los veredictos y el reporte de integración, y NO están:
8
+ # cerrar la release, planificar/abrir/drenar/cerrar el front, ni publicar contexto — esos
9
+ # viven en la consola del hub, con PDP, y las skills se limitan a instruir al humano.
10
+ #
11
+ # uso: release-ops.sh <verdict|close-hint|front-integration> …
12
+ set -uo pipefail
13
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "$HERE/lib/runtime-ops.sh"
15
+
16
+ usage() {
17
+ cat >&2 <<'USAGE'
18
+ uso: release-ops.sh <subcomando> [opciones]
19
+
20
+ verdict <line> <security|smell|ux|coherence|stack_arch|integration> <pass|fail|na>
21
+ [--evidence T | --evidence-file F] reporta un veredicto del Release Gate
22
+ close-hint <line> recuerda cómo se cierra (acto humano)
23
+ front-integration <front_id> --merge-status <pending|merged|conflict>
24
+ [--resmoke pass|fail] [--evidence T | --evidence-file F]
25
+
26
+ códigos de salida: 0 ok · 2 uso · 3 legacy · 5 offline · 6 rechazado
27
+ USAGE
28
+ }
29
+
30
+ # _rel_evidence_json — misma política que en slice-ops: fichero preferido, texto corto si no.
31
+ _rel_evidence_json() {
32
+ local ev="$1" evfile="$2"
33
+ if [ -n "$evfile" ]; then ops_json_str_file "$evfile"; else ops_json_str "$ev"; fi
34
+ }
35
+
36
+ # ── verdict ──────────────────────────────────────────────────────────────────────────
37
+ cmd_verdict() {
38
+ local line="${1:-}" gate="${2:-}" estado="${3:-}" ev="" evfile="" mode payload resp status pfile
39
+ if [ -z "$line" ] || [ -z "$gate" ] || [ -z "$estado" ]; then usage; return $OPS_RC_USAGE; fi
40
+ shift 3
41
+ while [ $# -gt 0 ]; do
42
+ case "$1" in
43
+ --evidence) ev="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
44
+ --evidence-file) evfile="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
45
+ *) shift ;;
46
+ esac
47
+ done
48
+ case " $OPS_RELEASE_GATES " in
49
+ *" $gate "*) : ;;
50
+ *) echo "⛔ gate de release desconocido «${gate}». Canónicos: $OPS_RELEASE_GATES" >&2
51
+ echo " (los gates por slice — dor, tdd, journey_smoke… — van por slice-ops.sh gate)" >&2
52
+ return $OPS_RC_REJECTED ;;
53
+ esac
54
+ case "$estado" in
55
+ pass|fail|na) : ;;
56
+ *) echo "⛔ estado inválido «${estado}» (pass|fail|na)" >&2; return $OPS_RC_REJECTED ;;
57
+ esac
58
+ mode="$(ops_mode)"
59
+ [ "$mode" = "legacy" ] && { echo "LEGACY: escribe el gate en releases[] del fichero"; return $OPS_RC_LEGACY; }
60
+ payload="{\"release_line\":$(ops_json_str "$line"),\"gate\":$(ops_json_str "$gate"),\"status\":$(ops_json_str "$estado"),\"evidence\":$(_rel_evidence_json "$ev" "$evfile")}"
61
+ if [ "$mode" = "dual" ]; then
62
+ pfile="$(ops_tmpjson "$payload")" || return $OPS_RC_OFFLINE
63
+ ops_mirror release_verdict_reported "$pfile" || echo "⚠ veredicto de release no espejado; el comparador dual lo marcará" >&2
64
+ rm -f "$pfile"
65
+ return $OPS_RC_OK
66
+ fi
67
+ resp="$(runtime_post "$(ops_path_release_verdicts "$(ops_urlenc "$line")")" "$payload")"
68
+ status="$(runtime_http_status)"
69
+ case "$status" in
70
+ 200|201|202)
71
+ echo "veredicto de release registrado: $gate=$estado en $line"
72
+ echo "El cierre de la release NO lo hace el agente: lo hace un humano en la consola (spec §2)."
73
+ return $OPS_RC_OK ;;
74
+ 422)
75
+ echo "⛔ 422 el servidor rechazó el veredicto: $resp — no reintentes; corrige y vuelve a medir." >&2
76
+ return $OPS_RC_REJECTED ;;
77
+ 401|403)
78
+ echo "⛔ token sin permiso para reportar veredictos de release ($status): habla con el ADMIN." >&2
79
+ return $OPS_RC_REJECTED ;;
80
+ *)
81
+ pfile="$(ops_tmpjson "$payload")" || return $OPS_RC_OFFLINE
82
+ # Sin slice_id: un veredicto de release es de ámbito release/proyecto, no de slice.
83
+ ops_enqueue_from_file verdict_reported "$pfile" ""
84
+ rm -f "$pfile"
85
+ echo "OFFLINE: veredicto de release encolado (tipo protegido)"
86
+ return $OPS_RC_OFFLINE ;;
87
+ esac
88
+ }
89
+
90
+ # ── close-hint ───────────────────────────────────────────────────────────────────────
91
+ # Cero red a propósito: es un recordatorio de gobierno, no una llamada.
92
+ cmd_close_hint() {
93
+ local line="${1:-}"
94
+ [ -n "$line" ] || { usage; return $OPS_RC_USAGE; }
95
+ cat <<HINT
96
+ El cierre de la release «${line}» es un acto de GOBIERNO HUMANO (spec §2):
97
+ 1. El agente reporta los 6 veredictos con \`release-ops.sh verdict $line <gate> <estado>\`.
98
+ 2. El runtime agrega: parciales NO promueven a passed.
99
+ 3. Una persona con permiso cierra la línea en la consola del hub.
100
+ El arnés no cierra releases ni con token de agente: esa superficie exige PDP.
101
+ HINT
102
+ return $OPS_RC_OK
103
+ }
104
+
105
+ # ── front-integration ────────────────────────────────────────────────────────────────
106
+ # Cada worktree se registra como AGENTE PROPIO (mismo token de proyecto, `agent_key`
107
+ # distinto): reporta la integración de SU miembro. Planificar, abrir, drenar y cerrar el
108
+ # front siguen siendo humanos.
109
+ cmd_front_integration() {
110
+ local front="${1:-}" merge="" resmoke="" ev="" evfile="" mode akey payload resp status pfile
111
+ [ -n "$front" ] || { usage; return $OPS_RC_USAGE; }
112
+ shift
113
+ while [ $# -gt 0 ]; do
114
+ case "$1" in
115
+ --merge-status) merge="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
116
+ --resmoke) resmoke="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
117
+ --evidence) ev="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
118
+ --evidence-file) evfile="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
119
+ *) shift ;;
120
+ esac
121
+ done
122
+ case "$merge" in
123
+ pending|merged|conflict) : ;;
124
+ *) echo "⛔ --merge-status debe ser pending|merged|conflict" >&2; return $OPS_RC_REJECTED ;;
125
+ esac
126
+ case "$resmoke" in
127
+ ""|pass|fail) : ;;
128
+ *) echo "⛔ --resmoke debe ser pass|fail" >&2; return $OPS_RC_REJECTED ;;
129
+ esac
130
+ mode="$(ops_mode)"
131
+ [ "$mode" = "legacy" ] && { echo "LEGACY: actualiza parallel_front.members[] en el fichero"; return $OPS_RC_LEGACY; }
132
+ payload="{\"merge_status\":$(ops_json_str "$merge"),\"re_smoke\":{\"status\":$(ops_json_str "$resmoke"),\"evidence\":$(_rel_evidence_json "$ev" "$evfile")}}"
133
+ if [ "$mode" = "dual" ]; then
134
+ pfile="$(ops_tmpjson "$payload")" || return $OPS_RC_OFFLINE
135
+ ops_mirror front_integration_reported "$pfile" || echo "⚠ integración de front no espejada; el comparador dual la marcará" >&2
136
+ rm -f "$pfile"
137
+ return $OPS_RC_OK
138
+ fi
139
+ akey="$(runtime_field agent_key)"
140
+ [ -n "$akey" ] || { echo "⛔ este worktree no está registrado como agente: corre \`trycore-build init\` en él" >&2; return $OPS_RC_REJECTED; }
141
+ resp="$(runtime_post "$(ops_path_front_integration "$(ops_urlenc "$front")" "$(ops_urlenc "$akey")")" "$payload")"
142
+ status="$(runtime_http_status)"
143
+ case "$status" in
144
+ 200|201|202) echo "integración reportada: merge_status=$merge re_smoke=${resmoke:-n/a}"; return $OPS_RC_OK ;;
145
+ 422) echo "⛔ 422 el servidor rechazó el reporte: $resp" >&2; return $OPS_RC_REJECTED ;;
146
+ 401|403) echo "⛔ token sin permiso ($status): habla con el ADMIN." >&2; return $OPS_RC_REJECTED ;;
147
+ *)
148
+ pfile="$(ops_tmpjson "$payload")" || return $OPS_RC_OFFLINE
149
+ ops_enqueue_from_file front_integration_reported "$pfile" ""
150
+ rm -f "$pfile"
151
+ echo "OFFLINE: reporte de integración encolado"
152
+ return $OPS_RC_OFFLINE ;;
153
+ esac
154
+ }
155
+
156
+ SUB="${1:-}"
157
+ [ $# -gt 0 ] && shift
158
+ case "$SUB" in
159
+ verdict) cmd_verdict "$@"; exit $? ;;
160
+ close-hint) cmd_close_hint "$@"; exit $? ;;
161
+ front-integration) cmd_front_integration "$@"; exit $? ;;
162
+ ""|-h|--help|help) usage; exit $OPS_RC_USAGE ;;
163
+ *) echo "subcomando desconocido: $SUB" >&2; usage; exit $OPS_RC_USAGE ;;
164
+ esac
@@ -8,11 +8,38 @@
8
8
  set -uo pipefail
9
9
 
10
10
  ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
11
- STATE="$ROOT/.claude/state/build-state.json"
12
- [ -f "$STATE" ] || exit 0
13
11
 
14
12
  INPUT="$(cat)"
15
13
 
14
+ # [EP-OR-08-B] En modo runtime los hechos vienen de la caché de proyección (la escribe
15
+ # session-start.sh desde /agent/context), NUNCA de la red: este hook corre en PreToolUse y
16
+ # la red está prohibida aquí. En legacy|dual el fichero sigue siendo primario (spec §6.1).
17
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ source "$HERE/lib/projection.sh"
19
+ if [ "$(runtime_mode)" = "runtime" ]; then
20
+ CACHE="$(runtime_projection_path)"
21
+ [ -f "$CACHE" ] || exit 0 # aún sin sincronizar: nada que vigilar (auto-arme)
22
+ if ! command -v python3 >/dev/null 2>&1; then
23
+ echo "⛔ scaffold-guard: python3 no disponible; no puedo verificar el gate de scaffold. Instala python3 (trycore-build doctor)." >&2
24
+ exit 2
25
+ fi
26
+ PHASE="$(projection_get active_slice.phase "")"
27
+ case "$PHASE" in
28
+ red|green|refactor|smoke|api|data) ;;
29
+ *) exit 0 ;; # sin slice o en dor/change/pr/archived: permitido
30
+ esac
31
+ [ "$(projection_get project.scaffold_confirmed false)" = "true" ] && exit 0
32
+ echo "⛔ scaffold-guard (Paso 1 fundamental): el slice está en fase de código pero el scaffold NO está confirmado." >&2
33
+ echo " Confirma primero que existe un scaffold runnable del proyecto (ver building-a-slice Fase 0 / DoR)." >&2
34
+ echo " El arnés NO genera el scaffold: créalo según tu stack (stack-allowlist.json) y confírmalo." >&2
35
+ projection_is_stale && echo " ⚠ Veredicto emitido con proyección stale (sin refrescar): el runtime no respondió en el último arranque." >&2
36
+ exit 2
37
+ fi
38
+
39
+ # Camino legacy|dual: leer build-state.json
40
+ STATE="$ROOT/.claude/state/build-state.json"
41
+ [ -f "$STATE" ] || exit 0
42
+
16
43
  # Guarda python3 [H4]: si falta, no podemos leer el estado de forma fiable. Fail-closed
17
44
  # DIRIGIDO: solo bloqueamos si la edición NO es del propio scaffold/planificación obvia.
18
45
  if ! command -v python3 >/dev/null 2>&1; then
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bash
2
+ # session-start.sh — SessionStart (startup|clear|compact) [EP-OR-08-B].
3
+ # La parte de RED del arranque de sesión: registro idempotente del agente (con el catálogo
4
+ # de asset types del paquete), hidratación de la caché de proyección desde /agent/context,
5
+ # sincronización de contexto por hash y aseguramiento del daemon de heartbeat.
6
+ # El RENDER del estado a `additionalContext` lo hace load-build-state.sh, que corre después.
7
+ # Fail-open absoluto: cualquier fallo imprime un aviso por stderr y sale 0 — un runtime
8
+ # caído jamás impide arrancar una sesión (RNF-6).
9
+ set -uo pipefail
10
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ source "$HERE/lib/runtime-client.sh"
12
+ source "$HERE/lib/projection.sh"
13
+ source "$HERE/lib/agent-context.sh"
14
+
15
+ cat >/dev/null 2>&1 # consumir el payload del hook (protocolo); no se usa aquí
16
+
17
+ MODE="$(runtime_mode)"
18
+ [ "$MODE" = "legacy" ] && exit 0
19
+ command -v python3 >/dev/null 2>&1 || exit 0
20
+ [ -n "$(runtime_url)" ] || exit 0 # sin `trycore-build init` (sub-slice D) no hay runtime
21
+
22
+ # sst_pkg_root — raíz del PAQUETE (no del proyecto). Los hooks se instalan como symlinks
23
+ # por fichero (linkChildren), así que hay que resolver la cadena de enlaces a mano:
24
+ # `readlink -f` no es portable al BSD readlink de macOS.
25
+ sst_pkg_root() {
26
+ local src="${BASH_SOURCE[0]}" target i=0
27
+ while [ -L "$src" ] && [ "$i" -lt 10 ]; do
28
+ target="$(readlink "$src" 2>/dev/null)" || break
29
+ case "$target" in
30
+ /*) src="$target" ;;
31
+ *) src="$(dirname "$src")/$target" ;;
32
+ esac
33
+ i=$((i + 1))
34
+ done
35
+ (cd "$(dirname "$src")/../.." >/dev/null 2>&1 && pwd -P) 2>/dev/null
36
+ }
37
+
38
+ # sst_harness_version — versión instalada en el consumidor; si no, la del paquete.
39
+ sst_harness_version() {
40
+ local f pkg
41
+ f="$(config_root)/.claude/.build-harness-version"
42
+ if [ -f "$f" ]; then
43
+ tr -d ' \n' < "$f"
44
+ return
45
+ fi
46
+ pkg="$(sst_pkg_root)"
47
+ if [ -n "$pkg" ] && [ -f "$pkg/VERSION" ]; then
48
+ tr -d ' \n' < "$pkg/VERSION"
49
+ return
50
+ fi
51
+ echo "0.0.0"
52
+ }
53
+
54
+ # sst_asset_types — el array `types` de asset-types.json del paquete (protocolo §6).
55
+ # Cadena de resolución: plugin > copia en .claude/ (la siembra es del sub-slice D) >
56
+ # paquete resuelto por symlink. Sin fichero: "[]" — el registro sigue funcionando.
57
+ sst_asset_types() {
58
+ local pkg candidates f
59
+ pkg="$(sst_pkg_root)"
60
+ candidates="${CLAUDE_PLUGIN_ROOT:-}/asset-types.json
61
+ $(config_root)/.claude/asset-types.json
62
+ ${pkg:-}/asset-types.json"
63
+ while IFS= read -r f; do
64
+ case "$f" in /asset-types.json|asset-types.json) continue ;; esac
65
+ [ -f "$f" ] || continue
66
+ python3 - "$f" <<'PY' 2>/dev/null && return
67
+ import json,sys
68
+ try:
69
+ d=json.load(open(sys.argv[1]))
70
+ t=d.get("types")
71
+ print(json.dumps(t if isinstance(t,list) else [],ensure_ascii=False))
72
+ except Exception:
73
+ raise SystemExit(1)
74
+ PY
75
+ done <<EOS
76
+ $candidates
77
+ EOS
78
+ echo "[]"
79
+ }
80
+
81
+ HV="$(sst_harness_version)"
82
+ AT="$(sst_asset_types)"
83
+
84
+ runtime_register "$HV" "$AT"
85
+ RC=$?
86
+ if [ "$RC" -eq 2 ]; then
87
+ echo "⛔ El runtime rechaza esta versión del arnés ($HV): incompatible_version." >&2
88
+ echo " Actualiza el paquete (npm i -g @trycore/spec-build-harness@latest) y reabre la sesión." >&2
89
+ echo " Mientras tanto se trabaja con la caché local: los guards siguen operando." >&2
90
+ exit 0
91
+ fi
92
+
93
+ if ! agent_context_fetch_and_cache; then
94
+ echo "⚠ El runtime no respondió: se trabaja con la caché de proyección local." >&2
95
+ fi
96
+
97
+ EXPECTED="$(projection_get context.manifest_hash "")"
98
+ [ -n "$EXPECTED" ] || EXPECTED="$(runtime_field manifest_hash)"
99
+ bash "$HERE/context-sync.sh" "$EXPECTED"
100
+
101
+ bash "$HERE/heartbeat.sh" --ensure "$PPID" >/dev/null 2>&1
102
+
103
+ exit 0
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # session-stop.sh — Stop [EP-OR-08-B].
3
+ # Flush de la cola al cerrar el turno, pero SIN red en el hilo del hook: deja un sentinela
4
+ # en outbox/.flush-request y se asegura de que el daemon esté vivo. El daemon lo consume en
5
+ # su siguiente tick y despacha (protocolo §5, spec §11: «señal local al daemon»).
6
+ # Nunca bloquea el cierre: exit 0 siempre, sin stdout (en Stop, stdout inyecta contexto).
7
+ set -uo pipefail
8
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "$HERE/lib/runtime-client.sh"
10
+
11
+ cat >/dev/null 2>&1 # consumir el payload del hook
12
+
13
+ MODE="$(runtime_mode)"
14
+ [ "$MODE" = "legacy" ] && exit 0
15
+
16
+ DIR="$(runtime_outbox_dir)"
17
+ mkdir -p "$DIR" 2>/dev/null
18
+ : > "$DIR/.flush-request" 2>/dev/null
19
+
20
+ bash "$HERE/heartbeat.sh" --ensure "$PPID" >/dev/null 2>&1
21
+
22
+ exit 0