@trycore/spec-build-harness 0.8.4 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/GOVERNANCE.md +43 -5
- package/INSTALL.md +28 -6
- package/METODOLOGIA.md +65 -10
- package/README.md +41 -7
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +33 -7
- package/agents/build/dor-dod-gatekeeper.md +17 -6
- package/agents/build/ux-fidelity-reviewer.md +4 -1
- package/agents/build/wiring-adversarial-verifier.md +52 -5
- package/commands/build/architect.md +1 -1
- package/commands/build/claim.md +46 -0
- package/commands/build/escalate.md +36 -0
- package/commands/build/front.md +9 -3
- package/commands/build/onboard.md +63 -14
- package/commands/build/prototype.md +23 -0
- package/commands/build/reflect.md +60 -40
- package/commands/build/release.md +10 -7
- package/commands/build/resume.md +33 -13
- package/commands/build/slice.md +32 -27
- package/commands/build/status.md +35 -0
- package/commands/build/work.md +11 -8
- package/config/build-config.template.json +4 -0
- package/dist/cli.js +22 -0
- package/dist/commands/doctor.js +42 -0
- package/dist/commands/init.js +84 -1
- package/dist/commands/migrate.js +48 -0
- package/dist/commands/status.js +34 -0
- package/dist/lib/normalize.js +276 -0
- package/dist/lib/paths.js +6 -0
- package/dist/lib/runtime-client.js +196 -0
- package/dist/lib/settings-merge.js +3 -3
- package/dist/lib/state-bundle.js +46 -0
- package/docs/commands.md +32 -9
- package/docs/getting-started.md +2 -1
- package/docs/hooks.md +114 -27
- package/docs/runtime/guia-modo-dual-y-migracion.md +136 -0
- package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
- package/docs/runtime/protocolo-cliente-runtime.md +109 -34
- package/hooks/build/build-gate-check.sh +21 -0
- package/hooks/build/context-monitor.sh +82 -15
- package/hooks/build/context-sync.sh +192 -0
- package/hooks/build/design-source-guard.sh +31 -3
- package/hooks/build/dual-compare.sh +92 -0
- package/hooks/build/event-emitter.sh +75 -0
- package/hooks/build/gitflow-guard.sh +164 -14
- package/hooks/build/heartbeat.sh +259 -0
- package/hooks/build/lib/agent-context.sh +139 -0
- package/hooks/build/lib/config.sh +27 -0
- package/hooks/build/lib/projection.sh +71 -0
- package/hooks/build/lib/runtime-client.sh +465 -0
- package/hooks/build/lib/runtime-ops.sh +221 -0
- package/hooks/build/lib/state-io.sh +5 -18
- package/hooks/build/load-build-state.sh +64 -2
- package/hooks/build/reflect-nudge.sh +15 -0
- package/hooks/build/release-gate-nudge.sh +15 -0
- package/hooks/build/release-ops.sh +164 -0
- package/hooks/build/scaffold-guard.sh +29 -2
- package/hooks/build/session-start.sh +103 -0
- package/hooks/build/session-stop.sh +22 -0
- package/hooks/build/slice-ops.sh +877 -0
- package/hooks/build/stack-guard.sh +8 -0
- package/hooks/build/statusline-bridge.sh +24 -3
- package/hooks/build-harness.json +16 -0
- package/package.json +3 -3
- package/scripts/check-agnostic.sh +3 -1
- package/scripts/check-pack-clean.sh +31 -0
- package/scripts/check-runtime-purity.sh +43 -0
- package/scripts/lib/front-plan.py +4 -0
- package/scripts/lib/graph-bundle.py +133 -0
- package/scripts/runtime-purity-allow.txt +5 -0
- package/scripts/smoke-test.sh +1 -1
- package/scripts/tests/lib/http-stub.py +46 -0
- package/scripts/tests/test-baseline-verdict.sh +92 -0
- package/scripts/tests/test-config.sh +25 -0
- package/scripts/tests/test-hooks-runtime.sh +853 -0
- package/scripts/tests/test-install.sh +57 -0
- package/scripts/tests/test-runtime-client.sh +298 -0
- package/scripts/tests/test-schema.sh +29 -1
- package/scripts/tests/test-skill-ops.sh +847 -0
- package/skills/building-a-micro-change/SKILL.md +22 -4
- package/skills/building-a-slice/SKILL.md +58 -23
- package/skills/building-a-slice/assets/baseline-verdict.sh +172 -0
- package/skills/building-a-slice/references/dod.md +12 -3
- package/skills/building-a-slice/references/dor.md +7 -3
- package/skills/building-a-slice/references/evidence-budget.md +51 -0
- package/skills/building-a-slice/references/exploration-fanout.md +1 -1
- package/skills/building-a-slice/references/gitflow.md +1 -1
- package/skills/building-a-slice/references/regression-baseline.md +67 -0
- package/skills/building-a-slice/references/runtime-protocol.md +75 -0
- package/skills/building-a-slice/references/state-protocol.md +12 -1
- package/skills/building-a-slice/workflows/README.md +7 -3
- package/skills/building-a-slice/workflows/explore-fanout.workflow.js +3 -3
- package/skills/building-a-slice/workflows/wiring-verify.workflow.js +26 -4
- package/skills/managing-parallel-front/SKILL.md +32 -16
- package/skills/openspec-archive-change/SKILL.md +15 -0
- package/skills/prototyping-screens/SKILL.md +104 -0
- package/skills/prototyping-screens/assets/DESIGN.md.template +55 -0
- package/skills/prototyping-screens/assets/manifest.schema.json +70 -0
- package/skills/prototyping-screens/assets/screen.template.html +34 -0
- package/skills/prototyping-screens/references/aesthetic-directions.md +42 -0
- package/skills/prototyping-screens/references/extraction.md +57 -0
- package/skills/prototyping-screens/references/self-check.md +40 -0
- package/skills/releasing-a-version/SKILL.md +25 -16
- package/skills/releasing-a-version/references/release-dod.md +7 -5
- package/skills/releasing-a-version/workflows/README.md +2 -1
- package/skills/releasing-a-version/workflows/release-gate.workflow.js +6 -5
- package/skills/setup-architecture/SKILL.md +4 -2
- package/state/README.md +20 -3
- package/state/build-state.schema.json +3 -2
- package/templates/CLAUDE.md.template +17 -1
- package/templates/settings-hooks.template.json +8 -4
- 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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|