@trycore/spec-build-harness 0.8.5 → 0.11.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 +27 -4
- package/INSTALL.md +27 -5
- package/METODOLOGIA.md +55 -5
- package/README.md +39 -6
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +33 -7
- package/agents/build/dor-dod-gatekeeper.md +13 -5
- 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 +75 -14
- package/commands/build/prototype.md +3 -2
- 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 +32 -0
- package/dist/commands/doctor.js +42 -0
- package/dist/commands/init.js +84 -1
- package/dist/commands/migrate.js +153 -0
- package/dist/commands/status.js +34 -0
- package/dist/lib/normalize.js +1123 -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 +150 -0
- package/docs/commands.md +25 -8
- package/docs/getting-started.md +1 -0
- package/docs/hooks.md +114 -27
- package/docs/runtime/guia-modo-dual-y-migracion.md +143 -0
- package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
- package/docs/runtime/protocolo-cliente-runtime.md +120 -35
- 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 +30 -2
- package/hooks/build/dual-compare.sh +92 -0
- package/hooks/build/event-emitter.sh +32 -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 +625 -0
- package/hooks/build/lib/runtime-ops.sh +227 -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 +171 -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 +948 -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/denylist.txt +4 -0
- package/scripts/lib/front-plan.py +4 -0
- package/scripts/lib/graph-bundle.py +181 -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 +828 -0
- package/scripts/tests/test-install.sh +103 -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 +1367 -0
- package/skills/building-a-micro-change/SKILL.md +22 -4
- package/skills/building-a-slice/SKILL.md +55 -21
- 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 +3 -2
- 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 +9 -5
- package/skills/releasing-a-version/SKILL.md +26 -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 +16 -1
- package/state/build-state.schema.json +2 -1
- package/templates/CLAUDE.md.template +16 -0
- package/templates/settings-hooks.template.json +8 -4
- package/internal/skills/auditar-arnes/SKILL.md +0 -29
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# runtime-ops.sh — base común de las OPERACIONES DE DOMINIO que conducen las skills
|
|
3
|
+
# [EP-OR-08-C]. Los hooks (sub-slice B) observan; esto transiciona: claim, veredictos,
|
|
4
|
+
# checkpoints, submit, archive y hechos de proyecto. Lo usan hooks/build/slice-ops.sh y
|
|
5
|
+
# hooks/build/release-ops.sh, que son INVOCADOS por las skills (no hooks registrados).
|
|
6
|
+
#
|
|
7
|
+
# Aquí vive el ÚNICO sitio del cliente bash donde se escriben rutas del runtime: si el
|
|
8
|
+
# re-volcado de OpenAPI (EP-OR-13) mueve un endpoint, se toca este fichero y nada más.
|
|
9
|
+
#
|
|
10
|
+
# Diseño fail-open, igual que runtime-client.sh: ninguna función lanza; ante red caída,
|
|
11
|
+
# python3 ausente o JSON corrupto se degrada a un valor seguro o a un rc distinguible.
|
|
12
|
+
|
|
13
|
+
source "$(dirname "${BASH_SOURCE[0]}")/projection.sh"
|
|
14
|
+
source "$(dirname "${BASH_SOURCE[0]}")/agent-context.sh"
|
|
15
|
+
|
|
16
|
+
# ── Códigos de salida (contrato con las skills; la prosa ramifica sobre estos números) ──
|
|
17
|
+
OPS_RC_OK=0
|
|
18
|
+
OPS_RC_USAGE=2
|
|
19
|
+
OPS_RC_LEGACY=3
|
|
20
|
+
OPS_RC_NO_SLICE=4
|
|
21
|
+
OPS_RC_OFFLINE=5
|
|
22
|
+
OPS_RC_REJECTED=6
|
|
23
|
+
OPS_RC_NO_WORK=7
|
|
24
|
+
|
|
25
|
+
# Tope de texto libre que el cliente manda al servidor (evidencias, notas): el protocolo §8
|
|
26
|
+
# dice que el servidor rechaza payloads sobredimensionados; se trunca aquí, marcándolo.
|
|
27
|
+
# [#44] El límite real del hub es 2000 (event_catalog._MAX_NOTE y VerdictIn.evidence):
|
|
28
|
+
# 1970 + los 27 caracteres del marcador de truncado de ops_json_str_file queda por debajo.
|
|
29
|
+
# El 4000 anterior producía rechazos en evidencias largas.
|
|
30
|
+
OPS_MAX_TEXT=1970
|
|
31
|
+
|
|
32
|
+
# Gates canónicos (se validan en el cliente: un nombre inventado por el modelo no llega
|
|
33
|
+
# a la red — compuerta mecánica barata, sin round-trip).
|
|
34
|
+
OPS_SLICE_GATES="dor coherence_link tdd journey_smoke fidelity api data wiring_verified dod"
|
|
35
|
+
OPS_RELEASE_GATES="security smell ux coherence stack_arch integration"
|
|
36
|
+
|
|
37
|
+
# ── Tabla de rutas ──
|
|
38
|
+
OPS_PATH_CLAIM="/tasks/next"
|
|
39
|
+
OPS_PATH_CHECKPOINTS="/checkpoints"
|
|
40
|
+
ops_path_verdicts() { echo "/slices/$1/verdicts"; }
|
|
41
|
+
ops_path_submit() { echo "/slices/$1/submit"; }
|
|
42
|
+
ops_path_release_verdicts() { echo "/releases/$1/verdicts"; }
|
|
43
|
+
ops_path_front_integration() { echo "/fronts/$1/members/$2/integration"; }
|
|
44
|
+
ops_path_mirror() { echo "/projects/$1/mirror/transitions"; }
|
|
45
|
+
ops_path_proposals() { echo "/projects/$1/context/agent-proposals"; }
|
|
46
|
+
|
|
47
|
+
ops_mode() { runtime_mode; }
|
|
48
|
+
|
|
49
|
+
# ops_slice_id — id del slice activo según la caché de proyección ("" si no hay).
|
|
50
|
+
ops_slice_id() { projection_get active_slice.slice_id ""; }
|
|
51
|
+
|
|
52
|
+
# ops_urlenc <texto> — percent-encoding para segmentos de ruta (una línea de release puede
|
|
53
|
+
# llevar espacios o barras). Sin python3 devuelve el texto tal cual: peor URL, nunca excepción.
|
|
54
|
+
ops_urlenc() {
|
|
55
|
+
command -v python3 >/dev/null 2>&1 || { printf '%s' "$1"; return; }
|
|
56
|
+
printf '%s' "$1" | python3 -c 'import sys,urllib.parse; sys.stdout.write(urllib.parse.quote(sys.stdin.read(), safe=""))' 2>/dev/null || printf '%s' "$1"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ops_json_str <texto> — imprime el texto como cadena JSON (comillas incluidas). El texto
|
|
60
|
+
# viaja por STDIN, nunca por argv: así ni ARG_MAX ni el quoting de bash lo limitan.
|
|
61
|
+
ops_json_str() {
|
|
62
|
+
command -v python3 >/dev/null 2>&1 || { printf '""'; return; }
|
|
63
|
+
printf '%s' "$1" | python3 -c 'import json,sys; sys.stdout.write(json.dumps(sys.stdin.read(), ensure_ascii=False))' 2>/dev/null || printf '""'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# ops_json_str_file <fichero> [max=OPS_MAX_TEXT] — igual, leyendo de un fichero y truncando
|
|
67
|
+
# al tope (marcado en el propio texto para que el servidor y el humano lo vean).
|
|
68
|
+
ops_json_str_file() {
|
|
69
|
+
local f="$1" max="${2:-$OPS_MAX_TEXT}"
|
|
70
|
+
[ -f "$f" ] || { printf '""'; return; }
|
|
71
|
+
command -v python3 >/dev/null 2>&1 || { printf '""'; return; }
|
|
72
|
+
python3 - "$f" "$max" <<'PY' 2>/dev/null || printf '""'
|
|
73
|
+
import json,sys
|
|
74
|
+
try:
|
|
75
|
+
t=open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
|
76
|
+
except Exception:
|
|
77
|
+
t=""
|
|
78
|
+
m=int(sys.argv[2])
|
|
79
|
+
if len(t) > m:
|
|
80
|
+
t = t[:m] + "\n…[truncado por el cliente]"
|
|
81
|
+
sys.stdout.write(json.dumps(t, ensure_ascii=False))
|
|
82
|
+
PY
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# ops_tmpjson <json> — materializa un JSON en un fichero temporal e imprime su ruta.
|
|
86
|
+
# Los payloads viajan a python3 y a curl POR FICHERO (regla dura del repo: nunca por argv).
|
|
87
|
+
ops_tmpjson() {
|
|
88
|
+
local f
|
|
89
|
+
f="$(mktemp)" || return 1
|
|
90
|
+
printf '%s' "$1" > "$f"
|
|
91
|
+
echo "$f"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ops_context_hashes — sha256 EFECTIVO local de cada archivo gobernado del lock
|
|
95
|
+
# (protocolo §3 regla 3: el servidor detecta drift/manipulación local comparándolos con los
|
|
96
|
+
# suyos). Se hashea el fichero real, NUNCA se repite el sha del lock: repetirlo convertiría
|
|
97
|
+
# el reporte en un espejo del servidor y el drift sería indetectable.
|
|
98
|
+
# Forma del reporte [#36]: el hub (`TasksNextIn.context_hashes: dict[str, str] | None`)
|
|
99
|
+
# exige un DICT {path: sha} — la lista [{"path":…,"sha256":…}] era 422 dict_type en TODO
|
|
100
|
+
# claim. Fallback {} (nunca []); fichero ilegible => "" (el dict no admite null en valores).
|
|
101
|
+
ops_context_hashes() {
|
|
102
|
+
local lock root
|
|
103
|
+
lock="$(runtime_lock_path)"
|
|
104
|
+
root="$(config_root)"
|
|
105
|
+
command -v python3 >/dev/null 2>&1 || { echo "{}"; return; }
|
|
106
|
+
python3 - "$lock" "$root" <<'PY' 2>/dev/null || echo "{}"
|
|
107
|
+
import json,sys,os,hashlib
|
|
108
|
+
lock,root=sys.argv[1],sys.argv[2]
|
|
109
|
+
try:
|
|
110
|
+
files=json.load(open(lock)).get("files") or []
|
|
111
|
+
except Exception:
|
|
112
|
+
files=[]
|
|
113
|
+
out={}
|
|
114
|
+
for f in files:
|
|
115
|
+
if not isinstance(f,dict):
|
|
116
|
+
continue
|
|
117
|
+
p=f.get("path")
|
|
118
|
+
if not isinstance(p,str) or not p:
|
|
119
|
+
continue
|
|
120
|
+
full=os.path.join(root,".claude",p)
|
|
121
|
+
sha=""
|
|
122
|
+
try:
|
|
123
|
+
h=hashlib.sha256()
|
|
124
|
+
with open(full,"rb") as fh:
|
|
125
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
126
|
+
h.update(chunk)
|
|
127
|
+
sha=h.hexdigest()
|
|
128
|
+
except Exception:
|
|
129
|
+
sha=""
|
|
130
|
+
out[p]=sha
|
|
131
|
+
print(json.dumps(out,ensure_ascii=False))
|
|
132
|
+
PY
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# ops_local_slice_ref — identidad del slice según el FICHERO local. Solo tiene sentido en
|
|
136
|
+
# modo `dual`, donde el fichero es primario y el runtime crea la identidad por el mismo
|
|
137
|
+
# camino que el import (spec §4.1-D). En `runtime` no se usa: allí manda la caché.
|
|
138
|
+
# Imprime {} si no hay fichero, no hay python3 o no hay slice activo — nunca lanza.
|
|
139
|
+
ops_local_slice_ref() {
|
|
140
|
+
local p
|
|
141
|
+
p="$(config_root)/.claude/state/build-state.json"
|
|
142
|
+
[ -f "$p" ] || { echo "{}"; return; }
|
|
143
|
+
command -v python3 >/dev/null 2>&1 || { echo "{}"; return; }
|
|
144
|
+
python3 - "$p" <<'PY' 2>/dev/null || echo "{}"
|
|
145
|
+
import json,sys
|
|
146
|
+
try:
|
|
147
|
+
d=json.load(open(sys.argv[1]))
|
|
148
|
+
s=d.get("active_slice") or {}
|
|
149
|
+
except Exception:
|
|
150
|
+
s={}
|
|
151
|
+
print(json.dumps({"epic_code": s.get("epica"),
|
|
152
|
+
"openspec_change": s.get("openspec_change"),
|
|
153
|
+
"branch": s.get("branch"),
|
|
154
|
+
"phase": s.get("phase")}, ensure_ascii=False))
|
|
155
|
+
PY
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ops_enqueue_from_file <type> <payload_file> [slice_id] — encola un evento cuyo payload
|
|
159
|
+
# puede ser grande (checklist de wiring, evidencia, contenido propuesto): el payload se lee
|
|
160
|
+
# del FICHERO dentro de python3, así que no pasa por argv (a diferencia de
|
|
161
|
+
# runtime_enqueue_event, que sigue sirviendo para payloads pequeños).
|
|
162
|
+
ops_enqueue_from_file() {
|
|
163
|
+
local type="$1" pfile="$2" slice_id="${3:-}" dir
|
|
164
|
+
dir="$(runtime_outbox_dir)"
|
|
165
|
+
mkdir -p "$dir"
|
|
166
|
+
command -v python3 >/dev/null 2>&1 || return 1
|
|
167
|
+
python3 - "$dir" "$type" "$pfile" "$slice_id" <<'PY' 2>/dev/null || return 1
|
|
168
|
+
import json,sys,os,tempfile,uuid,datetime
|
|
169
|
+
dir_,typ,pfile,slice_id=sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4]
|
|
170
|
+
try:
|
|
171
|
+
payload=json.load(open(pfile))
|
|
172
|
+
except Exception:
|
|
173
|
+
payload={}
|
|
174
|
+
cid=str(uuid.uuid4())
|
|
175
|
+
ts=datetime.datetime.now(datetime.timezone.utc)
|
|
176
|
+
d={"client_event_id":cid,"type":typ,"payload":payload,
|
|
177
|
+
"enqueued_at":ts.strftime("%Y-%m-%dT%H:%M:%S.%fZ")}
|
|
178
|
+
if slice_id:
|
|
179
|
+
d["slice_id"]=slice_id
|
|
180
|
+
fname=os.path.join(dir_, ts.strftime("%Y%m%d%H%M%S%f")+"-"+cid+".json")
|
|
181
|
+
fd,tmp=tempfile.mkstemp(dir=dir_,prefix=".outbox.",suffix=".tmp")
|
|
182
|
+
try:
|
|
183
|
+
with os.fdopen(fd,"w") as out:
|
|
184
|
+
json.dump(d,out,indent=2,ensure_ascii=False); out.flush(); os.fsync(out.fileno())
|
|
185
|
+
os.replace(tmp,fname)
|
|
186
|
+
except Exception:
|
|
187
|
+
try: os.unlink(tmp)
|
|
188
|
+
except OSError: pass
|
|
189
|
+
raise
|
|
190
|
+
PY
|
|
191
|
+
runtime_outbox_enforce_cap >/dev/null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# ops_mirror <kind> <payload_file> — ingesta espejo del modo `dual` (spec §4.1-D). El
|
|
195
|
+
# fichero local sigue siendo primario: un rechazo del reducer NO bloquea el trabajo, queda
|
|
196
|
+
# como discrepancia del comparador. rc 0 aceptado · 5 no se pudo (red) · 6 rechazado.
|
|
197
|
+
ops_mirror() {
|
|
198
|
+
local kind="$1" pfile="$2" pid body resp status tmp
|
|
199
|
+
pid="$(runtime_field project_id)"
|
|
200
|
+
[ -n "$pid" ] || return 5
|
|
201
|
+
command -v python3 >/dev/null 2>&1 || return 5
|
|
202
|
+
tmp="$(mktemp)" || return 5
|
|
203
|
+
python3 - "$kind" "$pfile" > "$tmp" <<'PY' 2>/dev/null
|
|
204
|
+
import json,sys,datetime
|
|
205
|
+
kind,pfile=sys.argv[1],sys.argv[2]
|
|
206
|
+
try:
|
|
207
|
+
payload=json.load(open(pfile))
|
|
208
|
+
except Exception:
|
|
209
|
+
payload={}
|
|
210
|
+
print(json.dumps({"kind":kind,"payload":payload,"origin":"mirror",
|
|
211
|
+
"occurred_at":datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")},
|
|
212
|
+
ensure_ascii=False))
|
|
213
|
+
PY
|
|
214
|
+
body="$(cat "$tmp" 2>/dev/null)"
|
|
215
|
+
rm -f "$tmp"
|
|
216
|
+
[ -n "$body" ] || return 5
|
|
217
|
+
resp="$(runtime_post "$(ops_path_mirror "$pid")" "$body")"
|
|
218
|
+
status="$(runtime_http_status)"
|
|
219
|
+
case "$status" in
|
|
220
|
+
200|201|202) return 0 ;;
|
|
221
|
+
409|422)
|
|
222
|
+
echo "⚠ el espejo rechazó «${kind}»: $resp" >&2
|
|
223
|
+
echo " (queda como discrepancia del comparador dual; el trabajo local NO se bloquea)" >&2
|
|
224
|
+
return 6 ;;
|
|
225
|
+
*) return 5 ;;
|
|
226
|
+
esac
|
|
227
|
+
}
|
|
@@ -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,171 @@
|
|
|
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
|
+
# [#44] SIN fallback offline: `release_gate_verdict` es del agregado `release` y
|
|
82
|
+
# POST /events solo ingiere ámbito slice (slice_id obligatorio y slice existente,
|
|
83
|
+
# progress_service._apply_and_append) o proyecto — el evento encolado era rechazo
|
|
84
|
+
# garantizado al drenar y el veredicto jamás llegaba. Mismo patrón que
|
|
85
|
+
# propose-asset: rc 5 y reintento explícito al reconectar.
|
|
86
|
+
echo "OFFLINE: no se pudo reportar el veredicto de release (status $status) y este acto" >&2
|
|
87
|
+
echo " no tiene camino por la cola offline. Reintenta cuando haya red:" >&2
|
|
88
|
+
echo " release-ops.sh verdict $line $gate $estado" >&2
|
|
89
|
+
return $OPS_RC_OFFLINE ;;
|
|
90
|
+
esac
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# ── close-hint ───────────────────────────────────────────────────────────────────────
|
|
94
|
+
# Cero red a propósito: es un recordatorio de gobierno, no una llamada.
|
|
95
|
+
cmd_close_hint() {
|
|
96
|
+
local line="${1:-}"
|
|
97
|
+
[ -n "$line" ] || { usage; return $OPS_RC_USAGE; }
|
|
98
|
+
cat <<HINT
|
|
99
|
+
El cierre de la release «${line}» es un acto de GOBIERNO HUMANO (spec §2):
|
|
100
|
+
1. El agente reporta los 6 veredictos con \`release-ops.sh verdict $line <gate> <estado>\`.
|
|
101
|
+
2. El runtime agrega: parciales NO promueven a passed.
|
|
102
|
+
3. Una persona con permiso cierra la línea en la consola del hub.
|
|
103
|
+
El arnés no cierra releases ni con token de agente: esa superficie exige PDP.
|
|
104
|
+
HINT
|
|
105
|
+
return $OPS_RC_OK
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# ── front-integration ────────────────────────────────────────────────────────────────
|
|
109
|
+
# Cada worktree se registra como AGENTE PROPIO (mismo token de proyecto, `agent_key`
|
|
110
|
+
# distinto): reporta la integración de SU miembro. Planificar, abrir, drenar y cerrar el
|
|
111
|
+
# front siguen siendo humanos.
|
|
112
|
+
cmd_front_integration() {
|
|
113
|
+
local front="${1:-}" merge="" resmoke="" ev="" evfile="" mode akey payload resp status pfile
|
|
114
|
+
[ -n "$front" ] || { usage; return $OPS_RC_USAGE; }
|
|
115
|
+
shift
|
|
116
|
+
while [ $# -gt 0 ]; do
|
|
117
|
+
case "$1" in
|
|
118
|
+
--merge-status) merge="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
119
|
+
--resmoke) resmoke="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
120
|
+
--evidence) ev="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
121
|
+
--evidence-file) evfile="${2:-}"; [ $# -ge 2 ] && shift 2 || shift ;;
|
|
122
|
+
*) shift ;;
|
|
123
|
+
esac
|
|
124
|
+
done
|
|
125
|
+
case "$merge" in
|
|
126
|
+
pending|merged|conflict) : ;;
|
|
127
|
+
*) echo "⛔ --merge-status debe ser pending|merged|conflict" >&2; return $OPS_RC_REJECTED ;;
|
|
128
|
+
esac
|
|
129
|
+
case "$resmoke" in
|
|
130
|
+
""|pass|fail) : ;;
|
|
131
|
+
*) echo "⛔ --resmoke debe ser pass|fail" >&2; return $OPS_RC_REJECTED ;;
|
|
132
|
+
esac
|
|
133
|
+
mode="$(ops_mode)"
|
|
134
|
+
[ "$mode" = "legacy" ] && { echo "LEGACY: actualiza parallel_front.members[] en el fichero"; return $OPS_RC_LEGACY; }
|
|
135
|
+
payload="{\"merge_status\":$(ops_json_str "$merge"),\"re_smoke\":{\"status\":$(ops_json_str "$resmoke"),\"evidence\":$(_rel_evidence_json "$ev" "$evfile")}}"
|
|
136
|
+
if [ "$mode" = "dual" ]; then
|
|
137
|
+
pfile="$(ops_tmpjson "$payload")" || return $OPS_RC_OFFLINE
|
|
138
|
+
ops_mirror front_integration_reported "$pfile" || echo "⚠ integración de front no espejada; el comparador dual la marcará" >&2
|
|
139
|
+
rm -f "$pfile"
|
|
140
|
+
return $OPS_RC_OK
|
|
141
|
+
fi
|
|
142
|
+
akey="$(runtime_field agent_key)"
|
|
143
|
+
[ -n "$akey" ] || { echo "⛔ este worktree no está registrado como agente: corre \`trycore-build init\` en él" >&2; return $OPS_RC_REJECTED; }
|
|
144
|
+
resp="$(runtime_post "$(ops_path_front_integration "$(ops_urlenc "$front")" "$(ops_urlenc "$akey")")" "$payload")"
|
|
145
|
+
status="$(runtime_http_status)"
|
|
146
|
+
case "$status" in
|
|
147
|
+
200|201|202) echo "integración reportada: merge_status=$merge re_smoke=${resmoke:-n/a}"; return $OPS_RC_OK ;;
|
|
148
|
+
422) echo "⛔ 422 el servidor rechazó el reporte: $resp" >&2; return $OPS_RC_REJECTED ;;
|
|
149
|
+
401|403) echo "⛔ token sin permiso ($status): habla con el ADMIN." >&2; return $OPS_RC_REJECTED ;;
|
|
150
|
+
*)
|
|
151
|
+
# [#44] SIN fallback offline: `front_integration_reported` NO existe en el catálogo
|
|
152
|
+
# v2 del hub (el hecho del catálogo, `front_member_integrated`, es del agregado
|
|
153
|
+
# `front` y POST /events no lo ingiere) y la superficie síncrona de integración es
|
|
154
|
+
# de gobierno humano (`can_govern_front`). Encolarlo era rechazo garantizado: rc 5
|
|
155
|
+
# y reintento explícito al reconectar (mismo patrón que propose-asset).
|
|
156
|
+
echo "OFFLINE: no se pudo reportar la integración (status $status) y este acto no" >&2
|
|
157
|
+
echo " tiene camino por la cola offline. Reintenta cuando haya red:" >&2
|
|
158
|
+
echo " release-ops.sh front-integration $front --merge-status $merge${resmoke:+ --resmoke $resmoke}" >&2
|
|
159
|
+
return $OPS_RC_OFFLINE ;;
|
|
160
|
+
esac
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
SUB="${1:-}"
|
|
164
|
+
[ $# -gt 0 ] && shift
|
|
165
|
+
case "$SUB" in
|
|
166
|
+
verdict) cmd_verdict "$@"; exit $? ;;
|
|
167
|
+
close-hint) cmd_close_hint "$@"; exit $? ;;
|
|
168
|
+
front-integration) cmd_front_integration "$@"; exit $? ;;
|
|
169
|
+
""|-h|--help|help) usage; exit $OPS_RC_USAGE ;;
|
|
170
|
+
*) echo "subcomando desconocido: $SUB" >&2; usage; exit $OPS_RC_USAGE ;;
|
|
171
|
+
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
|